1. Swift中类型安全的UserDefaults

作为一名iOS开发者,你可能总是使用UserDefaults来保存和检索本地数据。

// This is how you save a value
UserDefaults.standard.set("123", forKey: "userID")
// This is how you read it
let userID = UserDefaults.standard.string(forKey: "userID")

然而,在一个大项目中与UserDefaults交互是有风险的,可能会发生以下错误:

  • Wrong key
  • Wrong type
  • Keys overlapping(重叠)

1.1. Wrong key ⚠️

我们可以用一个键保存一个值,然后尝试用另一个键读取它。

UserDefaults.standard.set("123", forKey: "userID")
...
let userID = UserDefaults.standard.string(forKey: "UserID")

因为键是大小写敏感的**userID不同于userID **。这似乎是一个很容易避免的错误,但在一个有很多键的大项目中,这是可能发生的。

1.2. Wrong type ⚠️

每次我们与UserDefaults交互时,我们必须记住我们保存和加载的值的类型。

下面的示例包含一个错误,因为我们将userID保存为String,但随后试图将其检索为Int。

UserDefaults.standard.set("123", forKey: "userID")
...
let userID = UserDefaults.standard.int(forKey: "userID")

同样,这似乎很容易在代码中发现,但在大型项目中,这是可能发生的。

1.3. Keys overlapping ⚠️

我们可能会忘记在项目的另一部分中为某个值使用了一个给定的键,而决定为另一个值使用相同的键。

let currentUserID = "123"
UserDefaults.standard.set(currentUserID, forKey: "userID")
// and then in another file of our project 👇
let messageRecipientUserID = "456"
UserDefaults.standard.set(messageRecipientUserID, forKey: "userID")

1.4. A safer way

我们可以改进UserDefaults来解决上面讨论的三个问题吗?

这是一个我们可以用Computed Properties解决的问题。

除了存储属性之外,类、结构和枚举还可以定义计算属性,计算属性实际上并不存储值。相反,它们提供了一个getter和一个可选setter来间接地检索和设置其他属性和值. — The Swift Programming Language

Let’s add the following extension to our project

extension UserDefaults {
    var userID: String? {
        get {
            return string(forKey: #function)
        }
        set {
            set(newValue, forKey: #function)
        }
    }
}

userID计算属性不会在UserDefaults的当前实例中存储任何值。相反,它提供了一个getter(从本地存储检索值)和一个setter(将值写入本地存储)。

1.5. What is #function?

It is a special keyword and is automatically replaced with the name of the function that contains it.

E.g., in our example, it is replaced with the String userID.

这将确保我们用来保存(和检索)UserDefaults值的键是我们定义的属性的名称。

1.6. Test 🔨

Let’s run a quick test

UserDefaults.standard.userID = "123"
if let userID = UserDefaults.standard.userID {
    print(userID)
}
> 123

Cool right? 🎉

如果我们为每个键/值定义一个计算属性,我们希望将其存储到UserDefaults中,那么就自动解决了上面讨论的三个问题。

  1. Wrong key: ✅ fixed 编译器不允许我们使用未定义的属性。
  2. Wrong type: ✅ fixed 因为每个计算属性都有一个类型。
  3. Keys overlapping: ✅ fixed 因为现在键是计算属性的名称,Swift编译器将确保UserDefaults中定义的每个属性都是唯一的。

1.7. Conclusion

这种方法还有最后一个好处:封装。我们现在隐藏(到我们的扩展)一些关于如何保存值的细节。任何调用者都可以简单地使用此属性,而不必担心底层细节。