Defaults

Defaults

现代化的UserDefaults封装库

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

DefaultsUserDefaultsSwiftiOSSwiftUIGithub开源项目

默认值

快速现代的 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

编辑推荐精选

TRAE编程

TRAE编程

AI辅助编程,代码自动修复

Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

AI工具TraeAI IDE协作生产力转型热门
蛙蛙写作

蛙蛙写作

AI小说写作助手,一站式润色、改写、扩写

蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。

AI辅助写作AI工具蛙蛙写作AI写作工具学术助手办公助手营销助手AI助手
问小白

问小白

全能AI智能助手,随时解答生活与工作的多样问题

问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。

热门AI助手AI对话AI工具聊天机器人
Transly

Transly

实时语音翻译/同声传译工具

Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。

讯飞智文

讯飞智文

一键生成PPT和Word,让学习生活更轻松

讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。

AI办公办公工具AI工具讯飞智文AI在线生成PPTAI撰写助手多语种文档生成AI自动配图热门
讯飞星火

讯飞星火

深度推理能力全新升级,全面对标OpenAI o1

科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。

热门AI开发模型训练AI工具讯飞星火大模型智能问答内容创作多语种支持智慧生活
Spark-TTS

Spark-TTS

一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型

Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。

咔片PPT

咔片PPT

AI助力,做PPT更简单!

咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。

讯飞绘文

讯飞绘文

选题、配图、成文,一站式创作,让内容运营更高效

讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。

热门AI辅助写作AI工具讯飞绘文内容运营AI创作个性化文章多平台分发AI助手
材料星

材料星

专业的AI公文写作平台,公文写作神器

AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。

下拉加载更多