默认值

Swift 风格且现代的 UserDefaults

在应用多次启动之间持久存储键值对。

它在底层使用 UserDefaults,但对外暴露类型安全的 facade,并提供许多便捷功能。

它已被 我的所有应用(超过 400 万用户)在生产环境中使用。

亮点

优于 @AppStorage 的优点

兼容性

安装

在 Xcode 的 “Swift Package Manager” 标签中添加 https://github.com/sindresorhus/Defaults

支持的类型

Defaults 也支持以上类型包装在 ArraySetDictionaryRangeClosedRange 中,甚至包装在嵌套类型中。例如,[[String: Set<[String: Int]>]]

有关更多类型,请参阅 枚举示例Codable 示例高级用法。有关更多示例,请参阅 Tests/DefaultsTests

您可以轻松添加对任何自定义类型的支持。

如果类型同时遵循 NSSecureCodingCodable,则 Codable 将用于序列化。

用法

API 文档。

您预先使用类型和默认值声明默认键。

键名必须是 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"

(只要枚举的原始值是任何支持的类型,这就有效)

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 支持

View 中的 @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 中使用。

@Observable 中的 @ObservableDefault

通过 @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
}

Toggle

还有一个 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
	}

它只是带有语法糖的 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

// Or

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.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 负责序列化和反序列化。

它具有两个关联类型 ValueSerializable

Defaults.AnySerializable

Defaults.AnySerializable<Value: Defaults.Serializable>(_ value: Value)

类型:class

Defaults.Serializable 值的类型擦除包装器。

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 一起工作。

  1. 创建您自己的自定义类型。
struct User {
	let name: String
	let age: String
}
  1. 创建一个符合 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
		)
	}
}
  1. 创建 User 的扩展,使其符合 Defaults.Serializable。它的静态 bridge 应该是我们上面创建的 bridge。
struct User {
	let name: String
	let age: String
}

extension User: Defaults.Serializable {
	static let bridge = UserBridge()
}
  1. 创建一些键并享受它。
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 的值。

警告:类型擦除器应仅在没有其他方法处理时使用,因为它具有更差的性能。它应仅在包装类型中使用。例如,包装在 ArraySetDictionary 中。

原始类型

Defaults.AnySerializable 符合 ExpressibleByStringLiteralExpressibleByIntegerLiteralExpressibleByFloatLiteralExpressibleByBooleanLiteralExpressibleByNilLiteralExpressibleByArrayLiteralExpressibleByDictionaryLiteral

这意味着您可以直接分配这些原始类型

let any = Defaults.Key<Defaults.AnySerializable>("anyKey", default: 1)
Defaults[any] = "🦄"

其他类型

使用 getset

对于其他类型,您将必须像这样分配它

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"
}

包装在 ArraySetDictionary

Defaults.AnySerializable 也支持以上类型包装在 ArraySetDictionary 中。

这是 [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 & NSSecureCodingCodable & RawRepresentable 枚举的类型。默认情况下,Defaults 将优先选择 Codable 一致性,并使用 CodableBridge 将其序列化为 JSON 字符串。如果您想将其序列化为 NSSecureCoding 数据或使用 RawRepresentable 枚举的原始值,您可以遵循 Defaults.PreferNSSecureCodingDefaults.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 类型

  1. 创建您的 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]
	}
}
  1. 创建 Bag 的扩展,使其符合 Defaults.CollectionSerializable
extension Bag: Defaults.CollectionSerializable {
	init(_ elements: [Element]) {
		self.items = elements
	}
}
  1. 创建一些键并享受它。
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
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)
	}
}
  1. 创建 SetBag 的扩展,使其符合 Defaults.SetAlgebraSerializable
extension SetBag: Defaults.SetAlgebraSerializable {
	func toArray() -> [Element] {
		Array(store)
	}
}
  1. 创建一些键并享受它。
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 支持。

维护者

前任

相关项目

脚注

  1. 您不能使用 Color.accentColor 2