Swift 风格且现代的 UserDefaults
在应用多次启动之间持久存储键值对。
它在底层使用 UserDefaults
,但对外暴露类型安全的 facade,并提供许多便捷功能。
它已被 我的所有应用(超过 400 万用户)在生产环境中使用。
UserDefaults
值更改时更新视图。Codable
。Toggle
组件。在 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)
// ^ ^ ^ ^
// Key Type UserDefaults name Default value
}
然后您可以将其作为 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"
(只要枚举的原始值是任何支持的类型,这就有效)
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
您可以使用 @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
中使用。
通过 @ObservableDefault
宏,您可以在使用 Observation 框架的 @Observable
类内部使用 Defaults
。这样做就像导入 DefaultsMacros
并在属性中添加两行代码一样简单(请注意,需要添加 @ObservationIgnored
以防止与 @Observable
冲突)
重要提示
使用宏时,构建时间将会增加。
Swift 宏依赖于 swift-syntax
包。这意味着当您编译包含宏作为依赖项的代码时,您还必须编译 swift-syntax
。众所周知,这样做会对构建时间产生严重影响,虽然这是一个正在跟踪的问题(请参阅 swift-syntax
#2421),但目前尚未实施解决方案。
import Defaults
import DefaultsMacros
@Observable
final class UnicornManager {
@ObservableDefault(.hasUnicorn)
@ObservationIgnored
var hasUnicorn: Bool
}
还有一个 SwiftUI.Toggle
包装器,可以更轻松地基于具有 Bool
值的 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("Show All-Day Events", key: .showAllDayEvents)
// Note that this has to be directly attached to `Defaults.Toggle`. It's not `View#onChange()`.
.onChange {
print("Value", $0)
}
}
}
extension Defaults.Keys {
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
}
// …
Task {
for await value in Defaults.updates(.isUnicornMode) {
print("Value:", 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 {
// Update `.key1` without propagating the change to listeners.
Defaults[.key1] = 11
}
// This will be propagated.
Defaults[.someKey] = true
}
这也有效
extension Defaults.Keys {
static let isUnicorn = Key<Bool>("isUnicorn", default: true)
}
UserDefaults.standard[.isUnicorn]
//=> true
let extensionDefaults = UserDefaults(suiteName: "com.unicorn.app")!
extension Defaults.Keys {
static let isUnicorn = Key<Bool>("isUnicorn", default: true, suite: extensionDefaults)
}
Defaults[.isUnicorn]
//=> true
// Or
extensionDefaults[.isUnicorn]
//=> true
当您创建 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
中注册默认值。
类型:class
存储键。
Defaults.Key<T>(_ name: String, default: T, suite: UserDefaults = .standard)
类型:class
使用默认值创建键。
默认值将写入实际的 UserDefaults
,并可在其他地方使用。例如,使用 Interface Builder 绑定。
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
的类型的实例。
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<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
设置新值。类型:func
将给定的键重置为其默认值。
您还可以指定字符串键,如果您需要将某些键存储在集合中,这可能很有用,因为无法将 Defaults.Key
存储在集合中,因为它是一个泛型。
Defaults.removeAll(suite: UserDefaults = .standard)
类型:func
从给定的 UserDefaults
套件中删除所有条目。
执行闭包而不触发更改事件。
在闭包内进行的任何 Defaults
键更改都不会传播到 Defaults
事件侦听器 (Defaults.observe()
和 Defaults.publisher()
)。当您想在侦听同一键的更改的回调中更改键时,这对于防止无限递归很有用。
获取/设置 Defaults
项,并在值更改时更新 SwiftUI 视图。
public protocol DefaultsCollectionSerializable: Collection, Defaults.Serializable {
init(_ elements: [Element])
}
类型:protocol
可以存储到原生 UserDefaults
中的 Collection
。
它应该具有初始化器 init(_ elements: [Element])
以便 Defaults
进行反序列化。
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
的 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
。它的静态 bridge 应该是我们上面创建的 bridge。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] = "🦄"
对于其他类型,您将必须像这样分配它
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"
}
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 & NSSecureCoding
或 Codable & RawRepresentable
枚举的类型。默认情况下,Defaults
将优先选择 Codable
一致性,并使用 CodableBridge
将其序列化为 JSON 字符串。如果您想将其序列化为 NSSecureCoding
数据或使用 RawRepresentable
枚举的原始值,您可以遵循 Defaults.PreferNSSecureCoding
或 Defaults.PreferRawRepresentable
以覆盖默认 bridge
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
一致性,然后存储的表示形式将更改,这可能会使该值无法读取。通过显式定义要使用的 bridge,您可以确保存储的表示形式将始终保持不变。
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
并使其元素符合 Defaults.Serializable & Hashable
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)
}
}
SetBag
的扩展,使其符合 Defaults.SetAlgebraSerializable
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 支持。
前任