[SwiftUI 100 天] iExpense - part2

600 阅读7分钟
译自 Deleting items using onDelete()
更多内容,欢迎关注公众号 「Swift花园」
喜欢文章?不如来个 🔺💛➕三连?关注专栏,关注我 🚀🚀🚀


用 onDelete 删除列表项


SwiftUI 提供了 onDelete() modifier ,用于从集合中删除对象。实践中,它几乎都是显式地配合 ListForEach使用:我们用ForEach创建包含很多项的列表,然后把 onDelete() 添加到 ForEach 以便用户可以移除他们不想要的列表项。

这是又一处 SwiftUI 为我们做了大量工作的地方,不过里面有不少新奇的东西可以了解。

首先,我们先构造一个案例:一个显示数字的列表,每当我们点击按钮,一个新的数字会出现。下面是代码:

struct ContentView: View {
    @State private var numbers = [Int]()
    @State private var currentNumber = 1

    var body: some View {
        VStack {
            List {
                ForEach(numbers, id: \.self) {
                    Text("\($0)")
                }
            }

            Button("Add Number") {
                self.numbers.append(self.currentNumber)
                self.currentNumber += 1
            }
        }
    }
}

你可能觉得这里的 ForEach 是多余的,整个列表都是由动态的行构成的,所以我们可以把列表的代码改成下面这样:

List(numbers, id: \.self) {
    Text("\($0)")
}

这样当然也可以,接下来是第一个新奇的地方:onDelete() modifier 只存在于ForEach,因此假如我们想要用户能够从列表中删除项目,那么我们必须把它们放在 ForEach里面。这种限定意味着当我们只有动态的行时,列表需要额外多一点点代码,但另一方面,这种限定也保障了我们可以约定列表只有特定的某些行可以被删除。

为了让 onDelete() 工作,我们需要实现一个方法,这个方法会接收单一的 IndexSet类型的参数。这个类型有点像一组整数,除了它们是被排过序的之外。而这个参数的唯一作用就是告诉我们 ForEach 中所有要被删除的项的位置。

因为 ForEach 是借助一个数组来创建的,所以我们实际上可以把 index set 直接传给数组 —— 它有一个专门的的 remove(atOffsets:) 方法可以接收 index set 。

把这个方法添加到 ContentView

func removeRows(at offsets: IndexSet) {
    numbers.remove(atOffsets: offsets)
}

最后,我们还要让 SwiftUI 在它想要从 ForEach中删除数据时调用我们的方法,把代码改成下面这样:

ForEach(numbers, id: \.self) {
    Text("\($0)")
}
.onDelete(perform: removeRows)

运行 app ,添加一些数字。然后,在列表项上从右往左滑,你会发现删除按钮出现了。你可以直接点击删除,或者利用 iOS 提供的 swipe 手势多滑一点触发删除。

考虑实现的代码如此简单,这个结果已经很棒了。不过 SwiftUI 还私藏了另外一个技巧:我们可以在导航栏上添加 Edit/Done 按钮,以便用户可以更方便地一次删除几个列表项。

首先,把 VStackNavigationView包起来,然后添加这个 modifier 到 VStack

.navigationBarItems(leading: EditButton())

需要的代码就这些 —— 再次运行 app ,添加数字,然后点击 “编辑” 开始删除列表项。当你完成删除,再点击 “完成” 退出编辑模式。代码这么少,不赖嘛!


译自 Storing user settings with UserDefaults


用 UserDefaults 存储用户设置


网站和 app 的一个无争议的最大区别是它们处理用户数据的方式。一方面,网站通过跟踪 cookies,营销广告,以及观察我们在网站上的一举一动等方式极尽所能地侵入个体的隐私,所以大多数人不愿意托付更多的数据给网站。另一方面,我们却更期望 app 能存储我们的数据 —— 我们希望它们这么做。如果每个 app 启动的时候都出现一个 “我们能存储你的 cookies 吗?” 这类 GDPR 的提示,难道不是很奇怪吗?

因此,难怪 iOS 提供了好几种读取和写入用户数据的方式,我在这里介绍其中的两种。

第一种是 UserDefaults,它可以让我们直接存储少量的用户数据。对于这里的“少量”并没有具体的数字,但你可以记住:你存在 UserDefaults里的所有东西会在 app 启动时自动加载 —— 如果你存的太多,你的 app 启动速度会下降。为了明确一点,建议你应该力争把存在 UserDefaults 里的数据控制在 512KB 以下。

提示:如果你正在考虑 “512KB是多少”?那我给你一个形象的估计好了:大概相当于你目前读的这么多章教程的文本内容这么多。

UserDefaults 对于存放用户设置和其他重要数据尤其适用 —— 你可以在这里记录用户上一次启动 app 的时间,他们上一次阅读的内容进度,或者其他被动收集的信息。

让我们看看代码。下面是一个带有一个按钮的视图,按钮显示自己被点击的次数 —— 每被点击一次增加计数。

struct ContentView: View {
    @State private var tapCount = 0

    var body: some View {
        Button("Tap count: \(tapCount)") {
            self.tapCount += 1
        }
    }
}

假如我们想把用户已经点击的次数保存起来,以便他们将来回到 app 时可以继续之前的进度?

让这件事发生需要改动两个地方。首先,我们要在每次点击次数改变时把它写入UserDefaults,把下面的代码加在 self.tapCount += 1这行后面:

UserDefaults.standard.set(self.tapCount, forKey: "Tap")

这一行代码里你可以发掘三点:

  1. 我们需要用到 UserDefaults.standard,这是附给我们的 app 的内建的UserDefaults实例 ,在更复杂的 app 里,你还可以创建自己的实例。举个例子,假如你想要跨几个 app 扩展共享某个 default ,那你就需要自行创建 UserDefaults 实例。
  2. 有一个 set() 方法,接收各种类型的数据 —— 整数,布尔型,字符串,等等。
  3. 我们给数据配一个字符串名字,在我们的案例中就是这个键 “Tap” 。键就像常规的 Swift 字符串一样,大小写敏感,而且它很重要 —— 我们需要用同一个键在之后从 UserDefaults中读回我们的数据。

既然说到读取数据,相比把 tapCount 重置为 0 ,每次开始前我们应该从 UserDefaults 中读出数值作为它的初始值。

@State private var tapCount = UserDefaults.standard.integer(forKey: "Tap")

留意这里的键名,它确保了我们读取相同的数据。

尽管尝试这个 app 然后开动你的脑筋 —— 你可以多点几次按钮,然后回到 Xcode ,然后再次运行 app ,看看数字会如何变化。

有两件事你在代码中是看不到的,但这两件事都很重要。

第一件事,假如 “Tap” 键还没被设置过会怎么样?这正是 app 首次运行的情况,但你发现它工作正常 —— 假如键没有找到,那么它会返回 0 。

有的时候像 0 这样的默认值是有帮助的,但有的时候某些默认值会令人困惑。对于布尔型,举个例子,当你因为 boolean(forKey:)找不到键时返回 false ,那这个 false 究竟是你自己设置的呢?还是表示没有设置过?

第二件事,iOS 需要花一些事件来把数据写入永久存储 —— 实际上是把改动写入设备。它并不会立刻写入更新,因为你可能在短时间内来回改变或者写入其他数据,所以实际的情况是它会等待一段时间,然后一次性写入所有变化。等待的时间是多久我们无从得知,不过几秒钟可能是合适的。

基于这个机制,如果你点击了按钮,然后快速地重新启动 app ,你可能会发现你最近一次点击计数没有被保存。过去有一种方法可以强制更新立即写入永久存储,但目前这个方法意义不大 —— 因为即便用户在做了某个需要被保存的操作之后立刻执行杀死你的 app 进程的动作,你的 default 数据最后还是会被写入,所以不会有数据丢失。


我的公众号 这里有Swift及计算机编程的相关文章,以及优秀国外文章翻译,欢迎关注~