前言默认情况下,SwiftUI提供的各种导航API主要以用户的直接输入为中心——即导航是响应系统响应(例如按钮点击和标签切换等事件)进行处理的系统本身。然而,有时我们可能希望更直接地控制应用程序导航的执行方式,虽然在这方面SwiftUI仍然不如UIKit或AppKit灵活,但它确实提供了很多方法让我们可以执行完全自定义在我们构建导航的视图中。切换标签页(tabs)我们先来看看如何控制TabView中当前显示的标签页。通常情况下,当用户手动点击每个标签栏中的项目时,标签就会被切换,但是通过在给定的TabView中注入一个选择(selection)绑定就可以了,我们可以观察和控制当前显示的标签。在这里,我们所要做的就是在两个标记为整数0和1的选项卡之间切换:"切换到标签B"){activeTabIndex=1}.tag(0).tabItem{Label("TabA",systemImage:"a.circle")}Button("切换到标签A"){activeTabIndex=0}.tag(1).tabItem{Label("TabB",systemImage:"b.circle")}}}}但真正好的部分是,我们在识别和切换标签时不限于整数。相反,我们可以自由使用任何Hashable值来表示每个标签——例如,通过使用包含我们要显示的每个标签的案例的枚举。然后我们可以将这部分状态封装在一个ObservableObject中,我们可以轻松地将其注入到我们的视图层次环境中:enumTab{casehomecasesearchcasesettings}tab:Tab){activeTab=tab}}有了上面的内容,我们现在可以用新的Tab类型View标记每个TabView,如果我们将TabController注入到视图层次结构的环境中,那么其中的任何视图都可以切换随时显示的选项卡。structRootView:View{@StateObjectprivatevartabController=TabController()varbody:someView{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("设置",systemImage:"gearshape")}}.environmentObject(tabController)}}例如,现在我们的HomeView可以通过一个完全自定义的按钮切换到设置选项卡——它只需要获取我们的TabController,然后它就可以调用open方法来执行标签切换,像这样:}}}非常好的!此外,由于TabController是一个完全在我们控制之下的对象,我们还可以使用它来切换主视图层次结构之外的选项卡例如,我们可能希望根据推送通知或其他类型的服务器事件选项卡进行切换,现在可以通过在上面的视图代码中调用相同的打开方法来完成。要了解有关环境对象和SwiftUI状态管理系统其余部分的更多信息,请查看本指南。控制导航堆栈就像选项卡视图一样,SwiftUI的NavigationView也可以通过编程来自定义控件。例如,假设我们正在开发一个应用程序,在其主导航堆栈中将日历视图显示为根视图,然后用户可以通过单击位于应用程序导航栏中的编辑按钮来打开日历编辑视图。为了连接这两个视图,我们使用NavigationLink,它会在点击给定视图时自动将其推送到导航堆栈:structRootView:View{@ObservedObjectvarcalendarController:CalendarControllervarbody:someView{calendar).toolbar{ToolbarItem(placement:.navigationBarTrailing){NavigationLink("Edit"){CalendarEditView(calendar:$calendarController.calendar).navigationTitle("Edityoucalendar")}}.navigationTitle("Yourcalendar")}.navigationViewStyle(.stack)}}在这种情况下,我们在所有设备上使用堆叠导航样式,甚至是iPad,而不是让系统选择使用哪种导航样式。现在假设,我们希望我们的CalendarView以自定义方式显示其编辑视图,而无需构建单独的实例。为此,我们可以在编辑按钮的NavigationLink中注入一个isActive绑定并将其传递给我们的CalendarView:structRootView:View{@ObservedObjectvarcalendarController:CalendarController@StateprivatevarisEditViewShown=falsevarbody:someView{NavigationView{CalendarView(calendar:calendarController.calendar,isEditViewShown:$isEditViewShown).toolbar{ToolbarItem(placementActive:.navigation"BarTrailing){Navigation"链接是:$isEditViewShown){CalendarEditView(calendar:$calendarController.calendar).navigationTitle("编辑你的日历")}}}.navigationTitle("Yourcalendar")}.navigationViewStyle(.stack)}}如果我们现在也更新CalendarView使其使用@Binding绑定属性接受上述值,那么现在,每当我们想要显示我们的编辑视图时,我们只需将此属性设置为true,我们的根视图的NavigationLink就会自动触发:structCalendarView:View{varcalendar:Calendar@BindingvarisEditViewShown:Boolvarbody:someView{{...Button("Editcalendarsettings"){isEditViewShown=true}}}}当然,我们也可以选择将isEditViewShown属性封装在一些各种形式的ObservableObject中,比如NavigationControllers,就像我们之前对TabViews所做的那样。这就是我们如何以编程方式自定义触发我们的UINavigationLink中显示的内容——但是如果我们想在不给用户任何直接控制的情况下执行这种导航怎么办?例如,现在假设我们正在开发一个包含导出功能的视频编辑应用程序。当用户进入导出过程时,一个VideoExportView显示为模态,一旦导出操作完成,我们希望将VideoExportFinishedView推送到模态的导航堆栈上。起初,这可能看起来很棘手,因为(因为SwiftUI是一个声明式UI框架)当我们想要将新视图添加到导航堆栈时,没有可以调用的push方法。事实上,在NavigationView中显示新视图的唯一内置方法是使用NavigationLink,它需要成为我们视图层次结构本身的一部分。也就是说,这些NavigationLinks实际上不必是可见的-所以在这种情况下,实现我们目标的一种方法是向我们的视图添加一个隐藏的导航链接,然后我们可以在视频中显示它在导出操作完成。如果我们还在目标视图中隐藏系统提供的后退按钮,那么我们就可以完全阻止用户在这两个视图之间手动导航:=false@Environment(\.presentationMode)privatevarpresentationModevarbody:someView{NavigationView{VStack{...Button("Export"){exporter.export{didFinish=true}}.disabled(exporter.isExporting)NavigationLink("隐藏完成链接",isActive:$didFinish){VideoExportFinishedView(doneAction:{presentationMode.wrappedValue.dismiss()}).navigationTitle("导出完成").navigationBarBackButtonHidden(true)}.hidden()}.navigationTitle("导出这个视频")}.navigationViewStyle(.stack)}}structVideoExportFinishedView:View{vardoneAction:()->Voidvarbody:someView{VStack{Label("Yourvideowasexported",systemImage:"checkmark.circle")...Button("Done",action:doneAction)}}}我们将doneAction闭包注入VideoExportFinishedView而不是让它检索当前的presentationMode本身,因为我们想解耦整个模态流,而不仅仅是那个特定的视图。有关详细信息,请参阅“解耦SwiftUI模式或详细信息视图”。像这样使用隐藏的NavigationLink绝对可以被认为是一种有点“hacky”的解决方案,但它确实工作得很好,如果我们将导航链接视为导航堆栈中两个视图之间的连接(而不仅仅是一个按钮),那么以上设置可以说是有道理的。总结尽管SwiftUI的导航系统仍然不如UIKit和AppKit提供的系统灵活,但它足以满足许多不同的用例——尤其是当与SwiftUI非常全面的状态管理系统结合使用时。当然,我们也可以选择将我们的SwiftUI视图层次结构包装在托管控制器中,并只使用UIKit/AppKit作为我们的导航代码。哪种解决方案最合适可能取决于我们实际希望在每个项目中执行多少自定义和编程导航。
