SwiftUI实战教程-土豆List
代码库
教程中的项目代码都保存在这里: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
提供了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: String
let 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 => 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就可以同时在 ContentView
和 AddTodoView
两个页面里共享状态了。
最后,在 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关闭之后再打开数据就丢失了。
SwiftUI实战教程-土豆List的更多相关文章
- 【ASP.NET实战教程】ASP.NET实战教程大集合,各种项目实战集合
[ASP.NET实战教程]ASP.NET实战教程大集合,各种项目实战集合,希望大家可以好好学习教程中,有的比较老了,但是一直很经典!!!!论坛中很多小伙伴说.net没有实战教程学习,所以小编连夜搜集整 ...
- 【转】mybatis实战教程(mybatis in action),mybatis入门到精通
MyBatis 目录(?)[-] mybatis实战教程mybatis in action之一开发环境搭建 mybatis实战教程mybatis in action之二以接口的方式编程 mybatis ...
- NDK-JNI实战教程(二) JNI官方中文资料
声明 设计概述 JNI接口函数和指针 加载和链接本地方法 解析本地方法名 本地方法的参数 引用Java对象 全局和局部引用 实现局部引用 访问Java对象 访问基本类型数组 访问域和方法 报告编程错误 ...
- mybatis实战教程(mybatis in action),mybatis入门到精通
转自:http://www.yihaomen.com/article/java/302.htm (读者注:其实这个应该叫做很基础的入门一下下,如果你看过hibernate了那这个就非常的简单) (再加 ...
- ActiveReports 9实战教程(3): 图文并茂的报表形式
基于上面2节内容,我们搭建了AR9的开发环境,配置好了数据源.在本节,我们以官方提供的3个中文图文并茂的报表来展示AR9的功能,并通过实战的方式一一分享. 以往做报表相关的工作时,最害怕的是报表的UI ...
- 《软件性能测试与LoadRunner实战教程》新书上市
作者前三本书<软件性能测试与LoadRunner实战>.<精通软件性能测试与LoadRunner实战>和<精通软件性能测试与LoadRunner最佳实战>面市后,受 ...
- BI之SSAS完整实战教程7 -- 设计维度、细化维度中 :浏览维度,细化维度
上篇文章我们已经将Dim Geography维度设计好. 若要查看维度的成员, AS需要接收该维度的详细信息(包括已创建的特性.成员属性以及多级层次结构), 通过XMLA与AS的实例进行通信. 今天我 ...
- BI之SSAS完整实战教程6 -- 设计维度、细化维度上:创建维度定义特性关系
前面我们使用过数据源向导.数据源视图向导.Cube向导来创建相应的对象. 本篇我们将学习使用维度向导来创建维度. 通过前面几个向导的学习,我们归纳一下共同点,主要分成两步 1. 使用某种对象类型的向导 ...
- BI之SSAS完整实战教程5 -- 详解多维数据集结构
之前简单介绍过多维数据集(Cube)的结构. 原来计划将Cube结构这部分内容打散,在实验中穿插讲解, 考虑到结构之间不同的部分都有联系,如果打散了将反而不好理解,还是直接一次性全部讲完. 本篇我们将 ...
随机推荐
- 无鼠标打开Windows设备管理
转载:https://blog.csdn.net/weixin_39946767/article/details/118644619
- 教你用VS code 生成vue-cli代码片段
可以自定义设置名字:name.json { "Print to console": { "prefix": "vue", "bod ...
- kubernetes之常用核心资源对象
部门产品线本身是做DEVOPS平台,最近部署架构也在往K8S上靠了,不得不学一下K8S.自己搭建了K8S集群与harbor仓库来学习. 1.kubernetes之常用核心资源对象 1.1.K8s服务部 ...
- Java基础-并发篇
3.1. JAVA 并发知识库 3.2. JAVA 线程实现/创建方式 3.2.1. 继承 Thread 类 Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例. ...
- WPF第三方控件,只能输入数字型数据
话不多说,根据最近项目需求,为了减少输入验证等相关代码量,需要此控件 先上效果图 默认样式是这样,自己可以根据需求修改外形,但我更喜欢它自带的简洁版 有人可能会问怎么实现的呢?其实很简单,我们设置它的 ...
- yearning_sql审核平台搭建
Yearning SQL 审计平台 基于Vue.js与Django的整套mysql-sql审核平台解决方案.提供基于Inception的SQL检测及执行. GitHub:https://github. ...
- Java服务假死后续之内存溢出
一.现象分析 上篇博客说到,Java服务假死的原因是使用了Guava缓存,30分钟的有效期导致Full GC无法回收内存.经过优化后,已经不再使用Guava缓存,实时查询数据.从短期效果来看,确实解决 ...
- Mvcapi解决H5请求接口跨域问题
using Newtonsoft.Json;using System;using System.Collections.Generic;using System.Linq;using System.N ...
- 网络营销谁在行?PHP小哥打个样
PHP -ゞ 阿白同学的学习笔记 PHP学习笔记 - 01 - web2.0 - 网络营销 @ 目录 一. 前言 二. 开始(借助菜鸟教程平台练习) 1. Hello World -- 第一个案例 2 ...
- 2022-7-23 pan小堂 Object与Final
Object类 1.Object方法 public final native Class<?> getClass() 返回object运行时类 public native int hash ...