'SwiftUI TabView: how to detect click on a tab?

I would like to run a function each time a tab is tapped.

On the code below (by using onTapGesture) when I tap on a new tab, myFunction is called, but the tabview is not changed.

struct DetailView: View {
    var model: MyModel
    @State var selectedTab = 1
    
    var body: some View {
        TabView(selection: $selectedTab) {
            Text("Graphs").tabItem{Text("Graphs")}
               .tag(1)
            Text("Days").tabItem{Text("Days")}
               .tag(2)
            Text("Summary").tabItem{Text("Summary")}
               .tag(3)
        }
        .onTapGesture {
            model.myFunction(item: selectedTab)
        }
    }
}

How can I get both things:

  • the tabview being normally displayed
  • my function being called


Solution 1:[1]

As of iOS 14 you can use onChange to execute code when a state variable changes. You can replace your tap gesture with this:

.onChange(of: selectedTab) { newValue in
    model.myFunction(item: newValue)
}

If you don't want to be restricted to iOS 14 you can find additional options here: How can I run an action when a state changes?

Solution 2:[2]

Here is possible approach. For TabView it gives the same behaviour as tapping to the another tab and back, so gives persistent look & feel.

gif image

Full module code:

import SwiftUI

struct TestPopToRootInTab: View {
@State private var selection = 0
@State private var resetNavigationID = UUID()

var body: some View {

    let selectable = Binding(        // << proxy binding to catch tab tap
        get: { self.selection },
        set: { self.selection = $0

            // set new ID to recreate NavigationView, so put it
            // in root state, same as is on change tab and back
            self.resetNavigationID = UUID()
        })

    return TabView(selection: selectable) {
        self.tab1()
            .tabItem {
                Image(systemName: "1.circle")
            }.tag(0)
        self.tab2()
            .tabItem {
                Image(systemName: "2.circle")
            }.tag(1)
    }
}

private func tab1() -> some View {
    NavigationView {
        NavigationLink(destination: TabChildView()) {
            Text("Tab1 - Initial")
        }
    }.id(self.resetNavigationID) // << making id modifiable
}

private func tab2() -> some View {
    Text("Tab2")
}
}

struct TabChildView: View {
    var number = 1
    var body: some View {
        NavigationLink("Child \(number)",
                       destination: TabChildView(number: number + 1))
    }
}

struct TestPopToRootInTab_Previews: PreviewProvider {
    static var previews: some View {
        TestPopToRootInTab()
    }
}

Solution 3:[3]

The above answers work well except in one condition. If you are present in the same tab .onChange() won't be called. the better way is by creating an extension to binding

extension Binding {
 func onUpdate(_ closure: @escaping () -> Void) -> Binding<Value> {
    Binding(get: {
        wrappedValue
    }, set: { newValue in
        wrappedValue = newValue
        closure()
    })
}

the usage will be like this

TabView(selection: $selectedTab.onUpdate{ model.myFunction(item: selectedTab) }) {
        Text("Graphs").tabItem{Text("Graphs")}
           .tag(1)
        Text("Days").tabItem{Text("Days")}
           .tag(2)
        Text("Summary").tabItem{Text("Summary")}
           .tag(3)
    }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 pkamb
Solution 2 Rami Almofleh
Solution 3 veeresh ks