SwiftUI极简教程16:List列表的使用方法进阶学习

5,521 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第16天,点击查看活动详情

今日职言:商业环境的变化是迅速且规律莫测的,你要做的是,一旦决定了就立即去做。

在本章中,我们将基于List列表的基本使用方法上,进阶学习List列表的更多用法。

本章节将分成3个部分讲解。

1、onDelete滑动删除和onMove拖动排序

2、ContextMenu上下文菜单

3、ActionSheets弹窗的使用

那我们开始吧。

第一部分:onDelete滑动删除和onMove拖动排序

首先,我们先创建一个新项目,命名为SwiftUIList02

258.png

我们创建一个简单的列表,这里引用之前的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循环的方法建立了一个列表。

运行后的效果如下:

259.png

实现单条List列表数据的滑动删除,我们需要调用.onDelete(perform:XXXX)修饰符,它是ForEach的修饰符,用来删除List中的一条条数据;

perform中引用的是删除的方法,我们定义一个删除方法为deleteRow,具体实现方法如下:

//滑动删除方法
func deleteRow(at offsets: IndexSet) {
    Messages.remove(atOffsets: offsets)
}

deleteRow删除列的方法中,我们接收单一的 IndexSet类型的参数,它是用来定位要删除的列的位置的。

然后调用remove(atOffsets:XXXX)方法来删除Messages数组中的被定位的特定项。

这样我们就可以实现列表的滑动删除,运行模拟器,点击一行向右滑动,我们就可以实现删除一行数据。

260.png

科普一个知识点。

我们运行的时候会发现,我们滑动删了1行,系统自动又“创建”了一行回来。

这是因为每次我们删除List中特定的数据项时,系统会自动更新UI,而更新的数据源是我们创建的Messages数组。

也就是说,每次删除后,系统重新“渲染”页面,但Messages数组数据没有变化,也就删除了啥,就恢复了啥,就变成了怎么也删除不了了。

我们希望的是,删除了特定的数据项时,Messages数组的值也需要同步被更改。

因此,我们必须让SwiftUI监控属性,并在属性值发生变化时更新UI。我们使用@State定义数据:

@State var messagesItems = Messages

并且将ForEach循环的数据源由Messages数组,变为我们用@State定义的messagesItems,同时在deleteRow方法中,操作remove的对象也换成messagesItems数组。

这样,我们每次删除数组特定项的时候,messagesItems数组就知道我们删除了什么数据,并且“记住”它,在UI刷新的时候,ForEach就基于messagesItems数组内的内容遍历数据。

261.png

好了,我们实现了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())
}

262.png

再延伸地补充一个知识点。

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,那么系统将自动更新重新排列顺序。

从而实现排序的效果。

263.png

快来试试吧!

第二部分:ContextMenu上下文菜单

ContentMenu上下文菜单是iOS13引用的一个新功能,效果为长按列表时,弹出一个悬浮窗口,用于快捷操作。

iPhone当中存在大量这样的交互,示例:长按系统设置,打开快捷操作弹窗。

264.png

实现ContentMenu上下文菜单的方法也很简单,使用.contentMenu修饰符,在修饰符里面构建需要展示或者操作的内容。

这里我们尝试做一个长按删除的操作,长按列表的一个项目,弹出上下文ContentMenu菜单,里面是一个删除按钮,点击按钮,删除指定行的数据。

.contextMenu {
    Button(action: {
        // 点击删除
    }) {
        HStack {
            Text("删除")
            Image(systemName: "trash")
        }
    }
}

265.png

然后,我们实现下删除的操作,会有些复杂,请耐心查阅。

它不像我们使用ForEach使用的.onDelete删除修饰符的方法不一样,ContentMenu上下文菜单没有索引定位到特定的数据项,也就不知道我们点击选中的是哪一条数据。

这就需要我们自己定位到Messages数组里面的Messageid,这样我们就可以通过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函数循环遍历数组,并将给定Messagesid与数组中的id进行比较。

如果有id一样,则firstIndex函数返回Messages数组的索引。

这样我们就知道长按的是哪一条数据了!

接下来,我们就可以通过调用remove(at:XXXX)修饰符从Messages数组中删除对应的数据项。

self.delete(item: Message)

266.png

我们运行一下模拟器,长按,系统会给出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()
    }
}

267.png

第三部分:ActionSheets弹窗的使用

弹窗当中,我们在之前的章节学习了ModelView弹窗,这里我们再拓展一种弹窗模式,叫做.actionSheet

它算是.sheet的另一种形式,常用在用户敏感操作的二次确认,但又不像Alerts警告弹窗那么严肃,属于一般强调。

268.png

.actionSheetAlerts警告弹窗的实现方式大体相同。

.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("取消")
                ])
    }

269.png

下面,我们做一个“有趣”的操作。

我们承接上一部分的内容,长按列表,弹出一个ContentMenu上下文菜单,里面是一个删除按钮,点击删除按钮,打开ActionSheet弹窗,里面又是一个删除按钮,点击ActionSheet弹窗内的删除按钮,删除列表的数据项。

270.png

我们就实现了基于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()
    }
}

快来动手试试吧!

如果本专栏对你有帮助,不妨点赞、评论、关注~