🐼 SwiftUI 使用包装器属性封装 UserDefaults

2023/12/24

背景:开发 app 合理规划 UserDefault 的 KeyValue 很重要,如果前期没有合理规划进行统一管理,后期维护非常麻烦。

本文使用 SwiftUI 包装器属性 propertyWrapper 来封装 UserDefaults,所有的缓存在 Defaults 定义,使用时可以定义 @Default(.key) var key 拿到相关值。

//
//  UserDefaultsManager.swift
//  BarrageWords
//
//  Created by csd on 2023/9/14.
//

import Foundation
import SwiftUI

// MARK: 管理应用所有的 UserDefaults
public class Defaults: ObservableObject {
    public static let shared = Defaults()
    
    @AppStorage("hasPro") var hasPro: Bool = false // 是否订阅 pro
    
}

// MARK: UserDefaults 添加 @Default 包装器
// - 使用方法: @Default(\.key) var key
@propertyWrapper
public struct Default<T>: DynamicProperty {
    @ObservedObject
    private var defaults: Defaults
    
    private let keyPath: ReferenceWritableKeyPath<Defaults, T>
    
    public init(_ keyPath: ReferenceWritableKeyPath<Defaults, T>, defaults: Defaults = .shared) {
        self.keyPath = keyPath
        self.defaults = defaults
    }
    
    public var wrappedValue: T {
        get { defaults[keyPath: keyPath] }
        nonmutating set { defaults[keyPath: keyPath] = newValue }
    }
    
    public var projectedValue: Binding<T> {
        Binding(
            get: { defaults[keyPath: keyPath] },
            set: { value in
                defaults[keyPath: keyPath] = value
            }
        )
    }
}

// UserDefaults 支持 Date 类型
extension Date: 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
    }
}

使用方式:

import SwiftUI

struct ContentView: View {

    @Default(\.hasPro)
    private(set) var hasPro: Bool

    var body: some View {
        Text(hasPro ? "已订阅": "还没呢。")
    }
}