SwiftyUserDefaults

Platforms CI Status CocoaPods compatible Carthage compatible SPM compatible Swift version Swift version Swift version Swift version

用于 NSUserDefaults 的现代 Swift API

SwiftyUserDefaults 通过将富有表现力的 Swifty API 与静态类型的优势相结合,使 user defaults 的使用变得愉快。在一个地方定义你的键,轻松使用值类型,并免费获得额外的安全性和便捷的编译时检查。

先前版本的文档:4.0.0 版本3.0.1 版本
迁移指南:从 4.x 到 5.x从 4.0.0-alpha.1 到 4.0.0-alpha.3从 3.x 到 4.x

5.0.0 版本

特性用法CodableNSCodingRawRepresentable扩展现有类型自定义类型

属性包装器KVOdynamicMemberLookup启动参数实用工具安装

特性

只需一步即可开始使用 SwiftyUserDefaults

定义你的键!

extension DefaultsKeys {
    var username: DefaultsKey<String?> { .init("username") }
    var launchCount: DefaultsKey<Int> { .init("launchCount", defaultValue: 0) }
}

然后使用它 ;-)

// Get and set user defaults easily
let username = Defaults[\.username]
Defaults[\.hotkeyEnabled] = true

// Modify value types in place
Defaults[\.launchCount] += 1
Defaults[\.volume] -= 0.1
Defaults[\.strings] += "… can easily be extended!"

// Use and modify typed arrays
Defaults[\.libraries].append("SwiftyUserDefaults")
Defaults[\.libraries][0] += " 2.0"

// Easily work with custom serialized types
Defaults[\.color] = NSColor.white
Defaults[\.color]?.whiteComponent // => 1.0

如果你使用 Swift 5.1 - 好消息!你也可以使用 keyPath dynamicMemberLookup

Defaults.color = NSColor.white

请参阅 KeyPath dynamicMemberLookup 部分了解更多信息。

用法

定义你的键

为了充分利用 SwiftyUserDefaults,请提前定义你的 user defaults 键

let colorKey = DefaultsKey<String>("color", defaultValue: "")

只需创建一个 DefaultsKey 对象,将你要存储的值的类型放在尖括号中,将键名放在圆括号中,就可以开始了。如果你想要一个非可选值,只需在键中提供一个 defaultValue(查看上面的示例)。

你现在可以使用 Defaults 快捷方式来访问这些值

Defaults[key: colorKey] = "red"
Defaults[key: colorKey] // => "red", typed as String

编译器不会让你设置错误的值类型,并且获取操作会方便地返回 String

使用快捷方式

为了更加方便,通过扩展神奇的 DefaultsKeys 类并添加静态属性来定义你的键

extension DefaultsKeys {
    var username: DefaultsKey<String?> { .init("username") }
    var launchCount: DefaultsKey<Int> { .init("launchCount", defaultValue: 0) }
}

并使用快捷的点语法

Defaults[\.username] = "joe"
Defaults[\.launchCount] += 1

支持的类型

SwiftyUserDefaults 支持所有标准的 NSUserDefaults 类型,例如字符串、数字、布尔值、数组和字典。

这是一个内置的单值 defaults 的完整表格

单值 数组
字符串 [字符串]
整数 [整数]
双精度浮点数 [双精度浮点数]
布尔值 [布尔值]
数据 [数据]
日期 [日期]
URL [URL]
[字符串: 任意] [[字符串: 任意]]

但这还不是全部!

Codable

自版本 4 以来,SwiftyUserDefaults 支持 Codable!只需在你的类型中遵循 DefaultsSerializable

final class FrogCodable: Codable, DefaultsSerializable {
    let name: String
 }

无需任何实现!通过这样做,你将获得指定可选 DefaultsKey 的选项

let frog = DefaultsKey<FrogCodable?>("frog")

此外,你还可以免费获得数组支持

let froggies = DefaultsKey<[FrogCodable]?>("froggies")

NSCoding

NSCoding 在版本 4 之前就已受支持,但在本版本中,我们将支持提升到另一个层次。不再需要自定义下标!像支持 Codable 一样支持你的自定义 NSCoding 类型

final class FrogSerializable: NSObject, NSCoding, DefaultsSerializable { ... }

同样无需任何实现!通过这样做,你将获得指定可选 DefaultsKey 的选项

let frog = DefaultsKey<FrogSerializable?>("frog")

此外,你还可以免费获得数组支持

let froggies = DefaultsKey<[FrogSerializable]?>("froggies")

RawRepresentable

最后但并非最不重要的是,RawRepresentable 支持!同样,情况与 NSCodingCodable 类似

enum BestFroggiesEnum: String, DefaultsSerializable {
    case Andy
    case Dandy
}

同样无需任何实现!通过这样做,你将获得指定可选 DefaultsKey 的选项

let frog = DefaultsKey<BestFroggiesEnum?>("frog")

此外,你还可以免费获得数组支持

let froggies = DefaultsKey<[BestFroggiesEnum]?>("froggies")

扩展现有类型

假设你想扩展对 UIColor 或任何其他 NSCodingCodableRawRepresentable 类型的支持。将其扩展为 SwiftyUserDefaults 友好型应该像这样简单

extension UIColor: DefaultsSerializable {}

如果不是这样,我们有两个选择
a) 这是一个我们不知道如何序列化的自定义类型,在这种情况下,请参阅 自定义类型
b) 这是一个 bug,应该支持它,在这种情况下,请提交一个 issue(+ 你可以使用 自定义类型 方法作为临时的解决方法)

自定义类型

如果你想添加我们尚不支持的你自己的自定义类型,我们已经为你考虑到了。我们使用多种 DefaultsBridge 来指定你如何获取/设置值和值数组。当你查看 DefaultsSerializable 协议时,它期望每种类型都有两个属性:_defaults_defaultsArray,两者都是 DefaultsBridge 类型。

例如,这是一个用于使用 NSKeyedArchiver/NSKeyedUnarchiver 存储/检索单值数据的 bridge

public struct DefaultsKeyedArchiverBridge<T>: DefaultsBridge {

    public func get(key: String, userDefaults: UserDefaults) -> T? {
        userDefaults.data(forKey: key).flatMap(NSKeyedUnarchiver.unarchiveObject) as? T
    }

    public func save(key: String, value: T?, userDefaults: UserDefaults) {
        userDefaults.set(NSKeyedArchiver.archivedData(withRootObject: value), forKey: key)
    }

    public func deserialize(_ object: Any) -> T? {
        guard let data = object as? Data else { return nil }
        return NSKeyedUnarchiver.unarchiveObject(with: data) as? T
    }    
}

用于默认存储/检索数组值的 Bridge

public struct DefaultsArrayBridge<T: Collection>: DefaultsBridge {
    public func save(key: String, value: T?, userDefaults: UserDefaults) {
        userDefaults.set(value, forKey: key)
    }

    public func get(key: String, userDefaults: UserDefaults) -> T? {
        userDefaults.array(forKey: key) as? T
    }

    public func deserialize(_ object: Any) -> T? {
        nil
    }
}

现在,为了在我们的类型中使用这些 bridge,我们只需按如下方式声明它

struct FrogCustomSerializable: DefaultsSerializable {

    static var _defaults: DefaultsKeyedArchiverBridge( { DefaultsKeyedArchiverBridge() }
    static var _defaultsArray: DefaultsKeyedArchiverBridge { DefaultsKeyedArchiverBridge() }

    let name: String
}

不幸的是,如果你发现自己需要自定义 bridge,你可能需要编写自己的 bridge

final class DefaultsFrogBridge: DefaultsBridge {
    func get(key: String, userDefaults: UserDefaults) -> FrogCustomSerializable? {
        let name = userDefaults.string(forKey: key)
        return name.map(FrogCustomSerializable.init)
    }

    func save(key: String, value: FrogCustomSerializable?, userDefaults: UserDefaults) {
        userDefaults.set(value?.name, forKey: key)
    }

    func deserialize(_ object: Any) -> FrogCustomSerializable? {
        guard let name = object as? String else { return nil }

        return FrogCustomSerializable(name: name)
    }
}

final class DefaultsFrogArrayBridge: DefaultsBridge {
    func get(key: String, userDefaults: UserDefaults) -> [FrogCustomSerializable]? {
        userDefaults.array(forKey: key)?
            .compactMap { $0 as? String }
            .map(FrogCustomSerializable.init)
    }

    func save(key: String, value: [FrogCustomSerializable]?, userDefaults: UserDefaults) {
        let values = value?.map { $0.name }
        userDefaults.set(values, forKey: key)
    }

    func deserialize(_ object: Any) -> [FrogCustomSerializable]? {
        guard let names = object as? [String] else { return nil }

        return names.map(FrogCustomSerializable.init)
    }
}

struct FrogCustomSerializable: DefaultsSerializable, Equatable {

    static var _defaults: DefaultsFrogBridge { DefaultsFrogBridge() }
    static var _defaultsArray: DefaultsFrogArrayBridge { DefaultsFrogArrayBridge() }

    let name: String
}

为了支持具有不同 bridge 的现有类型,你可以类似地扩展它

extension Data: DefaultsSerializable {
    public static var _defaultsArray: DefaultsArrayBridge<[T]> { DefaultsArrayBridge() }
    public static var _defaults: DefaultsDataBridge { DefaultsDataBridge() }
}

另外,查看我们的源代码(或测试)以查看更多 bridge 示例。如果你发现自己对所有这些 bridge 感到困惑,请创建一个 issue,我们将想办法解决。

属性包装器

SwiftyUserDefaults 为 Swift 5.1 提供了属性包装器!属性包装器 @SwiftyUserDefault 提供了使用键路径和选项(缓存或观察)的选项。

缓存 意味着我们将为你存储该值,并且几乎永远不会访问 UserDefaults 来获取值,仅在首次获取值时访问。

观察 意味着我们将通过 KVO 观察你的属性,因此你无需担心它是否在其他地方被保存以及你是否使用缓存。

现在是用法!给定键

extension DefaultsKeys {
    var userColorScheme: DefaultsKey<String> { .init("userColorScheme", defaultValue: "default") }
    var userThemeName: DefaultsKey<String?> { .init("userThemeName") }
    var userLastLoginDate: DefaultsKey<Date?> { .init("userLastLoginDate") }
}

你可以声明一个 Settings 结构体

struct Settings {
    @SwiftyUserDefault(keyPath: \.userColorScheme)
    var userColorScheme: String

    @SwiftyUserDefault(keyPath: \.userThemeName, options: .cached)
    var userThemeName: String?

    @SwiftyUserDefault(keyPath: \.userLastLoginDate, options: [.cached, .observed])
    var userLastLoginDate: Date?
}

KVO

KVO 支持所有 DefaultsSerializable 类型。但是,如果你有自定义类型,则需要在其中正确定义 bridge 和序列化。

要观察本地 DefaultsKey 的值

let nameKey = DefaultsKey<String>("name", defaultValue: "")
Defaults.observe(key: nameKey) { update in
	// here you can access `oldValue`/`newValue` and few other properties
}

要观察在 DefaultsKeys 扩展中定义的键的值

Defaults.observe(\.nameKey) { update in
	// here you can access `oldValue`/`newValue` and few other properties
}

默认情况下,我们使用 [.old, .new] 选项进行观察,但你可以提供自己的选项

Defaults.observe(key: nameKey, options: [.initial, .old, .new]) { _ in }

KeyPath dynamicMemberLookup

SwiftyUserDefaults 使 KeyPath dynamicMemberLookup 在 Swift 5.1 中可用!

extension DefaultsKeys {
    var username: DefaultsKey<String?> { .init("username") }
    var launchCount: DefaultsKey<Int> { .init("launchCount", defaultValue: 0) }
}

然后使用它 ;-)

// Get and set user defaults easily
let username = Defaults.username
Defaults.hotkeyEnabled = true

// Modify value types in place
Defaults.launchCount += 1
Defaults.volume -= 0.1
Defaults.strings += "… can easily be extended!"

// Use and modify typed arrays
Defaults.libraries.append("SwiftyUserDefaults")
Defaults.libraries[0] += " 2.0"

// Easily work with custom serialized types
Defaults.color = NSColor.white
Defaults.color?.whiteComponent // => 1.0

启动参数

你喜欢通过 UserDefaults 自定义你的应用/脚本/测试吗?现在我们的方面完全支持它,当然是静态类型的。

注意:目前我们仅支持 BoolDoubleIntString 值,但如果你对此功能有任何其他要求,请打开一个 issue 或 PR,我们可以讨论在新版本中实现它。

你可以在你的 schema 中传递你的参数

Pass launch arguments in Xcode Schema editor.

或者你可以在 XCUIApplication 中使用启动参数

func testExample() {
    let app = XCUIApplication()
    app.launchArguments = ["-skipLogin", "true", "-loginTries", "3", "-lastGameTime", "61.3", "-nickname", "sunshinejr"]
    app.launch()
}

或者将它们作为命令行参数传递!

./script -skipLogin true -loginTries 3 -lastGameTime 61.3 -nickname sunshinejr

实用工具

移除所有键

要重置 user defaults,请使用 removeAll 方法。

Defaults.removeAll()

共享 user defaults

如果你在不同的应用或应用及其扩展之间共享你的 user defaults,你可以通过使用你自己的覆盖 Defaults 快捷方式来使用 SwiftyUserDefaults。只需在你的应用中添加

var Defaults = DefaultsAdapter<DefaultsKeys>(defaults: UserDefaults(suiteName: "com.my.app")!, keyStore: .init())

检查键

如果你想检查我们是否为 DefaultsKey 获取了值

let hasKey = Defaults.hasKey(\.skipLogin)

安装

要求

Swift 版本 >= 4.1
iOS 版本 >= 9.0
macOS 版本 >= 10.11
tvOS 版本 >= 9.0
watchOS 版本 >= 2.0

CocoaPods

如果你使用 CocoaPods,只需将此行添加到你的 Podfile

pod 'SwiftyUserDefaults', '~> 5.0'

通过在你的终端中运行此命令来安装

pod install

然后在所有你使用它的文件中导入库

import SwiftyUserDefaults

Carthage

只需添加到你的 Cartfile

github "sunshinejr/SwiftyUserDefaults" ~> 5.0

Swift Package Manager

只需添加到你的 Package.swift 中的 dependencies 下

let package = Package(
    name: "MyPackage",
    products: [...],
    dependencies: [
        .package(url: "https://github.com/sunshinejr/SwiftyUserDefaults.git", .upToNextMajor(from: "5.0.0"))
    ],
    targets: [...]
)

更多类似内容

如果你喜欢 SwiftyUserDefaults,请查看 SwiftyTimer,它将相同的 swifty 方法应用于 NSTimer

你可能也会对我解释这些库背后的设计过程的博客文章感兴趣

贡献

如果你有任何意见、建议或改进的想法,请随时打开 issue 或 pull request。

作者和许可证

维护者: Łukasz Mróz

创建者: Radek Pietruszewski

SwiftyUserDefaults 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE 文件。