默认值
快速现代的 UserDefaults
在应用程序的多次启动之间持久存储键值对。
它底层使用 UserDefaults
,但提供了类型安全的外观和许多便利功能。
它在我所有的应用程序(超过400万用户)的生产环境中使用。
亮点
- 强类型: 你可以预先声明类型和默认值。
- SwiftUI: 当
UserDefaults
值变化时更新视图的属性包装器。 - Codable 支持: 你可以存储任何 Codable 值,比如枚举。
- NSSecureCoding 支持: 你可以存储任何 NSSecureCoding 值。
- 观察: 观察键的变化。
- 可调试: 数据以 JSON 序列化值的形式存储。
- 可定制: 你可以以自己的方式序列化和反序列化自己的类型。
- iCloud 支持: 自动在设备之间同步数据。
相比 @AppStorage
的优势
- 你可以在一个地方定义强类型的标识符,并在任何地方使用它们。
- 你也可以在一个地方定义默认值,而不是必须记住在其他地方使用了什么默认值。
- 你可以在 SwiftUI 之外使用它。
- 你可以观察值的更新。
- 支持更多类型,甚至包括
Codable
。 - 易于为你自己的自定义类型添加支持。
- 附带一个便利的 SwiftUI
Toggle
组件。
兼容性
- macOS 11+
- iOS 14+
- tvOS 14+
- watchOS 9+
- visionOS 1+
安装
在 Xcode 的 "Swift Package Manager" 标签中添加 https://github.com/sindresorhus/Defaults
。
支持的类型
Int(8/16/32/64)
UInt(8/16/32/64)
Double
CGFloat
Float
String
Bool
Date
Data
URL
UUID
Range
ClosedRange
Codable
NSSecureCoding
Color
1 (SwiftUI)Color.Resolved
1 (SwiftUI)NSColor
UIColor
NSFontDescriptor
UIFontDescriptor
Defaults 还支持上述类型包装在 Array
、Set
、Dictionary
、Range
、ClosedRange
中,甚至包装在嵌套类型中。例如,[[String: Set<[String: Int]>]]
。
更多类型,请参见枚举示例、Codable
示例或高级用法。更多示例,请参见 Tests/DefaultsTests。
你可以轻松地为任何自定义类型添加支持。
如果一个类型同时遵循 NSSecureCoding
和 Codable
,则将使用 Codable
进行序列化。
用法
你需要预先声明默认值键,包括类型和默认值。
键名必须是 ASCII,不能以 @
开头,并且不能包含点 (.
)。
import Defaults
extension Defaults.Keys {
static let quality = Key<Double>("quality", default: 0.8)
// ^ ^ ^ ^
// 键 类型 UserDefaults 名称 默认值
}
然后你可以通过 Defaults
全局对象的下标访问它:
Defaults[.quality]
//=> 0.8
Defaults[.quality] = 0.5
//=> 0.5
Defaults[.quality] += 0.1
//=> 0.6
Defaults[.quality] = "🦄"
//=> [Cannot assign value of type 'String' to type 'Double']
你也可以声明可选键,当你不想预先声明默认值时:
extension Defaults.Keys {
static let name = Key<Double?>("name")
}
if let name = Defaults[.name] {
print(name)
}
此时默认值为 nil
。
你还可以指定动态默认值。这在默认值可能在应用程序生命周期内改变时很有用:
extension Defaults.Keys {
static let camera = Key<AVCaptureDevice?>("camera") { .default(for: .video) }
}
枚举示例
enum DurationKeys: String, Defaults.Serializable {
case tenMinutes = "10 Minutes"
case halfHour = "30 Minutes"
case oneHour = "1 Hour"
}
extension Defaults.Keys {
static let defaultDuration = Key<DurationKeys>("defaultDuration", default: .oneHour)
}
Defaults[.defaultDuration].rawValue
//=> "1 Hour"
(只要枚举的原始值是任何支持的类型,这就可以工作)
Codable 示例
struct User: Codable, Defaults.Serializable {
let name: String
let age: String
}
extension Defaults.Keys {
static let user = Key<User>("user", default: .init(name: "Hello", age: "24"))
}
Defaults[.user].name
//=> "Hello"
直接使用键
你不必将键附加到 Defaults.Keys
。
let isUnicorn = Defaults.Key<Bool>("isUnicorn", default: true)
Defaults[isUnicorn]
//=> true
SwiftUI 支持
@Default
你可以使用 @Default
属性包装器来获取/设置 Defaults
项,并在值变化时更新视图。这类似于 @State
。
extension Defaults.Keys {
static let hasUnicorn = Key<Bool>("hasUnicorn", default: false)
}
struct ContentView: View {
@Default(.hasUnicorn) var hasUnicorn
var body: some View {
Text("Has Unicorn: \(hasUnicorn)")
Toggle("Toggle", isOn: $hasUnicorn)
Button("Reset") {
_hasUnicorn.reset()
}
}
}
注意是 @Default
,而不是 @Defaults
。
你不能在 ObservableObject
中使用 @Default
。它是为在 View
中使用而设计的。
Toggle
还有一个 SwiftUI.Toggle
包装器,可以更容易地创建基于 Defaults
键的布尔值切换。
extension Defaults.Keys {
static let showAllDayEvents = Key<Bool>("showAllDayEvents", default: false)
}
struct ShowAllDayEventsSetting: View {
var body: some View {
Defaults.Toggle("Show All-Day Events", key: .showAllDayEvents)
}
}
你也可以监听变化:
struct ShowAllDayEventsSetting: View {
var body: some View {
Defaults.Toggle("显示全天事件", key: .showAllDayEvents)
// 注意这必须直接附加到 `Defaults.Toggle`。它不是 `View#onChange()`。
.onChange {
print("值", $0)
}
}
}
观察键的变化
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
// …
Task {
for await value in Defaults.updates(.isUnicornMode) {
print("值:", value)
}
}
与原生 UserDefaults
键观察相比,这里你会收到一个强类型的变化对象。
将键重置为默认值
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
Defaults[.isUnicornMode] = true
//=> true
Defaults.reset(.isUnicornMode)
Defaults[.isUnicornMode]
//=> false
这对可选类型的 Key
也适用,它会被重置回 nil
。
控制变更事件的传播
在 Defaults.withoutPropagation
闭包中进行的更改不会传播到观察回调(Defaults.observe()
或 Defaults.publisher()
),因此可以防止无限递归。
let observer = Defaults.observe(keys: .key1, .key2) {
// …
Defaults.withoutPropagation {
// 更新 `.key1` 而不将变化传播给监听器。
Defaults[.key1] = 11
}
// 这个会被传播。
Defaults[.someKey] = true
}
它只是带有语法糖的 UserDefaults
这也可以工作:
extension Defaults.Keys {
static let isUnicorn = Key<Bool>("isUnicorn", default: true)
}
UserDefaults.standard[.isUnicorn]
//=> true
共享 UserDefaults
let extensionDefaults = UserDefaults(suiteName: "com.unicorn.app")!
extension Defaults.Keys {
static let isUnicorn = Key<Bool>("isUnicorn", default: true, suite: extensionDefaults)
}
Defaults[.isUnicorn]
//=> true
// 或者
extensionDefaults[.isUnicorn]
//=> true
默认值被注册到 UserDefaults
当你创建一个 Defaults.Key
时,它会自动将 default
值注册到普通的 UserDefaults
中。这意味着你可以在例如 Interface Builder 的绑定中使用默认值。
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: true)
}
print(UserDefaults.standard.bool(forKey: Defaults.Keys.isUnicornMode.name))
//=> true
注意 具有动态默认值的
Defaults.Key
不会在UserDefaults
中注册默认值。
API
Defaults
Defaults.Keys
类型:class
存储键。
Defaults.Key
(别名 Defaults.Keys.Key
)
Defaults.Key<T>(_ name: String, default: T, suite: UserDefaults = .standard)
类型:class
创建一个带有默认值的键。
默认值被写入实际的 UserDefaults
并可以在其他地方使用。例如,与 Interface Builder 绑定。
Defaults.Serializable
public protocol DefaultsSerializable {
typealias Value = Bridge.Value
typealias Serializable = Bridge.Serializable
associatedtype Bridge: Defaults.Bridge
static var bridge: Bridge { get }
}
类型:protocol
遵循此协议的类型可以与 Defaults
一起使用。
该类型应该有一个静态变量 bridge
,它应该引用一个遵循 Defaults.Bridge
的类型实例。
Defaults.Bridge
public protocol DefaultsBridge {
associatedtype Value
associatedtype Serializable
func serialize(_ value: Value?) -> Serializable?
func deserialize(_ object: Serializable?) -> Value?
}
类型:protocol
Bridge
负责序列化和反序列化。
它有两个关联类型 Value
和 Serializable
。
Value
:你想要使用的类型。Serializable
:存储在UserDefaults
中的类型。serialize
:在存储到UserDefaults
之前执行。deserialize
:从UserDefaults
检索其值后执行。
Defaults.AnySerializable
Defaults.AnySerializable<Value: Defaults.Serializable>(_ value: Value)
类型:class
Defaults.Serializable
值的类型擦除包装器。
get<Value: Defaults.Serializable>() -> Value?
:从 UserDefaults 中检索类型为Value
的值。get<Value: Defaults.Serializable>(_: Value.Type) -> Value?
:指定你想要检索的Value
。这在某些模糊情况下可能有用。set<Value: Defaults.Serializable>(_ newValue: Value)
:为Defaults.AnySerializable
设置新值。
Defaults.reset(keys…)
类型:func
将给定的键重置回它们的默认值。
你也可以指定字符串键,这在你需要在集合中存储一些键时可能很有用,因为由于 Defaults.Key
是泛型,所以无法在集合中存储它。
Defaults.removeAll
Defaults.removeAll(suite: UserDefaults = .standard)
类型:func
从给定的 UserDefaults
套件中删除所有条目。
Defaults.withoutPropagation(_ closure:)
执行闭包而不触发变更事件。
在闭包内进行的任何 Defaults
键更改都不会传播到 Defaults
事件监听器(Defaults.observe()
和 Defaults.publisher()
)。当你想在监听同一个键的变化的回调中更改该键时,这可能有用,可以防止无限递归。
@Default(_ key:)
获取/设置一个 Defaults
项,并在值更改时更新 SwiftUI 视图。
高级用法
Defaults.CollectionSerializable
public protocol DefaultsCollectionSerializable: Collection, Defaults.Serializable {
init(_ elements: [Element])
}
类型:protocol
可以存储到原生 UserDefaults
中的 Collection
。
它应该有一个初始化器 init(_ elements: [Element])
以让 Defaults
进行反序列化。
Defaults.SetAlgebraSerializable
public protocol DefaultsSetAlgebraSerializable: SetAlgebra, Defaults.Serializable {
func toArray() -> [Element]
}
类型:protocol
可以存储到原生 UserDefaults
中的 SetAlgebra
。
它应该有一个函数 func toArray() -> [Element]
以让 Defaults
进行序列化。
高级用法
自定义类型
虽然 Defaults
已经内置支持许多类型,但你可能需要使用自己的自定义类型。以下指南将向你展示如何使自己的自定义类型与 Defaults
一起工作。
- 创建你自己的自定义类型。
struct User {
let name: String
let age: String
}
- 创建一个符合
Defaults.Bridge
的桥接器,负责处理序列化和反序列化。
struct UserBridge: Defaults.Bridge {
typealias Value = User
typealias Serializable = [String: String]
public func serialize(_ value: Value?) -> Serializable? {
guard let value else {
return nil
}
return [
"name": value.name,
"age": value.age
]
}
public func deserialize(_ object: Serializable?) -> Value? {
guard
let object,
let name = object["name"],
let age = object["age"]
else {
return nil
}
return User(
name: name,
age: age
)
}
}
- 创建
User
的扩展,使其符合Defaults.Serializable
。它的静态桥接器应该是我们上面创建的桥接器。
struct User {
let name: String
let age: String
}
extension User: Defaults.Serializable {
static let bridge = UserBridge()
}
- 创建一些键并使用。
extension Defaults.Keys {
static let user = Defaults.Key<User>("user", default: User(name: "Hello", age: "24"))
static let arrayUser = Defaults.Key<[User]>("arrayUser", default: [User(name: "Hello", age: "24")])
static let setUser = Defaults.Key<Set<User>>("user", default: Set([User(name: "Hello", age: "24")]))
static let dictionaryUser = Defaults.Key<[String: User]>("dictionaryUser", default: ["user": User(name: "Hello", age: "24")])
}
Defaults[.user].name //=> "Hello"
Defaults[.arrayUser][0].name //=> "Hello"
Defaults[.setUser].first?.name //=> "Hello"
Defaults[.dictionaryUser]["user"]?.name //=> "Hello"
动态值
可能会有一些情况,你想直接使用 [String: Any]
,但 Defaults
需要其值符合 Defaults.Serializable
。类型擦除器 Defaults.AnySerializable
可以帮助克服这个限制。
Defaults.AnySerializable
仅适用于符合 Defaults.Serializable
的值。
警告:类型擦除器应该只在没有其他方法处理时使用,因为它的性能要差得多。它应该只用于包装类型中。例如,包装在 Array
、Set
或 Dictionary
中。
基本类型
Defaults.AnySerializable
符合 ExpressibleByStringLiteral
、ExpressibleByIntegerLiteral
、ExpressibleByFloatLiteral
、ExpressibleByBooleanLiteral
、ExpressibleByNilLiteral
、ExpressibleByArrayLiteral
和 ExpressibleByDictionaryLiteral
。
这意味着你可以直接赋值这些基本类型:
let any = Defaults.Key<Defaults.AnySerializable>("anyKey", default: 1)
Defaults[any] = "🦄"
其他类型
使用 get
和 set
对于其他类型,你需要这样赋值:
enum mime: String, Defaults.Serializable {
case JSON = "application/json"
case STREAM = "application/octet-stream"
}
let any = Defaults.Key<Defaults.AnySerializable>("anyKey", default: [Defaults.AnySerializable(mime.JSON)])
if let mimeType: mime = Defaults[any].get() {
print(mimeType.rawValue)
//=> "application/json"
}
Defaults[any].set(mime.STREAM)
if let mimeType: mime = Defaults[any].get() {
print(mimeType.rawValue)
//=> "application/octet-stream"
}
包装在 Array
、Set
或 Dictionary
中
Defaults.AnySerializable
也支持上述类型包装在 Array
、Set
、Dictionary
中。
这里是 [String: Defaults.AnySerializable]
的示例:
extension Defaults.Keys {
static let magic = Key<[String: Defaults.AnySerializable]>("magic", default: [:])
}
enum mime: String, Defaults.Serializable {
case JSON = "application/json"
}
// …
Defaults[.magic]["unicorn"] = "🦄"
if let value: String = Defaults[.magic]["unicorn"]?.get() {
print(value)
//=> "🦄"
}
Defaults[.magic]["number"] = 3
Defaults[.magic]["boolean"] = true
Defaults[.magic]["enum"] = Defaults.AnySerializable(mime.JSON)
if let mimeType: mime = Defaults[.magic]["enum"]?.get() {
print(mimeType.rawValue)
//=> "application/json"
}
更多示例,请参见 Tests/DefaultsAnySerializableTests。
对模糊 Codable
类型的序列化
你可能有一个符合 Codable & NSSecureCoding
或 Codable & RawRepresentable
枚举的类型。默认情况下,Defaults
会优先使用 Codable
一致性,并使用 CodableBridge
将其序列化为 JSON 字符串。如果你想将其序列化为 NSSecureCoding
数据或使用 RawRepresentable
枚举的原始值,你可以遵循 Defaults.PreferNSSecureCoding
或 Defaults.PreferRawRepresentable
来覆盖默认桥接器:
enum mime: String, Codable, Defaults.Serializable, Defaults.PreferRawRepresentable {
case JSON = "application/json"
}
extension Defaults.Keys {
static let magic = Key<[String: Defaults.AnySerializable]>("magic", default: [:])
}
print(UserDefaults.standard.string(forKey: "magic"))
//=> application/json
如果我们没有添加 Defaults.PreferRawRepresentable
,存储的表示将会是 "application/json"
而不是 application/json
。
如果你让一个你无法控制的类型遵循 Defaults.Serializable
,这也可能很有用,因为该类型可能随时获得 Codable
一致性,然后存储的表示就会改变,这可能会导致该值不可读。通过明确定义使用哪个桥接器,你可以确保存储的表示始终保持不变。
自定义 Collection
类型
- 创建你的
Collection
,并使其元素符合Defaults.Serializable
。
struct Bag<Element: Defaults.Serializable>: Collection {
var items: [Element]
var startIndex: Int { items.startIndex }
var endIndex: Int { items.endIndex }
mutating func insert(element: Element, at: Int) {
items.insert(element, at: at)
}
func index(after index: Int) -> Int {
items.index(after: index)
}
subscript(position: Int) -> Element {
items[position]
}
}
- 创建
Bag
的扩展,使其符合Defaults.CollectionSerializable
。
extension Bag: Defaults.CollectionSerializable {
init(_ elements: [Element]) {
self.items = elements
}
}
- 创建一些键并使用。
extension Defaults.Keys {
static let stringBag = Key<Bag<String>>("stringBag", default: Bag(["Hello", "World!"]))
}
Defaults[.stringBag][0] //=> "Hello" Defaults[.stringBag][1] //=> "World!"
### 自定义 `SetAlgebra` 类型
1. 创建你的 `SetAlgebra` 并使其元素符合 `Defaults.Serializable & Hashable`
```swift
struct SetBag<Element: Defaults.Serializable & Hashable>: SetAlgebra {
var store = Set<Element>()
init() {}
init(_ store: Set<Element>) {
self.store = store
}
func contains(_ member: Element) -> Bool {
store.contains(member)
}
func union(_ other: SetBag) -> SetBag {
SetBag(store.union(other.store))
}
func intersection(_ other: SetBag) -> SetBag {
var setBag = SetBag()
setBag.store = store.intersection(other.store)
return setBag
}
func symmetricDifference(_ other: SetBag) -> SetBag {
var setBag = SetBag()
setBag.store = store.symmetricDifference(other.store)
return setBag
}
@discardableResult
mutating func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element) {
store.insert(newMember)
}
mutating func remove(_ member: Element) -> Element? {
store.remove(member)
}
mutating func update(with newMember: Element) -> Element? {
store.update(with: newMember)
}
mutating func formUnion(_ other: SetBag) {
store.formUnion(other.store)
}
mutating func formSymmetricDifference(_ other: SetBag) {
store.formSymmetricDifference(other.store)
}
mutating func formIntersection(_ other: SetBag) {
store.formIntersection(other.store)
}
}
- 创建一个符合
Defaults.SetAlgebraSerializable
的SetBag
扩展
extension SetBag: Defaults.SetAlgebraSerializable {
func toArray() -> [Element] {
Array(store)
}
}
- 创建一些键并使用它。
extension Defaults.Keys {
static let stringSet = Key<SetBag<String>>("stringSet", default: SetBag(["Hello", "World!"]))
}
Defaults[.stringSet].contains("Hello") //=> true
Defaults[.stringSet].contains("World!") //=> true
常见问题
如何存储任意值的字典?
在 Defaults
v5 之后,你不需要使用 Codable
来存储字典,Defaults
原生支持存储字典。
关于 Defaults
支持的类型,请参见支持的类型。
这与 SwiftyUserDefaults
有什么不同?
它受到该包和其他解决方案的启发。主要区别在于该模块不硬编码默认值,并提供 Codable 支持。
维护者
前任
相关
- KeyboardShortcuts - 为你的 macOS 应用添加用户可自定义的全局键盘快捷键
- LaunchAtLogin - 为你的 macOS 应用添加"登录时启动"功能
- DockProgress - 在你的应用的 Dock 图标中显示进度
- Gifski - 在你的 Mac 上将视频转换为高质量 GIF
- 更多…