19

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

4 Answers 4

24

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)
    }
Sign up to request clarification or add additional context in comments.

2 Comments

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.
thanks - this is why I wish we could pass actions, instead of SwiftUI forcing us to use bindings in certain components
16

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?

2 Comments

This only calls the function when a different tab is selected, but not when the same tab is tapped.
Don't forget to add the tag modifier to your tab items to make this work.
3

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()
    }
}

Comments

0

Koen's version was really helpful, I modified it to represent a working version of my code.

For my application I had the following problem: When the user logged in the home version of the videos tab was shown, but when pressing videos again, the user had to go to the videos version of that tab. And home version had a navigation in the sidemenu.

Koen's code can be shortened a bit, but it worked like a charm and I thank you for it.

struct MainView: View {
    @State private var selection = 0
    @Binding var shouldShowHome: Bool

    @State private var resetNavigationID = UUID()

    /** Navigate to tab view **/
    func navigate() -> some View {
        VStack {
            switch selection {
                case 0:
                    FolderVideoView(shouldShowHome: $shouldShowHome).id(self.resetNavigationID)
                case 1:
                    ChatView()
                case 2:
                    DocumentListView()
                case 3:
                    NewsView()
                case 4:
                    EventCalendarView()
                default:
                    EmptyView()
            }
        }
        .onAppear {
          print("Navigating to \(selection)")
        }
    }

    var body: some View {
        // Proxy binding to catch taps on selected tabs
        let selectable = Binding(
          get: { self.selection },
          set: { self.selection = $0

              // If recreating navigation ID, the tab can be relected, see tabContent()
              self.resetNavigationID = UUID()
          })

        TabView(selection: selectable)  {
            if GlobalData.sharedGlobal.role == "newuser" {
                navigate()
                    .tabItem {
                        Image("Chat")
                    }
                    .badge(chatViewModel.unreadMessagesCount)
                    .tag(1)
                navigate()
                    .tabItem {
                        Image("Video")
                    }
                    .tag(0)
            } else {
                navigate()
                    .tabItem {
                        Image("Video")
                    }
                    .tag(0)
                navigate()
                    .tabItem {
                        Image("Chat")
                    }
                    .badge(chatViewModel.unreadMessagesCount)
                    .tag(1)
            }
            navigate()
                .tabItem {
                    Image("Document")
                }
                .tag(2)
            navigate()
                .tabItem {
                    Image("News")
                }
                .tag(3)
            navigate()
                .tabItem {
                    Image("Event")
                }
                .tag(4)
        }
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.