代码库

教程中的项目代码都保存在这里:https://gitee.com/KINGWDY_admin/swiftui01

前言

在这一章节中,我们会使用List控件做一个土豆List,实现了列表填充、增加记录、删除记录以及列表记录重排序。

当你点击列表中的todo记录时将会跳转到详情页,详情页包含todo标题的放大版以及图标的放大版。

新建项目

怎么新建一个项目我们在第一章中介绍过,这里就不再赘述了,新建好的项目长这个样子:

为了在List中展示todo记录,我们在ContentView文件中添加如下代码:

struct ContentView: View {
var body: some View {
List {
Text("吃饭")
Text("睡觉")
Text("看看书")
Text("打打红警")
}
}
}

效果图:



现在List中的每一行数据还很单调,只有文字。

我想要在每一行之中除了文字之外还要展示一个分类的图标,表示该条todo记录属于什么类别,所以使用HStatck包裹我们要在一行展示的数据:

struct ContentView: View {
var body: some View {
List {
HStack {
Image(systemName: "desktopcomputer")
.resizable()
.frame(width: 20, height: 20)
Text("Coding...")
}
HStack {
Image(systemName: "house")
.resizable()
.frame(width: 20, height: 20)
Text("健身")
}
HStack {
Image(systemName: "theatermasks")
.resizable()
.frame(width: 20, height: 20)
Text("相亲")
}
}
}
}

效果图:

如果 Image中使用的图片是自己从网上找的,大小可能不一致,这就可能导致图片超过了屏幕的大小,整个屏幕只显示了图片的一角。

这时就可以使用resizeable让图片自适应大小。

Image(systemName: "desktopcomputer").resizable().frame(width: 50, height: 50)

使用frame来限定图片的大小。

使用数组内容填充List

目前为止,我们通过写死代码的方式在List中展示了几条数据,接下来我们要使其能够动态变化。

新建一个 Todo 结构体,保存 todo 数据

ContentView.swift代码中增加一个结构体:

struct Todo {
let name: String // todo 标题
let category: String // todo 分类
}

新建一个 @State 修饰的数组

ContentView.swift中增加数组:

struct ContentView: View {
@State private var todos = [
Todo(name: "coding", category: "desktopcomputer"),
Todo(name: "健身", category: "house"),
Todo(name: "相亲", category: "theatermasks")
]
...
}

我们创建了一个数组todos,并且用@State修饰,这样List中的数据条目就可以动态更新了。

声明数组的同时,我们还新建了3条 todo 结构体放到了数组中。

填充数组内容到 List

var body: some View {
List {
ForEach(todos, id:\.name) { (todo) in
HStack {
Image(systemName: todo.category)
.resizable()
.frame(width: 20, height: 20)
Text(todo.name)
}
}
}
}

在 List 中我们使用ForEach来取出数组中的所有记录并展示。Image 中展示分类图片,Text 中展示 todo 的 name。

在 ForEach 中我们使用 .name来唯一标记一条 todo 记录(不应该这么做,只是临时方案),这里我们假设 todo 的 name 属性是唯一不会重复的。

你现在运行app的话,看到的效果跟之前把 todo 数据写死在代码里时是一样的。

在 NavigationView 中展示 List

接下来我们来实现点击一条 todo 记录后跳转到详情页,实现这个功能需要将 List 包裹在 NavigationView 中:

var body: some View {
NavigationView {
List {
ForEach(todos, id:\.name) {(todo) in
...
}
}.navigationBarTitle("土豆List")
}
}

我们还通过 navigationBarTitle 为当前页面设置了一个标题。

注意: 我们的 navigationBarTitle 是放在了 NavigationView中的 List上。之所以这么做是因为在点击了 List 中的某条记录后, NavigationView 将会展示一个新的页面,而不是现在的 List,每一个页面都应该有一个不同的标题,如果把 navigationBarTitle 放到 NavigationView 上,那么切换页面时标题就固定死了,不会变化的。

让 List 中的记录可以点击

将 List 包裹在 NavigationView 中可以让 List 在被点击后跳转到新的页面。

要使 List 中的 todo 记录被点击后跳转到详情页,修改代码中的 ForEach如下:

ForEach(todos, id:\.name) { (todo) in
NavigationLink(destination: {
VStack {
Text(todo.name)
Image(systemName: todo.category)
.resizable()
.frame(width: 200, height: 200)
}
}) {
HStack {
Image(systemName: todo.category)
.resizable()
.frame(width: 20, height: 20)
Text(todo.name)
}
}
}

我们在 NavigationLink中提供了一个参数表示详情页的 UI。

NavigationLink(destination:
VStack {
Text(todo.name)
Image(systemName: todo.category)
.resizable()
.frame(width: 200, height: 200)
})

当你运行app并且点击 List 中的某个 todo 记录后,app 将会跳转到一个新的详情页,新详情页的上方还会显示一个返回按钮。

删除一条 todo 记录

在 iOS 中我们通常会通过向左滑动来显示删除按钮或者直接删除。

为了使我们 app 中的 List 也具有这个功能,我们需要在 ForEach 的结尾处添加 onDelete 修饰符。

var body: some View {
NavigationView {
List {
ForEach(todos, id: \.name) { (todo) in
NavigationLink(destination:
...
}
} .onDelete(perform: {indexSet in
todos.remove(asOffsets: indexSet)})
}.navigationBarTitle("土豆List")
}
}

onDelete会产生一个变量indexSet,里面包含了所有要删除的 todo记录 的索引位置,我们将这个参数indexSet传入todos.remove方法实现移除数组中某些元素。

重排序列表中的记录

可以通过在ForEach的结尾处添加onMove()修饰符来实现改变List中记录顺序的效果:

var body: some View {
NavigationView {
List {
ForEach(todos, id:\.name) {(todo) in
NavigationLink(destination:
VStack {
Text(todo.name)
Image(...)
}
){
HStack {
Image(...)
Text(todo.name)
}
}
}
.onDelete(perform: {indexSet in
todos.remove(atOffsets: indexSet)
})
.onMove(perform: {indices, newOffset in
todos.move(fromOffsets: indices, toOffset: newOffset)
})
}.navigationBarTitle("土豆List")
.navigationBarItems(trailing: EditButton())
}
}

onMove提供了indicesnewOffset两个变量,indices包含了所有要移动的todo记录的旧位置索引,newOffset包含了要移动到的新位置索引。

需要注意的是,只有进入了编辑模式后才可以移动todo记录,所以我在导航栏(Navigation Bar)中添加了一个 Edit 按钮,当点击了 Edit 按钮后就会进入编辑模式,这时候就可以移动 todo记录的位置了。

当点击 Edit 按钮进入编辑模式后,在每一个 todo记录 的左侧还会出现一个红色的删除按钮,这是编辑模式自带的效果。

为todo记录增加唯一标识

  • 为todo记录增加唯一标识

前面我们假设每一条 todo记录的标题都是唯一不重复的,所以使用 name 属性来唯一标记某条todo。

ForEach(todos, id:\.name)`

那如果出现多个重名的 todo记录怎么办呢??

如果有多个重名的todo记录的话,当我们删除记录的时候就会出现问题,因为它不知道到底应该删除哪条记录。为了解决这个问题,我们需要给 Todo结构体增加一个唯一标识符

struct Todo: Identifiable { // 遵守 Identifiable 协议
let id = UUID() // 新加一个 id 属性
let name: String
let category: String
}

我们做了两处修改:

  1. 遵守Identifiable协议
  2. 增加一个id属性

遵守Identifiable协议就需要我们增加一个 id属性,同时也意味着这个结构体是可以被唯一标识的。使用 UUID()函数生成一个唯一的标识符赋给每个新建的 Todo 结构体。

所以之前的代码就可以删除id:\.name,改完后代码如下:

ForEach(todos) {
...
}

List 会自动使用 todo.id 来唯一标识某条记录,不需要我们额外指明 id: \.id

增加一条 todo记录

想要增加一条 todo记录的话,就要新建一个 Todo 并加入到 todos数组中,同时我们在app导航栏的左侧增加一个按钮,点击后实现增加 todo记录。

var body: some View {
NavigationView {
List {
...
}
.navigationBarTitle("土豆List")
.navigationBarItems(
leading: Button(action: {}, label: {
Text("+1")
}),
trailing: EditButton()
)
}
}

我们为+1按钮增加一个点击事件处理函数 addTodo

NavigationView {
List {
...
}
.navigationBarTitle("土豆List")
.navigationBarItems(
leading: Button(action: addTodo,
label: {
Text("+1")
}),
trailing: EditButton()
)
}

addTodo函数的代码:

func addTodo() {
todos.append(Todo(name: "新的Todo", category: "desktopcomputer"))
}

修改完后运行app,点击 +1按钮后你将会看到屏幕上多出了一条记录:

使用用户输入数据新建 todo记录

目前我们新增的 todo记录的 name和 category 都是写死在代码里的,这样显然不符合常理,接下来我们就新建一个页面,根据用户输入的 name 和 category 新建 todo记录。

ContentView.swift 文件中增加以下代码:

struct AddTodoView: View {
var body: some View {
Text("这是增加 todo记录的界面")
}
}

同时在 ContentView 中增加一个 @State 变量,根据这个变量的值来判断是否需要跳转到 AddTodoView 来新建 todo记录:

@State private var showAddTodoView = false // 默认为 false,不跳转AddTodoView

接下来修改 +1按钮的点击处理逻辑如下:

{
...
}
.navigationBarItems(
leading: Button(action: {
// 反转 showAddTodoView 的值,false => true
self.showAddTodoView.toggle()
}, label: {
Text("+1")
})
.sheet(isPresented: $showAddTodoView) {
AddTodoView() // 我们刚才新建的新界面 struct
},
trailing: EditButton()
)

我们在 +1按钮后增加了一个 sheet修饰符用于自底向上弹出一个新界面,我们在新界面输入 name和 category来新建一个 todo记录。

sheet中的isPresented参数绑定了我们自定义的 showAddTodoView变量,当showAddTodoView的值为true时,弹出 sheet,否则隐藏 sheet。

可以手动向下拖拽 sheet 来隐藏 sheet。

这里我们还需要在 sheet界面里添加一个输入框获取用户输入,一个按钮用户点击后自动隐藏 sheet:

struct AddTodoView: View {
// @Binding 的作用下面马上会解释
@Binding var showAddTodoView: Bool var body: some View {
Text("添加一个 todo") Button(action: {
self.showAddTodoView = false // 变为false后sheet自动隐藏
}, label: {
Text("完成")
})
}
}

回到之前的 ContentView修改代码如下:

{
...
}
.navigationBarTitle(...)
.navigationBarItems(
leading:
Button(action: {
self.showAddTodoView.toggle()
}, label: {
Text("+1")
})
.sheet(isPresented: $showAddTodoView) {
AddTodoView(showAddTodoView: self.$showAddTodoView)
}
)
...

@Binding 这个变量会从任何地方传进来,并且这个变量的值会在当前位置和传此值进来的代码间共享。

在此代码中 showAddTodoView的值在 ContentView 和 AddTodoView 间共享,因为该变量是从 ContentView 中的 sheet里传进来的:

.sheet(isPresented: $showAddTodoView) {
AddTodoView(showAddTodoView: self.$showAddTodoView)
}

因此,当 AddTodoView 中的 showAddTodoView 变量发生变化时,ContentView 中的 showAddTodoView 也会发生变化,在 AddTodoView 中将 showAddTodoView 设为 false,那么 sheet就会自动隐藏。

下面我们在 AddTodoView 中增加一个输入框用于用户输入 Todo的 name,一个选择器用于选择 Todo的 category:

@State private var name: String = ""
// 用户选择了 categoryTypes中的某一项后,该变量为其索引值
@State private var selectedCategory = 0
// 存放预先定义的玄功选择的 category,展示在选择器 picker中
var categoryTypes = ["house", "theatermasks", "desktopcomputer"] var body: some View {
VStack {
Text("增加 todo").font(.largeTitle)
TextField("name", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.border(Color.black)
.padding() Text("选择 category")
Picker("", selection: $selectedCategory) {
ForEach(0 ..< categoryTypes.count) {
// $0 表示取第一个参数
Text(self.categoryTypes[$0])
}
}
.pickerStyle(SegmentedPickerStyle())
}.padding()
}

Picker 控件常用于用户从指定的列表中选择一个值,在 ForEach中我们遍历所有 categoryTypes中的元素展示到 Picker供用户选择。

Picker最后我们添加了一句pickerStyle(SegmentedPickerStyle()),你可以尝试去掉这一句代码,看看会产生什么影响。

接下来我们开始编写根据用户输入内容新建 Todo的代码,增加一条 todo记录的话除了要新建一个 Todo还要将添加到 ContentView中的 todos数组中去。

为此,我们要修改 ContentView中的+1按钮后添加的sheet代码:

.sheet(isPresented: $showAddTodoView) {
AddTodoView(
showAddTodoView: self.$showAddTodoView,
todos: self.$todos // 增加了一个 todos参数
)
}

同时还要修改 AddTodoView中的代码,添加一行@Binding标记的代码用于接收从 ContentView传入的 todos参数:

@Binding var todos: [Todo] // Todo类型的数组,用于接收其它地方传入参数

使用 @Binding标记后的 todos就可以同时在 ContentViewAddTodoView两个页面里共享状态了。

最后,在 AddTodoView中增加一个按钮,用户点击后获取输入的 name和选择的 category新建一个 Todo,然后添加到 todos数组中。

Button(action: {
self.showAddTodoView = false // 隐藏 sheet
// 新建一个 Todo并添加到 todos数组中
todos.append(Todo(name: name,
category: categoryTypes[selectedCategory]))
}, label: {
Text("点击新建")
})

运行一下代码看看效果吧!!

真棒!!我们已经得到了一个自己开发的 土豆List !!

在上面的代码中,我们添加新建的 Todo到 todos数组用的是 append,这会把新数据添加到数组的末尾,反映到app界面上也就是新添加的记录会展示的屏幕最下方。为了“修复”这个问题,你可以试试 insert !!

小结

在这一章节里我们自己完成了一款 土豆List app,涉及到了 List的数据填充、点击、移动、删除等,还使用了一个新控件:Picker!!

目前还有一个问题就是数据的持久化,我们的 todo记录目前都是保存在手机的内存中的,app关闭之后再打开数据就丢失了。

最新文章

  1. C++之路进阶——codevs2306(晨跑)
  2. Enhanced Mitigation Experience Toolkit 软件安全性强化工具
  3. JavaScript 正则表达式上——基本语法
  4. 与众不同 windows phone (46) - 8.0 通信: Socket, 其它
  5. matlab读入矩阵数据
  6. JavaScript的function对象
  7. java中调用js脚本
  8. 安装nginx 做反向代理
  9. Spring AOP With AspectJ
  10. TCP Health Checks
  11. LocalDateTime json格式化
  12. sql server profiler 的使用
  13. mysql操作说明,插入时外键约束,快速删除
  14. jQuery 插件写法示例
  15. UVa 12230 - Crossing Rivers(数学期望)
  16. Linux进程守护——Supervisor 使用记录
  17. getPropertyValue (实现 js框架中 css 的最终调用的函数)
  18. GitHub 出现这样的问题怎么办
  19. 中国石油大学(华东)暑期集训--二进制(BZOJ5294)【线段树】
  20. python学习手册 (第3版)

热门文章

  1. JAVA - 序列化的方式
  2. 线程崩溃为什么不会导致 JVM 崩溃
  3. 【Parcel 2 + Vue 3】从0到1搭建一款极快,零配置的Vue3项目构建工具
  4. 【黑马pink老师的H5/CSS课程】(二)标签与语法
  5. python小题目练习(七)
  6. python线程池 ThreadPoolExecutor 的用法及实战
  7. docker容器内修改文件
  8. 校验日期格式为yyyy-MM-dd
  9. 021(Keywords Search)(AC自动机)
  10. MarkDown语法——更好地写博客