Project Icon

Defaults

现代化的UserDefaults封装库

Defaults是一个Swift库,为UserDefaults提供了类型安全和现代化的封装。它支持SwiftUI、Codable对象存储和数据观察,并可扩展支持自定义类型。该库简化了iOS和macOS应用中的数据持久化操作,被多个百万用户级应用采用。

默认值

快速现代的 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 还支持上述类型包装在 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)
	//            ^            ^         ^                ^
	//           键           类型   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 负责序列化和反序列化。

它有两个关联类型 ValueSerializable

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

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

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 类型

  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`

```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)
	}
}
  1. 创建一个符合 Defaults.SetAlgebraSerializableSetBag 扩展
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 支持。

维护者

前任

相关

  • KeyboardShortcuts - 为你的 macOS 应用添加用户可自定义的全局键盘快捷键
  • LaunchAtLogin - 为你的 macOS 应用添加"登录时启动"功能
  • DockProgress - 在你的应用的 Dock 图标中显示进度
  • Gifski - 在你的 Mac 上将视频转换为高质量 GIF
  • 更多…

Footnotes

  1. 你不能使用 Color.accentColor 2

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号