SwiftUI中@Published与@propertyWrapper绑定及错误解决
解决@Published与@UserDefault组合使用的错误及UI实时更新问题
问题说明
- 给
user变量同时添加@UserDefault和@Published修饰时,触发错误:'nil' is not compatible with expected argument type 'Published<UserLoginOutput.Response.Data?' - 当前修改UserDefaults中的数据后,必须重启APP才能更新UI,需要实现数据变更后UI实时刷新的效果
现有代码片段
UserDefaultsRepository代码
import Foundation protocol CodableSaving: Codable { } class UserDefaultsRepository: ObservableObject { @UserDefault("user_profile", defaultValue: nil) @Published var user: UserLoginOutput.Response.Data? private let encoder = PropertyListEncoder() private let decoder = PropertyListDecoder() subscript<Value: CodableSaving>(index: DefaultKey<Value>) -> Value { get { if let data = UserDefaults.standard.data(forKey: index.key) { do { return try decoder.decode(Value.self, from: data) } catch { return index.defaultValue } } return index.defaultValue } set { do { let encoded = try encoder.encode(newValue) UserDefaults.standard.set(encoded, forKey: index.key) } catch { } } } subscript<Value: CodableSaving>(index: DefaultKey<Value?>) -> Value? { get { if let data = UserDefaults.standard.data(forKey: index.key) { do { return try decoder.decode(Value.self, from: data) } catch { return index.defaultValue } } return index.defaultValue } set { if let newValue = newValue { do { let encoded = try encoder.encode(newValue) UserDefaults.standard.set(encoded, forKey: index.key) } catch { } } else { UserDefaults.standard.removeObject(forKey: index.key) } } } } class DefaultKey<Value>: TypedKey<Value> { class var user: DefaultKey<UserLoginOutput.Response.Data?> { .init("user", default: nil) } } class TypedKey<Value> { var key: String var defaultValue: Value init(_ key: String, default defaultValue: Value) { self.key = key self.defaultValue = defaultValue } } extension TypedKey where Value: ExpressibleByNilLiteral { convenience init(_ key: String) { self.init(key, default: nil) } }
@UserDefault属性包装器代码
import Foundation @propertyWrapper public struct UserDefault<T> { let key: String let defaultValue: T public init(_ key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue } public var wrappedValue: T { get { return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) } } }
数据模型代码
import Foundation class UserLoginOutput: Codable { var Status: Int? var Message: String? var Response: Response class Response: Codable { var Data: Data class Data: Codable, CodableSaving { var id: String? var email: String? var password: String? var firstname: String? var lastname: String? var region_id: String? } } func getData() -> UserLoginOutput.Response.Data { return self.Response.Data } }
问题根源
- 属性包装器顺序错误:Swift中属性包装器的执行顺序有要求,
@Published需要直接修饰原始属性,而非@UserDefault返回的包装值,当前顺序导致类型不匹配报错。 - 缺少变更通知机制:现有
@UserDefault在修改值时,不会触发ObservableObject的objectWillChange通知,所以UI无法感知数据变化。
解决方案
步骤1:调整属性包装器顺序
将@Published放在@UserDefault前面,确保类型匹配:
class UserDefaultsRepository: ObservableObject { // 调整顺序:@Published在前,@UserDefault在后 @Published @UserDefault("user_profile", defaultValue: nil) var user: UserLoginOutput.Response.Data? // ... 其余代码不变 }
步骤2:升级@UserDefault属性包装器,支持变更通知
修改属性包装器,添加Combine订阅和UserDefaults监听,确保外部修改也能触发通知:
import Foundation import Combine @propertyWrapper public struct UserDefault<T> { let key: String let defaultValue: T private let publisher = PassthroughSubject<T, Never>() public init(_ key: String, defaultValue: T) { self.key = key self.defaultValue = defaultValue // 监听UserDefaults的外部变更 UserDefaults.standard.addObserver(self, forKeyPath: key, options: [.new], context: nil) } public var wrappedValue: T { get { return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue } set { UserDefaults.standard.set(newValue, forKey: key) publisher.send(newValue) } } // 提供投影值用于Combine订阅 public var projectedValue: AnyPublisher<T, Never> { publisher.eraseToAnyPublisher() } // 处理UserDefaults外部变更通知 public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == key, let newValue = change?[.newKey] as? T { publisher.send(newValue) } } // 销毁时移除观察者,避免内存泄漏 deinit { UserDefaults.standard.removeObserver(self, forKeyPath: key) } }
步骤3:在Repository中绑定变更通知
在UserDefaultsRepository的初始化方法中,订阅user的变更,触发objectWillChange通知:
class UserDefaultsRepository: ObservableObject { @Published @UserDefault("user_profile", defaultValue: nil) var user: UserLoginOutput.Response.Data? private var cancellables = Set<AnyCancellable>() private let encoder = PropertyListEncoder() private let decoder = PropertyListDecoder() init() { // 订阅user的变更,触发UI更新通知 $_user .sink { [weak self] _ in self?.objectWillChange.send() } .store(in: &cancellables) } // ... 其余代码不变 }
额外优化:修正数据模型可选类型
原模型中Response和Data为非可选类型,容易导致解码失败,建议改为可选:
class UserLoginOutput: Codable { var Status: Int? var Message: String? var Response: Response? // 改为可选 class Response: Codable { var Data: Data? // 改为可选 class Data: Codable, CodableSaving { var id: String? var email: String? var password: String? var firstname: String? var lastname: String? var region_id: String? } } func getData() -> UserLoginOutput.Response.Data? { return Response?.Data } }
验证效果
- 直接修改
user变量时,@Published会立即触发UI更新 - 即使其他地方直接调用
UserDefaults.standard.set(_:forKey:)修改数据,属性包装器的观察者也会捕获到变更,进而触发UI实时刷新
内容的提问来源于stack exchange,提问作者Aidar
相关产品推荐
相关产品推荐

