在本章中,我们将基于List
列表的基本使用方法上,进阶学习List
列表的更多用法。
本章节将分成3个部分讲解。
1、onDelete
滑动删除和onMove
拖动排序
2、ContextMenu
上下文菜单
3、ActionSheets
弹窗的使用
那我们开始吧。
第一部分:onDelete滑动删除和onMove拖动排序
首先,我们先创建一个新项目,命名为SwiftUIList02
。
我们创建一个简单的列表,这里引用之前的List
创建的代码。
完整代码如下:
import SwiftUI struct Message: Identifiable { var id = UUID() var name: String var image: String } // 定义数组,存放数据 var Messages = [ Message(name: "这是微信", image: "weixin"), Message(name: "这是QQ", image: "qq"), Message(name: "这是微博", image: "weibo"), Message(name: "这是手机", image: "phone"), Message(name: "这是邮箱", image: "mail"), ] struct ContentView: View { var body: some View { // 列表 List { ForEach(Messages) { Message in HStack { Image(Message.image) .resizable() .frame(width: 40, height: 40) .cornerRadius(5) Text(Message.name) .padding() } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } 复制代码
这里我们使用了List
列表和ForEach
循环的方法建立了一个列表。
运行后的效果如下:
实现单条List
列表数据的滑动删除,我们需要调用.onDelete(perform:XXXX)
修饰符,它是ForEach
的修饰符,用来删除List中的一条条数据;
perform
中引用的是删除的方法,我们定义一个删除方法为deleteRow
,具体实现方法如下:
//滑动删除方法 func deleteRow(at offsets: IndexSet) { Messages.remove(atOffsets: offsets) } 复制代码
在deleteRow
删除列的方法中,我们接收单一的 IndexSet
类型的参数,它是用来定位要删除的列的位置的。
然后调用remove(atOffsets:XXXX)
方法来删除Messages
数组中的被定位的特定项。
这样我们就可以实现列表的滑动删除,运行模拟器,点击一行向右滑动,我们就可以实现删除一行数据。
科普一个知识点。
我们运行的时候会发现,我们滑动删了1行,系统自动又“创建”了一行回来。
这是因为每次我们删除List
中特定的数据项时,系统会自动更新UI,而更新的数据源是我们创建的Messages
数组。
也就是说,每次删除后,系统重新“渲染
”页面,但Messages
数组数据没有变化,也就删除了啥,就恢复了啥,就变成了怎么也删除不了了。
我们希望的是,删除了特定的数据项时,Messages
数组的值也需要同步被更改。
因此,我们必须让SwiftUI
监控属性,并在属性值发生变化时更新UI。我们使用@State
定义数据:
@State var messagesItems = Messages 复制代码
并且将ForEach
循环的数据源由Messages
数组,变为我们用@State
定义的messagesItems
,同时在deleteRow
方法中,操作remove
的对象也换成messagesItems
数组。
这样,我们每次删除数组特定项的时候,messagesItems
数组就知道我们删除了什么数据,并且“记住”它,在UI刷新的时候,ForEach
就基于messagesItems
数组内的内容遍历数据。
好了,我们实现了List
页面的滑动删除操作了。
再补充一个知识点。
如果我们给List
列表创建导航栏,还可以使用SwiftUI
已经封装好的EditButton
编辑按钮,从而实现列表快速进入编辑状态,这在Apple
自家的备忘录中可以看到。
当我们点击Edit
按钮时,按钮文字会变成done
,同时List
列表都变成可删除的模式。
NavigationView { // 列表 List { ForEach(messagesItems) { Message in HStack { Image(Message.image) .resizable() .frame(width: 40, height: 40) .cornerRadius(5) Text(Message.name) .padding() } }.onDelete(perform: deleteRow) }.navigationBarItems(trailing: EditButton()) }
再延伸地补充一个知识点。
List
列表编辑模式下,除了删除之前,还可以针对于数据项进行拖动排序,我们用的是.onMove(perform:XXXX)
修饰符,它也是ForEach
的修饰符,用来实现List
列表的拖动单条数据的改变它的排序顺序;
NavigationView { // 列表 List { ForEach(messagesItems) { Message in HStack { Image(Message.image) .resizable() .frame(width: 40, height: 40) .cornerRadius(5) Text(Message.name) .padding() } } .onDelete(perform: deleteRow) .onMove(perform: moveItem) }.navigationBarItems(trailing: EditButton()) } 复制代码
perform
中引用的是排序的方法,我们定义一个删除方法为moveItem
,具体实现方法如下:
// 拖动排序方法 func moveItem(from source: IndexSet, to destination: Int) { messagesItems.move(fromOffsets: source, toOffset: destination) } 复制代码
同样,我们接收单一的 IndexSet
类型的参数,它是用来定位要排序的列的位置。
然后它的排序数值为Int
类型,简单来说,初始的排序是0、1、2、3、4
,假设我们把3
拖动到0
,那么系统将自动更新重新排列顺序。
从而实现排序的效果。
快来试试吧!
第二部分:ContextMenu上下文菜单
ContentMenu
上下文菜单是iOS13
引用的一个新功能,效果为长按列表时,弹出一个悬浮窗口,用于快捷操作。
iPhone
当中存在大量这样的交互,示例:长按系统设置,打开快捷操作弹窗。
实现ContentMenu
上下文菜单的方法也很简单,使用.contentMenu
修饰符,在修饰符里面构建需要展示或者操作的内容。
这里我们尝试做一个长按删除的操作,长按列表的一个项目,弹出上下文ContentMenu
菜单,里面是一个删除按钮,点击按钮,删除指定行的数据。
.contextMenu { Button(action: { // 点击删除 }) { HStack { Text("删除") Image(systemName: "trash") } } }
然后,我们实现下删除的操作,会有些复杂,请耐心查阅。
它不像我们使用ForEach
使用的.onDelete
删除修饰符的方法不一样,ContentMenu
上下文菜单没有索引定位到特定的数据项,也就不知道我们点击选中的是哪一条数据。
这就需要我们自己定位到Messages
数组里面的Message
的id
,这样我们就可以通过id
定位到是数组中的哪一条数据了。
方法如下:
//删除的方法 func delete(item Message: Message) { if let index = self.messagesItems.firstIndex(where: { $0.id == Message.id }) { self.messagesItems.remove(at: index) } }
我们定义了一个delete
的函数方法,接收了一个Message
对象,并在Messages
数组中搜索Message
对象的索引。
为了找到Message
对象的索引,我们调用firstIndex
函数循环遍历数组,并将给定Messages
的id
与数组中的id
进行比较。
如果有id
一样,则firstIndex
函数返回Messages
数组的索引。
这样我们就知道长按的是哪一条数据了!
接下来,我们就可以通过调用remove(at:XXXX)
修饰符从Messages
数组中删除对应的数据项。
self.delete(item: Message)
我们运行一下模拟器,长按,系统会给出ContentMenu
上下文菜单,它是一个删除按钮,我们点击删除按钮,该行数据就被删除了。
完整代码如下:
import SwiftUI struct Message: Identifiable { var id = UUID() var name: String var image: String } // 定义数组,存放数据 var Messages = [ Message(name: "这是微信", image: "weixin"), Message(name: "这是QQ", image: "qq"), Message(name: "这是微博", image: "weibo"), Message(name: "这是手机", image: "phone"), Message(name: "这是邮箱", image: "mail"), ] struct ContentView: View { // 定义数组 @State var messagesItems = Messages var body: some View { NavigationView { // 列表 List { ForEach(messagesItems) { Message in HStack { Image(Message.image) .resizable() .frame(width: 40, height: 40) .cornerRadius(5) Text(Message.name) .padding() } .contextMenu { Button(action: { // 点击删除 self.delete(item: Message) }) { HStack { Text("删除") Image(systemName: "trash") } } } } .onDelete(perform: deleteRow) .onMove(perform: moveItem) }.navigationBarItems(trailing: EditButton()) } } //删除的方法 func delete(item Message: Message) { if let index = self.messagesItems.firstIndex(where: { $0.id == Message.id }) { self.messagesItems.remove(at: index) } } // 滑动删除方法 func deleteRow(at offsets: IndexSet) { messagesItems.remove(atOffsets: offsets) } // 拖动排序方法 func moveItem(from source: IndexSet, to destination: Int) { messagesItems.move(fromOffsets: source, toOffset: destination) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
第三部分:ActionSheets弹窗的使用
弹窗当中,我们在之前的章节学习了ModelView
弹窗,这里我们再拓展一种弹窗模式,叫做.actionSheet
。
它算是.sheet
的另一种形式,常用在用户敏感操作的二次确认,但又不像Alerts
警告弹窗那么严肃,属于一般强调。
.actionSheet
和Alerts
警告弹窗的实现方式大体相同。
.actionSheet(isPresented:$showActionSheet) { //ActionSheet结构体 }
我们还是使用isPresented
触发,我们定义一个变量showActionSheet
的状态,初始状态是false
。
@State var showActionSheet = false 复制代码
接下来,我们实现下ActionSheet
结构体:
// ActionSheet弹窗 .actionSheet(isPresented: self.$showActionSheet) { ActionSheet( title: Text("你确定要删除此项吗?"), message: nil, buttons: [ .destructive(Text("删除"), action: { //点击删除 }), .cancel(Text("取消") ]) }
下面,我们做一个“有趣”的操作。
我们承接上一部分的内容,长按列表,弹出一个ContentMenu
上下文菜单,里面是一个删除按钮,点击删除按钮,打开ActionSheet
弹窗,里面又是一个删除按钮,点击ActionSheet
弹窗内的删除按钮,删除列表的数据项。
我们就实现了基于ActionSheet
弹窗的删除操作啦!
完整代码如下:
import SwiftUI struct Message: Identifiable { var id = UUID() var name: String var image: String } // 定义数组,存放数据 var Messages = [ Message(name: "这是微信", image: "weixin"), Message(name: "这是QQ", image: "qq"), Message(name: "这是微博", image: "weibo"), Message(name: "这是手机", image: "phone"), Message(name: "这是邮箱", image: "mail"), ] struct ContentView: View { // 定义数组 @State var messagesItems = Messages @State var showActionSheet = false var body: some View { NavigationView { // 列表 List { ForEach(messagesItems) { Message in HStack { Image(Message.image) .resizable() .frame(width: 40, height: 40) .cornerRadius(5) Text(Message.name) .padding() } // 上下文菜单 .contextMenu { Button(action: { // 点击打开ActionSheet弹窗 self.showActionSheet.toggle() }) { HStack { Text("删除") Image(systemName: "trash") } } } // ActionSheet弹窗 .actionSheet(isPresented: self.$showActionSheet) { ActionSheet( title: Text("你确定要删除此项吗?"), message: nil, buttons: [ .destructive(Text("删除"), action: { //点击删除 self.delete(item: Message) }), .cancel(Text("取消")) ]) } } .onDelete(perform: deleteRow) .onMove(perform: moveItem) }.navigationBarItems(trailing: EditButton()) } } // 删除的方法 func delete(item Message: Message) { if let index = messagesItems.firstIndex(where: { $0.id == Message.id }) { messagesItems.remove(at: index) } } // 滑动删除方法 func deleteRow(at offsets: IndexSet) { messagesItems.remove(atOffsets: offsets) } // 拖动排序方法 func moveItem(from source: IndexSet, to destination: Int) { messagesItems.move(fromOffsets: source, toOffset: destination) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
快来动手试试吧!