SwiftUI 入门之 NavigationSplitView 应用

孙康

SwiftUI 于 2019 年度 WWDC 全球开发者大会上发布,它是基于 Swift 建立的声明式框架。初识 SwiftUI 感觉很是怪异,完全依靠编码实现用户界面,没有 Storyboard 所见即所得来的舒适。但真的使用过后,发现 SwiftUI 这种描述式的构建方式非常简洁(Swift 相比于 Objective-C 也更加简洁明了,两者可谓相辅相成),代码量会减少很多。不过 Xcode 上界面预览的支持有点差,很多时候还需要调试后查看编写效果。

在 macOS 上使用 SwiftUI 开发应用,SplitView 可能是最常使用的布局模式之一,大量的系统原生应用(如设置、邮件等)均采用了这种布局方式。NavigationSplitView 是非常方便使用的显示两到三列视图的容器,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Two column view
NavigationSplitView {
// Sidebar
} detail: {
// Detail
}

// Three column view
NavigationSplitView {
// Sidebar
} content: {
// Sub menu
} detail: {
// Detail
}

NavigationSplitView 一般会搭配 List 使用,以渲染列表视图。如下实现了一个简单的两列视图。

两列 SplitView 视图
两列 SplitView 视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import SwiftUI

struct EmployeeModel {
var name: String
var details: String
}

struct ContentView: View {
@State var selectedIndex = 0
let model = [EmployeeModel(name: "ZhangSan", details: "C/C++, Swift"),
EmployeeModel(name: "LiSi", details: "macOS Kernel, objc"),
EmployeeModel(name: "WangWu", details: "Linux, C++")]

var body: some View {
NavigationSplitView {
List(0 ..< model.count, id: \.self, selection: $selectedIndex) { index in
Text(model[index].name)
}
} detail: {
Text(model[selectedIndex].details)
}

}
}

#Preview {
ContentView()
}

首先定义了数据结构为结构体数组,然后使用 List 遍历该数组显示所有项目。这里 id: \.self 表示使用变量自身作为列表元素的标识符,以便于系统正确管理列表数据。另外使用 @State 声明了一个表示用户选择索引的变量 selectedIndex,并在使用 List 遍历时将其绑定到当前列表。这样每次用户选择列表某一行,将触发 selectedIndex 更新,进而重新渲染视图。@State 是一种属性包装器,这里不做具体解释。

有些时候可能会有将相关联的项目分组显示的场景,那么可以在 List 中使用 SectionGroup 等容器,而不要嵌套使用 List。那么这里将代码改动一下,使用 Section 进行分组显示,并使用 ForEach 将每一个元素插入列表。详细视图这里暂不处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct ContentView: View {
@State var selectedIndex = 0
let model = [[EmployeeModel(name: "ZhangSan", details: "C/C++, Swift"),
EmployeeModel(name: "LiSi", details: "macOS Kernel, objc"),
EmployeeModel(name: "WangWu", details: "Linux, C++")],
[EmployeeModel(name: "SuSan", details: "C/C++, Swift"),
EmployeeModel(name: "LiSi", details: "macOS Kernel, objc"),
EmployeeModel(name: "WangWu", details: "Linux, C++")]]

var body: some View {
NavigationSplitView {
List(0 ..< model.count, id: \.self, selection: $selectedIndex) { index in
Section("Section \(index)") {
ForEach(0 ..< model[index].count, id: \.self) { i in
Text(model[index][i].name)
}
}
}
} detail: {
// Text(model[selectedIndex].details)
}

}
}

奇怪的现象发生了,如下图,当我们选择某个组的其中一项时,会发现其他组的同位置项也被选中了。这是因为系统将不同 Section 中的同位置项目当成了一个,毕竟我们要求系统以变量自身作为列表元素的标识符,这里系统可能是以元素在 Section 中的顺序作为标识符。

侧边栏索引异常
侧边栏索引异常

修复该问题也很简单,自定义不会重复的标识符即可。可以显式指定 Texttag,使得标识不会重复,这样用户选择列表元素时就不会单击选中多个。需要注意的是,这里的 tag 就是 selectedIndex,所以在详细视图处理时需要将其转化为正确的索引量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct ContentView: View {
@State var selectedIndex = 0
let model = [[EmployeeModel(name: "ZhangSan", details: "C/C++, Swift"),
EmployeeModel(name: "LiSi", details: "macOS Kernel, objc"),
EmployeeModel(name: "WangWu", details: "Linux, C++")],
[EmployeeModel(name: "SuSan", details: "C/C++, Swift"),
EmployeeModel(name: "LiSi", details: "macOS Kernel, objc"),
EmployeeModel(name: "WangWu", details: "Linux, C++")]]

var body: some View {
NavigationSplitView {
List(0 ..< model.count, id: \.self, selection: $selectedIndex) { index in
Section("Section \(index)") {
ForEach(0 ..< model[index].count, id: \.self) { i in
let tag = index == 0 ? i : model[index].count + i
Text(model[index][i].name)
.tag(tag)
}
}
}
} detail: {
let sectionIndex = selectedIndex >= model[0].count ? 1 : 0
let itemIndex = sectionIndex == 0 ? selectedIndex : selectedIndex - model[0].count
Text(model[sectionIndex][itemIndex].details)
}

}
}

侧边栏索引正常
侧边栏索引正常

  • Title: SwiftUI 入门之 NavigationSplitView 应用
  • Author: 孙康
  • Created at : 2023-09-01 22:31:41
  • Updated at : 2023-08-31 20:01:11
  • Link: https://conradsun.github.io/2023/09e9decc83.html
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments