11

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
Daniele B
  • 19,801
  • 29
  • 115
  • 173

3 Answers3

13

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?

pkamb
  • 33,281
  • 23
  • 160
  • 191
Joe
  • 3,664
  • 25
  • 27
  • 13
    This only calls the function when a different tab is selected, but not when the same tab is tapped. – pipacs Jul 09 '21 at 22:23
12

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)
    }
RyuX51
  • 2,779
  • 3
  • 26
  • 33
veeresh ks
  • 151
  • 1
  • 5
  • 1
    This should be the accepted answer – it is elegant and in harmony with SwiftUI working paradigms. Note that one can add parameters for ```oldValue``` and ```newValue``` to the ```onUpdate``` closure. – AVS Jun 04 '23 at 03:55
1

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

enter image description here

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()
    }
}
koen
  • 5,383
  • 7
  • 50
  • 89