【问题标题】:SwiftUI: Pop to root view when selected tab is tapped againSwiftUI:再次点击所选选项卡时弹出到根视图
【发布时间】:2020-06-26 16:16:06
【问题描述】:

起点是 TabView 中的 NavigationView。当再次点击选定的选项卡时,我正在努力寻找一个 SwiftUI 解决方案以弹出到导航堆栈中的根视图。在 SwiftUI 之前的时代,这很简单:

func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
    let navController = viewController as! UINavigationController
    navController.popViewController(animated: true)
}

你知道如何在 SwiftUI 中实现同样的事情吗?

目前,我使用以下依赖于 UIKit 的解决方法:

if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)

            let navigationController = UINavigationController(rootViewController: UIHostingController(rootView:
                MyCustomView() // -> this is a normal SwiftUI file
                    .environment(\.managedObjectContext, context)
            ))
            navigationController.tabBarItem = UITabBarItem(title: "My View 1", image: nil, selectedImage: nil)

            // add more controllers that are part of tab bar controller

            let tabBarController = UITabBarController()
            tabBarController.viewControllers = [navigationController /*,  additional controllers */]

            window.rootViewController = tabBarController // UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    这是可能的方法。对于TabView,它提供与点击另一个选项卡并返回相同的行为,因此具有持久的外观和感觉。

    经过测试并适用于 Xcode 11.2 / iOS 13.2

    完整模块代码:

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

    【讨论】:

    • 谢谢@Asperi。然而,这仍然没有给我与我们从 UIKit 知道的完全相同的行为。此外,在选项卡之间切换时,TabBar 不会保留导航堆栈。
    • @burki 我也对切换时的导航堆栈不满意。我用这个稍微修改过的 setter 进行了修复:``` set: { let oldSelection = self.selection self.selection = $0 // 设置新 ID 以重新创建 NavigationView,所以把它 // 置于根状态,与更改选项卡和返回 if selection == oldSelection { self.settingsNavigationId = UUID() } } ``` 我也喜欢漂亮的 pop 动画,它需要传递一个绑定变量,但我真的很喜欢这个解决方案的简单性。它仅限于单个文件。
    • 我对真实应用做了一些改进: 1. @State private var navIds: [ UUID ] = [ UUID(), UUID(),UUID(), UUID(), UUID()] 2 . let selectable = Binding(get: { self.tabSelection }, set: { if tabSelection == $0 { self.navIds[$0] = UUID() } self.tabSelection = $0 }) 3. TabView (selection: selectable){ Tab1(navId: navIds[0]) Tab2(navId: navIds[1]) ... }
    【解决方案2】:

    这里有一种方法,它使用 PassthroughSubject 来在重新选择选项卡时通知子视图,并使用视图修饰符允许您将 .onReselect() 附加到视图。

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

    【讨论】:

    • 迄今为止最好的解决方案。请注意,在触发action() 之前,我必须删除self.isVisible 签入ReselectTabViewModifieronAppear & onDisappear 不会持续触发。
    【解决方案3】:

    我是这样做的:

    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 {
        var nearestNavigationController: UINavigationController? {
            if let selfTypeCast = self as? UINavigationController {
                return selfTypeCast
            }
            if children.isEmpty {
                return nil
            }
            for child in self.children {
                return child.nearestNavigationController
            }
            return nil
        }
    }
    
    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)
        }
    }
    

    然后在 ContentView.swift 我这样使用它:

    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"))
                    ),
                ])
                
            }
        }
    }
    

    请注意,当用户已经在根视图上时,它会自动滚动到顶部

    【讨论】:

    • 看起来是个不错的解决方案。但是它说nearestNavigationController 未找到。它是怎么定义的?
    • @FilpeSa 有没有办法在你的 TabBarController: UIViewControllerRepresentable 中改变 tabview 控制器的高度?我尝试在 func makeUIViewController(context: Context) -&gt; UITabBarController { 中指定高度,但它什么也没做
    • @xiaolingxiao 如果有办法我不知道该怎么做。尝试再深入一点,一定有办法
    【解决方案4】:

    这是我使用 introspect swiftUI 库所做的。 https://github.com/siteline/SwiftUI-Introspect

    struct TabBar: View {
        @State var tabSelected: Int = 0
        @State var navBarOne: UINavigationController?
        @State var navBarTwo: UINavigationController?
        @State var navBarThree: UINavigationController?
    
        
        var body: some View {
            
           return  TabView(selection: $tabSelected){
               NavView(navigationView: $navBarOne).tabItem {
                   Label("Home1",systemImage: "bag.fill")
                }.tag(0)
                
               NavView(navigationView: $navBarTwo).tabItem {
                    Label("Orders",systemImage: "scroll.fill" )
                }.tag(1)
                
               NavView(navigationView: $navBarThree).tabItem {
                    Label("Wallet", systemImage: "dollarsign.square.fill" )
                   // Image(systemName: tabSelected == 2 ? "dollarsign.square.fill" : "dollarsign.square")
                }.tag(2)
        
            }.onTapGesture(count: 2) {
                switch tabSelected{
                case 0:
                    self.navBarOne?.popToRootViewController(animated: true)
                case 1:
                    self.navBarTwo?.popToRootViewController(animated: true)
                case 2:
                    self.navBarThree?.popToRootViewController(animated: true)
                default:
                    print("tapped")
                }
            }
        }
    }
    

    导航视图:

    import SwiftUI
    import Introspect
    
    struct NavView: View {
        
        @Binding var navigationView: UINavigationController?
        var body: some View {
            NavigationView{
                VStack{
                    NavigationLink(destination: Text("Detail view")) {
                        Text("Go To detail")
                    }
                }.introspectNavigationController { navController in
                    navigationView = navController
                }
            }
        }
    }
    

    【讨论】:

    • 这实际上不是最好的方法,因为它使整个选项卡视图及其内部的所有内容都具有双击手势,这会将视图弹出到其根目录。 Goona 打算只在 tabBar 项目本身上制作它。当我找到解决方案时,我会更新答案
    猜你喜欢
    • 2011-11-30
    • 2023-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-28
    • 1970-01-01
    • 2014-06-10
    相关资源
    最近更新 更多