🔥

让 SwiftUI @AppStorage 支持数组、Date 与自定义类型

(更新于 )

核心速览@AppStorage 原生不支持 DateArray。通过让自定义类型(或扩展系统类型)遵循 RawRepresentable 协议,并将其 RawValue 映射为 StringData,即可轻松突破这一限制。

背景

@AppStorage 是 SwiftUI 提供的属性包装器,它是 UserDefaults 的优雅封装。然而,默认情况下,它仅支持 Bool, Int, Double, String, URL, Data 等基础类型。当开发者尝试存储 Date、数组 ([String]) 或自定义结构体时,编译器会直接报错。

解决方案

虽然 @AppStorage 不直接支持复杂类型,但它支持任何符合 RawRepresentable 协议且 RawValueIntString 的类型。我们可以利用 JSON 编解码,将复杂对象转换为 String,从而“欺骗” @AppStorage 完成存储。

以下是针对 Swift 6 环境的实现方案(注意 @retroactive 的使用)。

1. 支持 Date 类型

Date 编码为 JSON 字符串存储。

Swift
// Swift 6: 使用 @retroactive 消除追溯一致性警告
extension Date: @retroactive RawRepresentable {
    public typealias RawValue = String
    
    public init?(rawValue: RawValue) {
        guard let data = rawValue.data(using: .utf8),
              let date = try? JSONDecoder().decode(Date.self, from: data) else {
            return nil
        }
        self = date
    }

    public var rawValue: RawValue {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8) else {
            return ""
        }
        return result
    }
}

// 使用
struct DateView: View {
    @AppStorage("lastLogin") var lastLogin = Date()
    // ...
}

2. 支持数组 (Array)

通过泛型扩展,让所有元素符合 Codable 的数组都支持 @AppStorage

Swift
// Swift 6: 使用 @retroactive 明确意图
extension Array: @retroactive RawRepresentable where Element: Codable {
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode([Element].self, from: data) else {
            return nil
        }
        self = result
    }

    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8) else {
            return "[]"
        }
        return result
    }
}

// 使用
struct ListView: View {
    @AppStorage("savedIds") var ids = [1, 2, 3]
    @AppStorage("history") var history: [String] = []
    // ...
}

3. 支持自定义结构体

对于自定义结构体,无需扩展,直接声明遵循 RawRepresentable 即可。

Swift
struct UserSettings: Codable, RawRepresentable {
    var isDarkMode: Bool
    var username: String
    
    // 样板代码:实现 RawRepresentable 将自身转为 String
    public init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode(UserSettings.self, from: data) else {
            return nil
        }
        self = result
    }

    public var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8) else {
            return "{}"
        }
        return result
    }
}

// 使用
@AppStorage("settings") var settings = UserSettings(isDarkMode: false, username: "Guest")

注意事项

  1. 性能开销:每次读写都会触发 JSON 编解码。对于存储大量数据的数组,请谨慎使用此方法,以免阻塞主线程(@AppStorage 的读取通常在主线程)。
  2. Swift 6 兼容性:在 Swift 6 中,对标准库类型(如 DateArray)进行追溯一致性(Retroactive Conformance)扩展会产生警告。建议使用 @retroactive 关键字(如上例所示)来显式声明,或封装一个 Wrapper 结构体来避免污染全局命名空间。
  3. 默认值:如果 JSON 解码失败,@AppStorage 将回退到你提供的默认值。

延伸阅读

相关提示

订阅 Fatbobman 周报

每周精选 Swift 与 SwiftUI 开发技巧,加入众多开发者的行列。

立即订阅