SwiftUI:再次点按所选标签时,弹出到根视图

时间:2020-03-15 08:17:55

标签: swift swiftui

起点是TabView中的NavigationView。我正在努力寻找一个SwiftUI解决方案,以便在再次轻按所选标签时弹出导航堆栈内的根视图。在Swiftift之前的时代,这非常简单:

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

3 个答案:

答案 0 :(得分:6)

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

经过测试,可与Xcode 11.2 / iOS 13.2一起使用

demo

完整的模块代码:

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

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

答案 2 :(得分:0)

这是我的做法:

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

然后在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"))
                ),
            ])

        }
    }
}

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