swift-user-defaults 0.0.5

swift-user-defaults 0.0.5

Ryan PatersonLiam Nichols 维护。



  • Liam Nichols 和 Ryan Paterson

Swift User Defaults

一组针对 Foundation 的 UserDefaults 类的 Swift 仍有优化实用程序。

功能

  • 🔑 常量键 - 使用专用类型管理默认键,以帮助防止错误并保持项目组织。
  • 🦺 类型安全 - 自动转换为正确类型,无需担心 Any?
  • 🔍 观察 - Swift 中轻松实现观察。
  • 👩‍💻 支持 Codable 和 RawRepresentable - 无需额外努力即可一致地编码和解码 CodableRawRepresentable 类型。
  • 🧪 UI 测试中的模拟 - 将为您 UI 测试套件中的默认值直接注入到您的应用程序。
  • 🎁 属性包装器 - 将 SwiftUI 的 @AppStorage 包装器功能带入 Swift,使用 @UserDefault

🔑 常量键

在今天的 UserDefaults 中,您使用给定的 '键' 存储值。此键是 String,随着时间的推移,使用字符串可能导致一些容易忽略的错误,除非你在某个地方定义了自己的常量。

您今天的项目可能需要进行以下操作

let userDefaults = UserDefaults.standard
var value = (userDefaults.object(forKey: "UserCount") as? Int) ?? 0
value += 1
userDefaults.set(value, forKey: "UserCoumt")

如您从上述示例中可以看到,重复使用字符串可能导致由于输入错误而产生的错误,因此一种常见的防范方法是定义常量

struct Constants {
    static let userCountDefaultsKey = "UserCount"
}

// ...

let userDefaults = UserDefaults.standard
var value = (userDefaults.object(forKey: Constants.userCountDefaultsKey) as? Int) ?? 0
value += 1
userDefaults.set(value, forKey: Constants.userCountDefaultsKey)

这会更好,因为您可以放心地使用正确的键,但我们还能做得更好。

类似于 Foundation 的 Notification.Name,SwiftUserDefaults 提供了一种新的 UserDefaults.Key 类型,它作为命名空间,为您提供自己的常量,可以方便地在您的应用程序中使用,无需担心在重构过程中可能出现的错误或问题。

import Foundation
import SwiftUserDefaults

extension UserDefaults.Key {
    /// The number of users interacted with.
    static let userCount = Self("UserCount")

    /// The name of the user.
    static let userName = Self("UserName")

    /// The last visit.
    static let lastVisit = Self("LastVisit")
}

SwiftUserDefaults 在此类型之上提供了一系列附加 API。继续阅读以了解如何使用它们。

🦺 类型安全

当使用 UserDefaults 时,您只能尝试设置布尔值、数据、日期、数字或字符串,以及由这些类型组成的字典或数组,否则您将看到没有编译器保护的运行时崩溃。

SwiftUserDefaults 提供了更安全的 API,结合 UserDefaults.Key 提供了与 UserDefaults 相关的更安全的使用体验。

let userDefaults = UserDefaults.standard
var value = userDefaults.x.object(Int.self, forKey: .userCount) ?? 0
value += 1
userDefaults.x.set(value, forKey: .userCount)

在上面的示例中,key 参数使用 UserDefaults.Key 常量,而值自动转换为已知类型,由通过通过 x 扩展访问更安全的 API 实现。

此外,编译器可以帮助捕获在传递不支持类型到 set(_:forKey:) 时的错误。

struct User {
    let id: UUID
}

func updateCurrentUser(_ user: User) {
    // ❌ Runtime Crash
    userDefaults.set(user.id, forKey: "UserId")
    // SIGABRT
    //
    // Attempt to insert non-property list object
    // DAE8F83E-5760-475D-B28D-D493F695E765 for key UserId

    // ✅ Compile Time Error
    userDefaults.x.set(user.id, forKey: .userId)
    // Instance method 'set(_:forKey:)' requires that 'UUID' conform to 'UserDefaultsStorable'
}

🔍 观察

UserDefaults 是键值观察兼容的,但是您不能使用基于 Swift key 编码的观察覆盖,因为存储的默认值没有关联到实际的属性。SwiftUserDefaults 通过围绕 Objective C 基础的 KVO 方法提供包装,从而帮助解决此问题。

import Foundation
import SwiftUserDefaults

class MyViewController: UIViewController {
    let store = UserDefaults.standard
    var observation: UserDefaults.Observation?

    // ...

    override func viewDidLoad() {
        super.viewDidLoad()

        // ...

        observation = store.x.observeObject(String.self, forKey: .userName) { change in
            self.nameLabel.text = change.value
        }
    }

    deinit {
        observation?.invalidate()
    }
}

《change》属性是UserDefaults.Change枚举,包含两个情况,分别表示.initial值以及任何后续的.update值。如果您不关心这些,可以通过value属性访问底层值。

支持Codable和RawRepresentable

除了支持UserDefaults的默认值类型外,还提供了一些便捷方法,方便使用CodableRawRepresentable类型(包括枚举)。

对于RawRepresentable类型,您可以像使用StringInt值一样使用它们,SwiftUserDefaults将自动读取和写入底层存储中的rawValue

enum Tab: String { // String and Int backed enum's are `RawRepresentable`.
    case home, search, create
}

let initialTab = userDefaults.x.object(Tab.self, forKey: .lastTab) ?? .home
showTab(initialTab)

// ...

func tabDidChange(_ tab: Tab) {
    userDefaults.x.set(tab, forKey: .lastTab)
}

对于Codable类型,您需要传递一个额外的CodingStrategy参数(.json.plist)来指定读取和写入值时使用的编码格式。

struct Activity: Codable {
    let id: UUID
    let name: String
}

let restoredActivity = userDefaults.x.object(Activity.self, forKey: .currentActivity, strategy: .json)

func showActivity(_ activity: Activity) {
    userDefaults.x.set(activity, forKey: .currentActivity, strategy: .json)
}

⚠️ 警告:虽然这些API可能会使您希望将大型模型编码到UserDefaults中,但您应该继续记住,某些平台对UserDefaults存储的大小有严格的限制。

更多信息,请参阅官方Apple 开发者文档

🧪 UI 测试中的模拟

SwiftUserDefaults提供了一种结构化方法,可以将值从UI测试目标注入到您的应用目标的UserDefaults中。这是通过格式化启动参数的负载来实现的,该负载是由UserDefaults读取到NSArgumentDomain的。

MyAppCommon 目标

import SwiftUserDefaults

extension UserDefaults.Key {
    /// The current level of the user
    public static let currentLevel = Self("CurrentLevel")
    /// The name of the user using the app
    public static let userName = Self("UserName")
    /// The unique identifier assigned to this user
    public static let userGUID = Self("UserGUID")
}

MyAppUITests 目标

import MyAppCommon
import SwiftUserDefaults
import XCTest

struct MyAppConfiguration: LaunchArgumentEncodable {
    @UserDefaultOverride(.currentLevel)
    var currentLevel: Int?
    
    @UserDefaultOverride(.userName)
    var userName: String?
    
    @UserDefaultOverride(.userGUID)
    var userGUID = "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"
}

final class MyAppTests: XCTestCase {
    func testMyApp() throws {
        var configuration = MyAppConfiguration()
        container.currentLevel = 8
        container.userName = "John Doe"

        let app = XCUIApplication()
        app.launchArguments = try configuration.encodeLaunchArguments()
        app.launch()

        // ...
    }
}

MyApp 目标

import SwiftUserDefaults
import UIKit

class ViewController: UIViewController {
    // ...

    override func viewDidLoad() {
        super.viewDidLoad()

        let store = UserDefaults.standard
        store.x.object(Int.self, for: .currentLevel) // 8
        store.x.object(String.self, for: .userName) // "John Doe"
        store.x.object(String.self, for: .userGUID) // "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"
    }
}

🎁 属性包装器

SwiftUserDefaults将UserDefaults.Key引入了SwiftUI的@AppStorage属性包装器,此外,它还引入了一个具有类似行为的@UserDefault属性包装器,适用于SwiftUI之外的使用。

使用属性包装器的最简单方法是如下所示:

import SwiftUserDefaults

class MyStore {
    @UserDefault(.userName)
    var userName: String?

    @UserDefault(.currentLevel)
    var currentLevel: Int = 1

    @UserDefault(.difficulty)
    var difficulty: Difficulty = .medium
}

如果您需要能够将依赖项注入到MyStore中,您可以按如下方式操作:

import SwiftUserDefaults

class MyStore {
    @UserDefault var userName: String?
    @UserDefault var currentLevel: Int
    @UserDefault var difficulty: Difficulty

    init(userDefaults store: UserDefaults) {
        _userName = UserDefault(.userName, store: store)
        _currentLevel = UserDefault(.currentLevel, store: store, defaultValue: 1)
        _difficulty = UserDefault(.difficulty, store: store, defaultValue: .medium)
    }
}

最后,通过投影值,@UserDefault允许您重置并观察存储的值

let store = MyStore(userDefaults: .standard)

// Removes the value from user defaults
store.$userName.reset()

// Observes the user default, respecting the default value
let observer = store.$currentLevel.addObserver { change in
    change.value // Int, 1
}

UserDefault.X API一样,属性包装器支持原始类型、RawRepresentableCodable类型。

安装

CocoaPods

CocoaPods是Cocoa项目的依赖项管理器。有关使用和安装说明,请访问他们的网站。要使用CocoaPods将SwiftUserDefaults集成到您的Xcode项目中,请在您的Podfile中指定它。

pod 'swift-user-defaults'

Swift 包管理器

在您的Package.swift中添加以下内容:

dependencies: [
    .package(url: "https://github.com/cookpad/swift-user-defaults.git", .upToNextMajor(from: "0.1.0"))
]

或者使用Xcode中的以下https://github.com/cookpad/swift-user-defaults.git仓库链接。