SwiftUI实战教程 第三章 土豆List
代码库
教程中的项目代码都保存在这里:
https://github.com/NDFour/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) inNavigationLink(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) inNavigationLink(destination:...}} .onDelete(perform: {indexSet intodos.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) inNavigationLink(destination:VStack {Text(todo.name)Image(...)}){HStack {Image(...)Text(todo.name)}}}.onDelete(perform: {indexSet intodos.remove(atOffsets: indexSet)}).onMove(perform: {indices, newOffset intodos.move(fromOffsets: indices, toOffset: newOffset)})}.navigationBarTitle("土豆List").navigationBarItems(trailing: EditButton())}
}
onMove
提供了indices
和newOffset
两个变量,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: Stringlet category: String
}
我们做了两处修改:
- 遵守Identifiable协议
- 增加一个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 => trueself.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: Boolvar 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())
,你可以尝试去掉这一句代码,看看会产生什么影响
SwiftUI实战教程 第三章 土豆List相关推荐
- 视频教程-SEM实战教程(三)-网络营销
SEM实战教程(三) 毕业于中国人民大学,从事网络营销推广多年,网络营销讲师,有丰富的SEM.微博微信营销培训经验,多年的网络营销实战派研究者,操作过医疗集团.出国留学.教育培训等推广项目,现专注SE ...
- Python--Redis实战:第三章:Redis命令:第七节:其他命令
上一篇文章:Python--Redis实战:第三章:Redis命令:第六节:发布与订阅 下一篇文章:Python--Redis实战:第四章:数据安全与性能保障:第1节:持久化选项 到目前为止,本章介绍 ...
- ffmpeg实战教程(三)音频PCM采样为AAC,视频YUV编码为H264/HEVC
ffmpeg实战教程(三)音频PCM采样为AAC,视频YUV编码为H264/HEVC https://blog.csdn.net/King1425/article/details/71180330 音 ...
- Android Camera2 教程 · 第三章 · 预览
Android Camera2 教程 · 第三章 · 预览 DarylGo关注 Android Camera 上一章<Camera2 开启相机>我们学习了如何开启和关闭相机,接下来我们来学 ...
- android远程打电话,Android打电话功能 Android实战教程第三篇之简单实现拨打电话功能...
想了解Android实战教程第三篇之简单实现拨打电话功能的相关内容吗,杨道龙在本文为您仔细讲解Android打电话功能的相关知识和一些Code实例,欢迎阅读和指正,我们先划重点:Android拨打电话 ...
- 软考 程序员教程-第三章 数据库基础知识
软考 程序员教程-第三章 数据库基础知识 第三章 数据库基础知识 3.1 基本概念 数据库系统(DataBase System,DBS)由数据库(DataBase,DB).硬件.软件和人员4大部分组成 ...
- c语言多个附加说明符,C语言教程第三章.ppt
<C语言教程第三章.ppt>由会员分享,可在线阅读,更多相关<C语言教程第三章.ppt(36页珍藏版)>请在人人文库网上搜索. 1.第三章,顺序结构程序设计 数据输入输出 及程 ...
- C#图解教程(第三章)
C#图解教程第三章 3.1 C#程序是一组类型声明 3.2 类型是一种模板 3.3 实例化类型 3.4 数据成员和函数成员 3.5预定义类型 3.6用户定义类型 3.7 栈和堆 3.7.1 栈 3.7 ...
- matlab中任意两边之和大于第三边,MATLAB教程第三章.ppt
<MATLAB教程第三章.ppt>由会员分享,可在线阅读,更多相关<MATLAB教程第三章.ppt(34页珍藏版)>请在人人文库网上搜索. 1.MATLAB程序语言设计, ,第 ...
- 机器学习实战教程(三):决策树实战篇
一.前言 上篇文章机器学习实战教程(二):决策树基础篇_M_Q_T的博客-CSDN博客讲述了机器学习决策树的原理,以及如何选择最优特征作为分类特征.本篇文章将在此基础上进行介绍.主要包括: 决策树构建 ...
最新文章
- Android之Base64
- bfs:01迷宫(洛谷P1141)
- 方向盘开极品飞车9很Hapyy
- python元组元素抓7_Python3基础 tuple 通过拆分元组向元组中加入新的元素
- android app应用签名生成工具,android应用签名详细步骤
- 1000道Python题库系列分享十一(9道)
- 浅析计算机应用管理及前景,论计算机应用技术的现状及前景
- Tkinter实现模拟鼠标单击四位数字验证码自动刷新功能
- catia二次开发:IDE界面介绍
- 安装UWB定位系统设备需要注意什么?
- 在家也可以免费下载知网文献,5种免费下载知网文献方法
- (第六章)UI--PS 基础 图层蒙版与混合模式
- thinkphp 检测上传的图片中是否含有木马脚本
- 没有这个传奇工程师,就没有今天的 Windows
- 达尔豪西大学 计算机科学,西安大略大学和达尔豪西大学哪个好
- springboot跳转外部链接
- 2020前端最新面试题总结(js、html、小程序、React、ES6、Vue、算法、全栈热门视频资源)(3年前端菜鸟级开发师含泪总结)
- 不知道如何入门编程?最全在线教程网站汇总来了,还不赶快收藏
- Pytorch Note46 生成对抗网络的数学原理
- world标题是大写数字,题注要阿拉伯数字,交叉引用不会出错
热门文章
- android nat64,dpvs学习笔记: 18 nat64 的实现
- linux 流式传输,Steam家用流式传输设置教程 Steam家用流式传输怎么用
- 身份证护照扫描仪助力酒店信息录入
- ARCHPR(压缩密码暴力破解软件)
- 使用navicat创建mysql全文索引
- vscode 初始化HTML结构
- python用tkinter做简易计算器_基于python tkinter的简单计算器(v1.0)
- 为什么央行降息降准,会导致债券价格上涨?
- Trajan/强连通(石油大学组队赛 B: Thrall’s Dream)
- mac屏幕分辨率调整:SwitchResX