SwiftUI: Pop to root view when selected tab is tapped again

后端 未结 3 931
轮回少年
轮回少年 2020-12-03 16:08

Starting point is a NavigationView within a TabView. I\'m struggling with finding a SwiftUI solution to pop to the root view within the navigation stack when the selected ta

相关标签:
3条回答
  • 2020-12-03 16:56

    Here's how I did it:

    struct UIKitTabView: View {
        var viewControllers: [UIHostingController<AnyView>]
    
        init(_ tabs: [Tab]) {
            self.viewControllers = tabs.map {
                let host = UIHostingController(rootView: $0.view)
                host.tabBarItem = $0.barItem
                return host
            }
        }
    
        var body: some View {
            TabBarController(controllers: viewControllers).edgesIgnoringSafeArea(.all)
        }
    
        struct Tab {
            var view: AnyView
            var barItem: UITabBarItem
    
            init<V: View>(view: V, barItem: UITabBarItem) {
                self.view = AnyView(view)
                self.barItem = barItem
            }
        }
    }
    
    
    struct TabBarController: UIViewControllerRepresentable {
        var controllers: [UIViewController]
    
        func makeUIViewController(context: Context) -> UITabBarController {
            let tabBarController = UITabBarController()
            tabBarController.viewControllers = controllers
            tabBarController.delegate = context.coordinator
            return tabBarController
        }
    
        func updateUIViewController(_ uiViewController: UITabBarController, context: Context) { }
    }
    
    extension TabBarController {
        func makeCoordinator() -> TabBarController.Coordinator {
            Coordinator(self)
        }
        class Coordinator: NSObject, UITabBarControllerDelegate {
            var parent: TabBarController
            init(_ parent: TabBarController){self.parent = parent}
            var previousController: UIViewController?
            private var shouldSelectIndex = -1
    
            func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
                shouldSelectIndex = tabBarController.selectedIndex
                return true
            }
    
            func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
                if shouldSelectIndex == tabBarController.selectedIndex {
                    if let navVC = tabBarController.viewControllers![shouldSelectIndex].nearestNavigationController {
                        if (!(navVC.popViewController(animated: true) != nil)) {
                            navVC.viewControllers.first!.scrollToTop()
                        }
                    }
                }
            }
        }
    }
    
    extension UIViewController {
        func scrollToTop() {
            func scrollToTop(view: UIView?) {
                guard let view = view else { return }
                switch view {
                case let scrollView as UIScrollView:
                    if scrollView.scrollsToTop == true {
                        scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.safeAreaInsets.top), animated: true)
                        return
                    }
                default:
                    break
                }
    
                for subView in view.subviews {
                    scrollToTop(view: subView)
                }
            }
            scrollToTop(view: view)
        }
    }
    

    Then in ContentView.swift I use it like this:

    struct ContentView: View {
        var body: some View {
            ZStack{
                UIKitTabView([
                    UIKitTabView.Tab(
                        view: FirstView().edgesIgnoringSafeArea(.top),
                        barItem: UITabBarItem(title: "Tab1", image: UIImage(systemName: "star"), selectedImage: UIImage(systemName: "star.fill"))
                    ),
                    UIKitTabView.Tab(
                        view: SecondView().edgesIgnoringSafeArea(.top),
                        barItem: UITabBarItem(title: "Tab2", image: UIImage(systemName: "star"), selectedImage: UIImage(systemName: "star.fill"))
                    ),
                ])
    
            }
        }
    }
    

    Note that when the user is already on the root view, it scrolls to top automatically

    0 讨论(0)
  • 2020-12-03 16:59

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

    Tested & works with Xcode 11.2 / iOS 13.2

    demo

    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()
        }
    }
    
    0 讨论(0)
  • 2020-12-03 17:04

    Here's an approach that uses a PassthroughSubject to notify the child view whenever the tab is re-selected, and a view modifier to allow you to attach .onReselect() to a view.

    import SwiftUI
    import Combine
    
    enum TabSelection: String {
        case A, B, C // etc
    
    }
    
    private struct DidReselectTabKey: EnvironmentKey {
        static let defaultValue: AnyPublisher<TabSelection, Never> = Just(.Mood).eraseToAnyPublisher()
    }
    
    private struct CurrentTabSelection: EnvironmentKey {
        static let defaultValue: Binding<TabSelection> = .constant(.Mood)
    }
    
    private extension EnvironmentValues {
        var tabSelection: Binding<TabSelection> {
            get {
                return self[CurrentTabSelection.self]
            }
            set {
                self[CurrentTabSelection.self] = newValue
            }
        }
    
        var didReselectTab: AnyPublisher<TabSelection, Never> {
            get {
                return self[DidReselectTabKey.self]
            }
            set {
                self[DidReselectTabKey.self] = newValue
            }
        }
    }
    
    private struct ReselectTabViewModifier: ViewModifier {
        @Environment(\.didReselectTab) private var didReselectTab
    
        @State var isVisible = false
        
        let action: (() -> Void)?
    
        init(perform action: (() -> Void)? = nil) {
            self.action = action
        }
            
        func body(content: Content) -> some View {
            content
                .onAppear {
                    self.isVisible = true
                }.onDisappear {
                    self.isVisible = false
                }.onReceive(didReselectTab) { _ in
                    if self.isVisible, let action = self.action {
                        action()
                    }
                }
        }
    }
    
    extension View {
        public func onReselect(perform action: (() -> Void)? = nil) -> some View {
            return self.modifier(ReselectTabViewModifier(perform: action))
        }
    }
    
    struct NavigableTabViewItem<Content: View>: View {
        @Environment(\.didReselectTab) var didReselectTab
    
        let tabSelection: TabSelection
        let imageName: String
        let content: Content
        
        init(tabSelection: TabSelection, imageName: String, @ViewBuilder content: () -> Content) {
            self.tabSelection = tabSelection
            self.imageName = imageName
            self.content = content()
        }
    
        var body: some View {
            let didReselectThisTab = didReselectTab.filter( { $0 == tabSelection }).eraseToAnyPublisher()
    
            NavigationView {
                self.content
                    .navigationBarTitle(tabSelection.localizedStringKey, displayMode: .inline)
            }.tabItem {
                Image(systemName: imageName)
                Text(tabSelection.localizedStringKey)
            }
            .tag(tabSelection)
            .navigationViewStyle(StackNavigationViewStyle())
            .keyboardShortcut(tabSelection.keyboardShortcut)
            .environment(\.didReselectTab, didReselectThisTab)
        }
    }
    
    struct NavigableTabView<Content: View>: View {
        @State private var didReselectTab = PassthroughSubject<TabSelection, Never>()
        @State private var _selection: TabSelection = .Mood
    
        let content: Content
    
        init(@ViewBuilder content: () -> Content) {
            self.content = content()
        }
    
        var body: some View {
            let selection = Binding(get: { self._selection },
                                    set: {
                                        if self._selection == $0 {
                                            didReselectTab.send($0)
                                        }
                                        self._selection = $0
                                    })
    
            TabView(selection: selection) {
                self.content
                    .environment(\.tabSelection, selection)
                    .environment(\.didReselectTab, didReselectTab.eraseToAnyPublisher())
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题