通过构建经典的待办事项应用程序来学习List、NavigationView的使用。实现动态填充List、编辑List、添加Item、界面导航功能。
主要内容:
- 填充列表
- 导航
- 编辑列表
- 生成新的项
1. 填充列表
1.1 创建列表
要拥有一个显示待办事项列表的List视图,请在ContentView中的代码输入以下命令:
var body: some View {
List{
HStack{ Image("work").resizable().frame(width: 50, height: 50)
Text("Write SwiftUI book")
}
HStack{ Image("personal").resizable().frame(width: 50, height: 50)
Text("Read Bible")
}
HStack{ Image("family").resizable().frame(width: 50, height: 50)
Text("Bring kids out to play")
}
HStack{ Image("family").resizable().frame(width: 50, height: 50)
Text("Fetch wife")
}
HStack{ Image("family").resizable().frame(width: 50, height: 50)
Text("Call mum")
}
}
}
说明:
- 通过List添加多行数据,
- 每一行包含一个图像和一个水平文本,通过HStack来包装
- 因为图像大小不同,大的图像会被扩展,除了屏幕大小,只显示了一部分。为了解决这个问题,我们应用. resizable修改器使图像适合于使用面积。
- 然后应用.frame修饰符将图像的大小限制为一个自定义的框架。
1.2 动态添加列表
目前,我们有一个静态列表视图,其中有5个固定的数据。现在让我们看看如何从一个数组拿出值填充每一行。
因为现在每一行都包含一个类别和一个文本,我们需要创建一个结构体来存储它们。
结构体允许我们创建多个值组成的复杂数据类型。然后,我们可以创建该结构的实例并填充值,在代码中中传递这些值。
1.2.1 定义一个Todo结构类型
struct Todo {
let name: String
let category: String
}
说明:
- 我们定义了一个Todo结构类型,它包含两个属性:名称和类别。
1.2.2 todos状态数组:
在ContentView.swift中声明Todo结构体的状态数组
@State private var todos = [
Todo(name:"Write SwiftUI book",category: "work"),
Todo(name:"Read Bible",category: "personal"),
Todo(name:"Bring kids out to play",category: "family"),
Todo(name:"Fetch wife",category: "family"),
Todo(name:"family",category: "Call mum")
]
说明:
- 我们使用一个状态变量todos数组,以便List视图中的项可以动态更新。
- 这里创建好每一项数据的数组,之后通过数组动态更新。
- 注意Swift是如何让创建Todo结构体的实例变得简单的。我们只需传入名称和类别的初始值。
1.2.3 List动态显示数组数据
var body: some View {
List{
ForEach(todos, id:\.name){ (todo) in
HStack{
Image(todo.category) .resizable().frame(width: 50, height: 50)
Text(todo.name)
}
}
}
}
说明:
- 在List视图中,我们使用ForEach,它接收一个数组,然后创建多个子视图。
- 我们必须提供id来唯一标识每一项。现在,我们提供.name来使用todo名称作为每一行的标识符。
- 当预览应用程序时,它显示的内容应该是要和以前一样的,只是这一次,将以编程方式填充行。
1.2.4 ID标识
上面是通过itme的名称来标识todo项的,现在,如果我们有多个名称相同的todo项,该怎么办?
如果有多个名称相同的待办事项,这将导致当我们试图删除行,列表视图会因为有多个相同名称的cell而不知道的要删除哪一行。为了解决这个问题,我们将添加一个唯一的标识符到Todo结构体。
struct Todo: Identifiable {
let id = UUID()
let name: String
let category: String
}
- 使用id来唯一标识一个item,这里使用UUID()函数来生成一个随机标识符
- 此时我们就是不需要手动设置id了。List视图将会自动使用id作为每行的标识符
2. 导航
接下来,我们将实现一个待办事项详细信息界面。即当用户点击一个待办事项,我们会跳转到一个单独的待办事项详细信息界面。
我们通过将我们的List包装在一个NavigationView中来实现这一点。
2.1 导航页面
代码:
var body: some View {
NavigationView{
List{
ForEach(todos, id:\.name){ (todo) in
…
}
}.navigationBarTitle("Todo Items")
}
}
说明:
- 使用navigationBarTitle方法给控件设置导航栏的标题
- 注意navigationBarTitle修饰符属于列表视图,而不是导航视图。
- 这是因为导航视图从右边通过push来显示新界面
- 每个界面都有自己的标题。如果标题是附加到导航视图,标题就被固定了。
- 通过附加的标题到导航视图的里面内容,标题可以更改为其内容的变化。
2.2 导航跳转
通过NavigationView包装的List视图允许我们在点击一行时导航到待办事项界面上。
我们可以通过给NavigationLink函数包装row item来导航到详情界面:
NavigationView{
List{
ForEach(todos, id:\.name){ (todo) in
NavigationLink(destination:
VStack{
Text(todo.name)
Image(todo.category)
.resizable()
.frame(width: 200, height: 200)
}
){
HStack{
…
}
}
}
}.navigationBarTitle("Todo Items")
}
说明:
- 注意,我们必须向NavigationLink提供一个参数destination,也就是点击项目时显示的视图。
- 这里代码中可以看到,视图将包括:Text和Image
- 当运行应用程序,点击一个item就会跳转到另一个界面,界面显示选择的项目的详细信息。
- 新界面的顶部栏也会显示带有上一个项目的符号
3. 编辑列表
3.1 删除项
在iOS中,要删除特定的行,我们通常向左滑动到显示一行上显示的Delete按钮。
要启用此功能,我们需要在控件末尾添加.onDelete()修饰符
.onDelete(perform: { indexSet in todos.remove(atOffsets: indexSet) })//加给单个item的
说明:
- onDelete提供了一个indexSet参数,它包含了ForEach视图中的项的位置。供我们查找删除项
- 我们将indexSet传递给todos的remove函数来删除特定的行。
3.2 重新安排行
控件允许用户重新排列List视图中的行,在ForEach视图末尾设置. onmove()修饰符。
NavigationView{
List{
...
}
.onDelete(perform: { indexSet in todos.remove(atOffsets: indexSet) })//加给单个item的
.onMove(perform: { indices, newOffset in
todos.move(fromOffsets: indices, toOffset: newOffset)
})
}
.navigationBarTitle("Todo Items")
.navigationBarItems(trailing: EditButton())
}
说明:
- .navigationBarItems(trailing: EditButton())给list增加编辑按钮
- onMove提供了索引参数fromOffsets和toOffset参数,从开始位置移动到新位置
- 直接将数组todos进行move操作,传入位置参数即可
- 只有当用户输入“Edit”模式时才能移动项目。因此,我们需要在导航栏中添加一个EditButton按钮,当用户点击“编辑”时,就可以继续移动了
- 另外,在Edit模式下,每个项目都显示一个Delete按钮,用户可以通过点击它快速删除项目。这是在编辑模式下自动启用,我们不需要添加任何代码!
结果:
4. 生成新的项
4.1 添加Add项
为了向List视图添加行,我们向todos数组中添加了一个新的todo项。
.navigationBarItems(
leading: Button(action: addTodo,
label: {
Text("Add")
}),
trailing: EditButton()
)
- 我们将在NavigationView左上角添加一个导航栏按钮项,用于添加待办事项:
- 我们必须在按钮的动作中指定一个函数来添加一个新的待办事项对待办事项。
- 实现addTodo函数用于添加item
4.2 创建AddTodoView
我们当前的新待办事项的名称和类别硬编码为" newTodo "和" work ",实际开发中是不会这样的,
因此,现在注释掉addTodo()函数,专门创建AddTodoView,之后通过ContentView来呈现的AddTodoView,供用户添加待办事项,这样就可以由用户来自定义添加爱办事项的具体内容。
struct AddTodoView: View {
var body: some View {
Text("Add Todo view")
}
}
- 设置有一个状态变量showAddTodoView来决定是否显示AddTodoView。它最初默认为false(不显示)。
@State private var showAddTodoView = false
.navigationBarItems(
leading: Button(action: {
self.showAddTodoView.toggle()
},
label: {
Text("Add")
}).sheet(isPresented: $showAddTodoView){
AddTodoView()
},
trailing: EditButton()
)
说明:
- 在按钮的动作中,我们调用showAddTodoView的toggle()来进行切换
- 要显示sheet,我们使用sheet修饰符,并将其附加到按钮上。
- 我们将showAddTodoView状态变量绑定到.sheet()修饰符的参数。
- 当如果showAddTodoView为true,则显示sheet
- 用户可以向下拖动工作表来关闭它。
- 但在我们的案例中,我们希望有两个文本字段和一个Add按钮,
- 当单击按钮时,以编程方式退出界面。
4.3 @Binding
我们首先添加一个Button视图和一个绑定到AddTodoView中的绑定变量showAddTodoView
代码:
.navigationBarItems(
leading: Button(action: {
self.showAddTodoView.toggle()
},
label: {
Text("Add")
}).sheet(isPresented: $showAddTodoView){
AddTodoView(showAddTodoView: self.$showAddTodoView )
},
trailing: EditButton()
)
struct AddTodoView: View {
@Binding var showAddTodoView: Bool
var body: some View {
Text("Add Todo view")
Button(action: {
self.showAddTodoView = false
},
label: {
Text("Done")
})
}
}
说明:
- 通过将showAddTodoView声明为@Binding,我们就声明了它的值将来自其他地方,并将被共享到AddTodoView和其他地方。
- ContentView和AddTodoView共享showAddTodoView值。当修改“AddTodoView”中的“showAddTodoView”时,则变化也会反射回ContentView,然后它会解散sheet。
4.4 添加用户输入框
接下来,我们将为Todo名称添加一个TextField,为用户添加一个Picker用来选择一个待办事项类别(“工作”、“家庭”、“个人”),还有一个“完成”按钮。
@State private var name: String = ""
@State private var selectedCategory = 0
var categoryTypes = ["family","personal","work"]
说明:
- 数组categoryTypes将存储我们将要定义的类别,这些类别会显示在pickerview中。
- 状态变量selectedCategory将存储所选类别的数组索引。
添加一个VStack,其中包含输入控件TextFiled
VStack{
Text("Add Todo").font(.largeTitle)
TextField("To Do name",text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.black).padding()
Text("Select Category")
Picker("",selection: $selectedCategory){
ForEach(0 ..< categoryTypes.count){
Text(self.categoryTypes[$0])
}
}.pickerStyle(SegmentedPickerStyle())
}.padding()
说明:
- Picker视图允许用户从一组已定义的项中选择某一项。
- ForEach循环遍历categoryTypes数组和选择器。
- 注意,我们在选择器视图的末尾添加了一个修饰符. pickerstyle (SegmentedPickerStyle())。
- 这实际上将我们的Picker视图显示为SegmentedControl。
- 注意如果我们不写这个修饰符会发生什么
4.5 将Todo项添加到Todos数组
我们有接受用户输入的字段,让我们实现添加待办事项到待办事项数组。要做到这一点,AddTodoView需要能够访问待办事项ContentView。
Button(action: {
self.showAddTodoView = false
todos.append(Todo(name: name, category: categoryTypes[selectedCategory]))
},
label: {
Text("Done")
})
- 在AddTodo视图中,我们通过@Binding接收todo:
- 记住@Binding允许我们访问ContentView中的待办事项
- 在Button的操作中,我们创建一个Todo,然后将其添加到待办事项:
注意:
- 注意,当使用append()时,新项会被添加到末尾,这意味着新的待办事项显示在最后一行。
- 这里还能看到数据源绑定的操作,SwiftUI的理念是“真正的简单数据来源”。也就是说,视图背后的数据只有一个源。
- 在我们的例子中,todos只有一个来源ContentView。AddTodoView中的待办事项指ContentView中的待办事项
- 通过@Binding关键字。使用@Binding允许我们进行绑定之间的数据视图。这可以防止同一文件的多个或多个副本导致数据不一致的数据。
总结
整体代码实现:
struct Todo {
let name: String
let category: String
}
struct ContentView: View {
@State private var todos = [
Todo(name:"Write SwiftUI book",category: "work"),
Todo(name:"Read Bible",category: "personal"),
Todo(name:"Bring kids out to play",category: "family"),
Todo(name:"Fetch wife",category: "family"),
Todo(name:"family",category: "family")
]
@State private var showAddTodoView = false
func addTodo(){
todos.append(Todo(name: "newTodo", category: "work"))
}
var body: some View {
NavigationView{
List{
ForEach(todos, id:\.name){ (todo) in
NavigationLink(destination:
VStack{
Text(todo.name)
Image(todo.category)
.resizable()
.frame(width: 200, height: 200)
}
){
HStack{
Image(todo.category) .resizable().frame(width: 50, height: 50)
Text(todo.name)
}
}
}
.onDelete(perform: { indexSet in
todos.remove(atOffsets: indexSet)
})//加给单个item的
.onMove(perform: { indices, newOffset in
todos.move(fromOffsets: indices, toOffset: newOffset)
})//设置位置可移动
}
.navigationBarTitle("Todo Items")
.navigationBarItems(
leading: Button(action: {
self.showAddTodoView.toggle()
},
label: {
Text("Add")
}).sheet(isPresented: $showAddTodoView){
AddTodoView(showAddTodoView: self.$showAddTodoView, todos:self.$todos)
},
trailing: EditButton()
)
}
}
}
struct AddTodoView: View {
@Binding var showAddTodoView: Bool
@State private var name: String = ""
@State private var selectedCategory = 0
var categoryTypes = ["family","personal","work"]
@Binding var todos: [Todo]
var body: some View {
// Text("Add Todo view")
VStack{
Text("Add Todo").font(.largeTitle)
TextField("To Do name",text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.black).padding()
Text("Select Category")
Picker("",selection: $selectedCategory){
ForEach(0 ..< categoryTypes.count){
Text(self.categoryTypes[$0])
}
}.pickerStyle(SegmentedPickerStyle())
}.padding()
Button(action: {
self.showAddTodoView = false
todos.append(Todo(name: name, category: categoryTypes[selectedCategory]))
},
label: {
Text("Done")
})
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
.previewInterfaceOrientation(.portraitUpsideDown)
}
}
}
运行结果: