iOS 17中,以前的 NavigationView
已经被 NavigationStack
和 NavigationSplitView
所替代。
NavigationSplitView
允许我们在较大的设备(iPadOS、macOS 和横向的大型 iPhone)上创建多列布局,但当空间有限时会自动折叠为 NavigationStack
样式的布局。即在大多数 iPhone 上,导航标题将缩小为小文本,以便占用更少的空间;但在最大尺寸的 iPhone 上,您会看到标题变成了左上角的蓝色按钮,点击该按钮主视图会从边缘滑入。
// 以下代码,您看到的内容取决于设备和方向。在最大尺寸的手机和 iPad 上,会先看到“Secondary”,而主工具栏按钮会显示“Hello,world!”
NavigationSplitView(
sidebar: {
List(1..<50) { i in
NavigationLink("Row \\(i)", value: i)
}
// 通常情况下,需要向侧边栏添加某种选择机制,用户点击后,会在 detail 视图中加载选择的目标视图
.navigationDestination(for: Int.self) {
Text("Selected row \\($0)")
}
.navigationTitle("Split View")
},
detail: {
// 这个算是默认视图,如果用户还没有点任何链接,这个是默认显示在 detail 辅助视图上的
Text("Secondary")
}
)
截止到现在,以上特性都有一些缺点,希望这些缺点能够在未来得到修复:
.navigationBarHidden(true)
来隐藏它.toolbar(.hidden, for: .navigationBar)
在详细视图中隐藏工具栏,但要小心,因为它会隐藏切换侧边栏的按钮!struct UltimatePortfolioApp: App {
var body: some Scene {
WindowGroup {
//第一个闭包可以省略参数名
NavigationSplitView {
SidebarView()
} content: {
ContentView()
} detail: {
DetailView()
}
}
}
}
columnVisibility
实际上是作为绑定提供的,因此您可以将选项存储在某种状态并动态更新。
//例如希望在空间受限时,也保留主工具栏视图
NavigationSplitView(columnVisibility: .constant(.all)) {
NavigationLink("Primary") {
Text("New view")
}
} detail: {
Text("Content")
.navigationTitle("Content View")
}
.navigationSplitViewStyle(.balanced)
//这要求主副视图以 balance 策略显示,结果是纵向模式下的 iPad 现在也将显示主工具视图
其次,您可以告诉系统【默认情况】下更喜欢详细视图,这对于选择主视图作为标准的 iPhone 很有帮助:
NavigationSplitView(preferredCompactColumn: .constant(.detail)) {...}
// 强制选择详细视图
// 如果您提供的值不存在,例如您要求它选择内容视图(但实际上只有侧边栏和详细视图),那么 SwiftUI 将只选择侧边栏
struct ContentView: View {
@State private var preferredColumn = NavigationSplitViewColumn.detail
var body: some View {
NavigationSplitView(preferredCompactColumn: $preferredColumn) {
Text("Sidebar View")
} detail: {
Text("Detail View")
}
}
}
inspector()
修饰符可以在任何需要的地方添加检查器视图。这就像 Xcode 一样:检查器从 UI 的末端出现,并且可以根据需要与 NavigationStack
或 NavigationSplitView
一起工作。
struct ContentView: View {
@State private var isShowingInspector = false
var body: some View {
Button("Hello, world!") {
isShowingInspector.toggle()
}
.font(.largeTitle)
.inspector(isPresented: $isShowingInspector) {
Text("Inspector View")
}
}
}
在支持它的平台上,可以通过为其提供固定大小 ( .inspectorColumnWidth(500)
) 或通过为其提供大小范围 ( .inspectorColumnWidth(min: 50, ideal: 150, max: 200)
) 来调整检查器占用的空间大小。该修饰符应该应用于检查器的内容,如下所示:
struct ContentView: View {
@State private var isShowingInspector = false
var body: some View {
Button("Hello, world!") {
isShowingInspector.toggle()
}
.font(.largeTitle)
.inspector(isPresented: $isShowingInspector) {
Text("Inspector View")
.inspectorColumnWidth(min: 50, ideal: 150, max: 200)
}
}
}
<aside> 💡 提醒:ideal 理想尺寸将用于首次显示时检查器的尺寸,但系统会记住用户的更改。
</aside>
在以下示例中,我们利用 inspector()
修饰符来模拟四栏布局。请确保您的 SwiftUI 版本支持该修饰符:
SidebarView
是最左边的侧边栏,放置“分类”列表。ContentView
是中间的内容视图,根据选择的分类展示文章列表。DetailView
是右边的详细视图,根据选择的具体文章展示文章的具体内容。InspectorView
是第四栏,使用 inspector()
修饰符来添加。import SwiftUI
struct ContentView: View {
var body: some View {
NavigationSplitView {
SidebarView()
} content: {
ContentView()
} detail: {
DetailView()
}
.inspector(isPresented: .constant(true)) {
InspectorView()
}
}
}
struct SidebarView: View {
var body: some View {
List {
Text("Category 1")
Text("Category 2")
Text("Category 3")
}
.navigationTitle("Sidebar")
}
}
struct ContentView: View {
var body: some View {
List {
Text("Article 1")
Text("Article 2")
Text("Article 3")
}
.navigationTitle("Content")
}
}
struct DetailView: View {
var body: some View {
Text("Detail View")
.navigationTitle("Detail")
}
}
struct InspectorView: View {
var body: some View {
Text("Inspector View")
.navigationTitle("Inspector")
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
注意:系统可以选择忽略您指定的宽度。在撰写本文时,此修饰符在 iPhone 上被忽略,并且在 iPad 上仅适用于低于默认大小的值。
NavigationSplitView
使用系统标准宽度呈现它的视图;使用 navigationSplitViewColumnWidth()
修饰符则可以进行自定义。在最简单的形式中,向 navigationSplitViewColumnWidth()
发送单个值会导致它使用固定大小,不小于或大于。
NavigationSplitView {
Text("Sidebar")
.navigationSplitViewColumnWidth(100)
} content: {
Text("Content")
.navigationSplitViewColumnWidth(200)
} detail: {
Text("Detail")
}
// 如果要支持灵活性(目前可能只是 macOS),您可以提供最小、理想和最大尺寸,如下所示:
NavigationSplitView {
Text("Sidebar")
.navigationSplitViewColumnWidth(min: 100, ideal: 200, max: 300)
} content: {
Text("Content")
.navigationSplitViewColumnWidth(min: 100, ideal: 200, max: 300)
} detail: {
Text("Detail")
}
在 macOS 和 iPadOS 上使用 NavigationSplitView
时,SwiftUI 允许使用 NavigationSplitViewVisibility
枚举,显示侧边栏、内容视图和详细视图。例如以下代码:
struct ContentView: View {
@State private var columnVisibility = NavigationSplitViewVisibility.detailOnly
var body: some View {
// 提供 columnVisibility 是使用绑定完成的,因为当用户与您的 UI 交互时,您的值将自动更新
NavigationSplitView(columnVisibility: $columnVisibility) {
Text("Sidebar")
} content: {
Text("Content")
} detail: {
VStack {
// 在 .detailOnly 模式下,detail 视图将占用应用程序的所有可用屏幕空间
Button("Detail Only") {
columnVisibility = .detailOnly
}
// 在 .doubleColumn 模式下,您将看到 content 视图和 detail 视图
Button("Content and Detail") {
columnVisibility = .doubleColumn
}
// 在 .all 模式下,系统将尝试显示所有三个视图(如果存在)。如果您没有 content 视图(中间视图),它只会显示两个
Button("Show All") {
columnVisibility = .all
}
// 在 .automatic 模式下,系统将根据当前设备和方向执行其认为最好的操作
}
}
}
}
NavigationSplitView
有三个选项来控制侧边栏的显示方式,每个选项都可以使用 navigationSplitViewStyle()
修饰符进行调整。
NavigationSplitView {
Text("Sidebar")
} content: {
Text("Content")
} detail: {
Text("Detail")
}
.navigationSplitViewStyle(.prominentDetail)
其中参数值有以下几个选项:
.prominentDetail
:代表详细信息视图始终保持其完整大小,侧边栏和内容视图将被挤压,而不能挤压详细视图.balanced
:代表当显示侧边栏或内容栏时,它将减小详细视图的大小.automatic
:默认值,根据平台的不同而有所不同在 NavigationSplitView
中使用 NavigationLink
,与在传统的 NavigationStack
中不同。NavigationSplitView
是为了在 iPad 或者Mac 这样的多栏界面中使用而设计的,它自然地支持在侧边栏(sidebar)、内容视图(content view)和详细视图(detail view)之间的导航。
例如当使用 NavigationLink
时,如果代码中指定了导航的目的视图。那在 NavigationSplitView
的结构中点击 NavigationLink
后 ,实际上是会导致内容视图(content view)的更新,在里面呈现前面指定的视图内容。而不是直接跳转到新的视图。这是 NavigationSplitView
的设计逻辑决定的。简单说就是跳转的视图永远是 contentView,只是在 contentView 容器里加载指定的目标视图内容。
//例如以下代码中,点击 NavigationLink "链接按钮1",会自动在二级视图中展示 “Text("新的二级视图")”
NavigationSplitView(
sidebar: {
NavigationLink{
//点击后会在辅助视图窗口中展示以下视图
Text("新的二级视图")
} label: {
Text("链接按钮1")
}
.navigationTitle("主视图")
},
detail: {
//这个是辅助视图中的默认视图
Text("二级视图")
}
)
当 NavigationLink
没有指定目标视图,改用了 value
的写法,但我们又没有设置 navigationDestination
修饰符。这时无法跳转到下一级视图。这时我们如果在 List
列表中,使用了 selection
绑定和 value
同样类型的数据,就可以实现跳转。并且可以把 value
值传递给其他视图使用。
List(selection: $dataController.selectedFilter){
ForEach(smartFilters){
item in
NavigationLink(value: Filter.all) {
Text("链接按钮\\(item.name)")
}
}
}
//在 NavigationSplitView 中使用 NavigationLink:
//1. 如果 NavigationLink 是显性指定了目标视图,那跳转没有任何问题,会自动在下一级辅助页面打开指定的目标视图
//2. 如果 NavigationLink 没有指定目标视图,而是用了 value 的形式创建,那无法正常跳转到下一级视图,会提示没设置 navigationDestination
//解决这个问题,正确写法是在 List 上设定绑定值,将 selection 绑定到一个与 NavigationLink 的 value 同等类型的属性上
第一种情况:如果没有设置 List
绑定
NavigationLink
中设置了 value
为 Filter.all
NavigationSplitView
中,它默认会将 content
视图设置为导航栈的目标视图。也就是说,当你点击 SidebarView
中的 NavigationLink
时,它应该会跳转到 ContentView
ContentView
中使用 @Environment(\\.presentationMode)
或其他方式来获取导航栈中的 Filter.all
值,所以 ContentView
无法感知这个值的变化,因此无法正确更新视图第二种情况:设置 List(selection: $dataController.selectedFilter)
List
的 selection
绑定属性。这意味着当点击 SidebarView
中的链接时,dataController.selectedFilter
属性会被更新,并且 ContentView
可以感知到这个变化,从而更新视图SidebarView
和 ContentView
之间的状态传递和同步总的来说在使用 NavigationSplitView
时,需要注意以下几点:
ContentView
能够感知导航栈中的值变化,可以使用 @Environment(\\.presentationMode)
或自定义的 @Published
属性List
的 selection
绑定属性可以更好地在视图之间传递状态这个问题的关键在于确保 ContentView
能够正确地感知和响应导航栈中值的变化,从而能够正确地更新视图。使用 List
的 selection
绑定属性是一种很好的方式来实现这一点。
//例如有一个三栏视图
NavigationSplitView(){
SidebarView()
} content: {
ContentView()
} detail: {
DetailView()
}
//其中在 SidebarView 中定义了链接,链接中有一个附加值 filter,它是一个结构体
NavigationLink(value: filter) {
Label(filter.name, systemImage: filter.icon)
}
在这里附加 filter 值有什么作用吗? 后面的视图可以怎么用上它吗?
filter
值是一个可以被后续视图使用的上下文信息。它可以被用来根据用户在侧边栏中的选择,在 ContentView
中显示不同内容。例如,可以在 ContentView
中根据传递的 filter
值来决定显示哪些数据或者信息。这种方式非常适合创建过滤或者分类查看数据的应用场景。NavigationLink
中,我们将 value
设置为 filter
。这意味着当用户点击这个链接时,导航栈中会压入一个 filter
值。这个 filter
值可以被后续的视图(如 ContentView
和 DetailView
)访问和使用。