持续创造,加快成长!这是我参与「日新计划 6 月更文应战」的第13天,点击检查活动详情

前言

默认状况下,SwiftUI供给的各种导航API在很大程度上是以用户直接输入为中心的——也便是说,导航是在体系呼应例如按钮的点击和标签切换等事件时由体系自身处理的。

然而,有时咱们或许想更直接地操控应用程序的导航履行办法,尽管SwiftUI在这方面依然不如UIKit或AppKit灵敏,但它的确供给了相当多的办法,让咱们在构建的视图中履行彻底自定义的导航。

切换标签(tabs)

让咱们先来看看咱们如何能操控当时在TabView中显现的标签。通常状况下,当用户手动点击每个标签栏中的一个项目时,标签就会被切换,但是经过在一个给定的TabView中注入一个挑选(selection)绑定,咱们能够观察并操控当时显现的标签。在这里,咱们要做的便是在两个标签之间切换,这两个标签是用整数01标记的:

struct RootView: View {
    @State private var activeTabIndex = 0
    var body: some View {
        TabView(selection: $activeTabIndex) {
            Button("Switch to tab B") {
                activeTabIndex = 1
            }
            .tag(0)
            .tabItem { Label("Tab A", systemImage: "a.circle") }
            Button("Switch to tab A") {
                activeTabIndex = 0
            }
            .tag(1)
            .tabItem { Label("Tab B", systemImage: "b.circle") }
        }
    }
}

但真正好的当地是,在识别和切换标签时,咱们并不只是局限于运用整数。相反,咱们能够自由地运用任何Hashable值来表明每个标签——例如经过运用一个枚举,其间包含咱们想要显现的每个标签的状况。然后咱们能够将这部分状况封装在一个ObservableObject中,这样咱们就能够很容易地注入到咱们的视图层次环境中:

enum Tab {
    case home
    case search
    case settings
}
class TabController: ObservableObject {
    @Published var activeTab = Tab.home
    func open(_ tab: Tab) {
        activeTab = tab
    }
}

有了上述内容,咱们现在能够用新的Tab类型来标记TabView中的每个视图,假如咱们再把TabController注入到视图层次结构的环境中,那么其间的任何视图都能够随时切换显现的Tab。

struct RootView: View {
    @StateObject private var tabController = TabController()
    var body: some View {
        TabView(selection: $tabController.activeTab) {
            HomeView()
                .tag(Tab.home)
                .tabItem { Label("Home", systemImage: "house") }
            SearchView()
                .tag(Tab.search)
                .tabItem { Label("Search", systemImage: "magnifyingglass") }
            SettingsView()
                .tag(Tab.settings)
                .tabItem { Label("Settings", systemImage: "gearshape") }
        }
        .environmentObject(tabController)
    }
}

例如,现在咱们的HomeView能够运用一个彻底自定义的按钮切换到设置标签——它只需求从环境中获取咱们的TabController,然后它能够调用open办法来履行标签切换,像这样:

struct HomeView: View {
    @EnvironmentObject private var tabController: TabController
    var body: some View {
        ScrollView {
            ...
            Button("Open settings") {
                tabController.open(.settings)
            }
        }
    }
}

很好! 别的,因为TabController是一个彻底由咱们操控的对象,咱们也能够用它来切换主视图层次结构以外的标签。例如,咱们或许想依据推送告诉或其他类型的服务器事件来切换标签,现在能够经过调用上述视图代码中的相同的open办法来完结。

要了解更多关于环境对象以及SwiftUI状况办理体系的其余部分,请检查本攻略。

操控导航仓库

就像标签视图一样,SwiftUI的NavigationView也能够被编程自定义操控。例如,假定咱们正在开发一个应用程序,在其主导航仓库中显现一个日历视图作为根视图,然后用户能够经过点击位于该应用程序导航栏中的修改按钮来打开一个日历修改视图。为了衔接这两个视图,咱们运用了一个NavigationLink,每逢点击一个给定的视图时,它就会主动将其压入到导航栈中:

struct RootView: View {
    @ObservedObject var calendarController: CalendarController
    var body: some View {
        NavigationView {
            CalendarView(
                calendar: calendarController.calendar
            )
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink("Edit") {
   										 CalendarEditView(
       										 calendar: $calendarController.calendar
   										 )
   										 .navigationTitle("Edit your calendar")
										}
                }
            }
            .navigationTitle("Your calendar")
        }
        .navigationViewStyle(.stack)
    }
}

在这种状况下,咱们在所有设备上运用仓库式导航风格,甚至是iPad,而不是让体系挑选运用哪种导航风格。

现在咱们假定,咱们想让咱们的CalendarView以自定义办法显现其修改视图,而不需求构建一个独自的实例。要做到这一点,咱们能够在修改按钮的NavigationLink中注入一个isActive绑定,然后将其传递给咱们的CalendarView:

struct RootView: View {
    @ObservedObject var calendarController: CalendarController
    @State private var isEditViewShown = false
    var body: some View {
        NavigationView {
            CalendarView(
                calendar: calendarController.calendar,
                isEditViewShown: $isEditViewShown
            )
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink("Edit", isActive: $isEditViewShown) {
                        CalendarEditView(
                            calendar: $calendarController.calendar
                        )
                        .navigationTitle("Edit your calendar")
                    }
                }
            }
            .navigationTitle("Your calendar")
        }
        .navigationViewStyle(.stack)
    }
}

假如咱们现在也更新CalendarView,使其运用@Binding绑定特点承受上述值,那么现在只需咱们想显现咱们的修改视图,就能够简单地将该特点设置为true,咱们的根视图的NavigationLink将主动被触发:

struct CalendarView: View {
    var calendar: Calendar
    @Binding var isEditViewShown: Bool
    var body: some View {
        ScrollView {
            ...
            Button("Edit calendar settings") {
                isEditViewShown = true
            }
        }
    }
}

当然,咱们也能够挑选将isEditViewShown特点封装在某种形式的ObservableObject中,例如NavigationController,就像咱们之前处理TabView时那样。

这便是咱们如何故自定义编程办法触发显现在咱们的用户界面中的NavigationLink——但假如咱们想在不给用户任何直接操控的状况下履行这种导航呢?

例如,咱们现在假定咱们正在开发一个包含导出功用的视频修改应用程序。当用户进入导出流程时,一个VideoExportView被显现为模态,一旦导出操作完结,咱们想把VideoExportFinishedView推送到该模态的导航栈中。

开始,这或许看起来十分棘手,因为(因为SwiftUI是一个声明式的UI结构)没有push办法,当咱们想在导航栈中增加一个新视图时,咱们能够调用该办法。事实上,在NavigationView中显现一个新视图的唯一内置办法是运用NavigationLink,它需求成为咱们视图层次结构自身的一部分。

也便是说,这些NavigationLink实际上纷歧定是可见的——所以在这种状况下,完成咱们方针的一个办法是在咱们的视图中增加一个躲藏的导航链接,然后咱们能够在视频导出操作完结后以编程办法触发该链接。假如咱们也在咱们的方针视图中躲藏体系供给的返回按钮,那么咱们就能够彻底锁定用户能够在这两个视图之间手动导航:

struct VideoExportView: View {
    @ObservedObject var exporter: VideoExporter
    @State private var didFinish = false
    @Environment(\.presentationMode) private var presentationMode
    var body: some View {
        NavigationView {
            VStack {
                ...
                Button("Export") {
                    exporter.export {
    didFinish = true
}
                }
                .disabled(exporter.isExporting)
                NavigationLink("Hidden finish link", isActive: $didFinish) {
                    VideoExportFinishedView(doneAction: {
                        presentationMode.wrappedValue.dismiss()
                    })
                    .navigationTitle("Export completed")
                    .navigationBarBackButtonHidden(true)
                }
                .hidden()
            }
            .navigationTitle("Export this video")
        }
        .navigationViewStyle(.stack)
    }
}
struct VideoExportFinishedView: View {
    var doneAction: () -> Void
    var body: some View {
        VStack {
            Label("Your video was exported", systemImage: "checkmark.circle")
            ...
            Button("Done", action: doneAction)
        }
    }
}

咱们在VideoExportFinishedView中注入一个doedAction闭包,而不是让它检索当时的presentationMode自身,是因为咱们希望解耦整个模态流程,而不只是是那个特定的视图。要了解更多信息,请检查 “解耦SwiftUI模态或具体视图”。

运用这样一个躲藏的NavigationLink绝对能够被认为是一个有点 “黑 “的解决方案,但它的作用十分好,假如咱们把一个导航链接看成是导航仓库中两个视图之间的衔接(而不只是是一个按钮),那么上述设置能够说是有意义的。

小结

尽管SwiftUI的导航体系依然不如UIKit和AppKit供给的体系灵敏,但它已经满意强壮,能够满意很多不同的运用情——-特别是当与SwiftUI十分全面的状况办理体系相结合时。

当然,咱们也能够挑选将咱们的SwiftUI视图层次包裹在托管操控器中,只运用UIKit/AppKit来完成咱们的导航代码。哪种解决方案是最合适的,或许取决于咱们在每个项目中实际想要履行多少自定义和程序化的导航。

感谢您的阅读!