You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
    }
}

问题根源

  1. 属性包装器顺序错误:Swift中属性包装器的执行顺序有要求,@Published需要直接修饰原始属性,而非@UserDefault返回的包装值,当前顺序导致类型不匹配报错。
  2. 缺少变更通知机制:现有@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
    }
}

验证效果

  1. 直接修改user变量时,@Published会立即触发UI更新
  2. 即使其他地方直接调用UserDefaults.standard.set(_:forKey:)修改数据,属性包装器的观察者也会捕获到变更,进而触发UI实时刷新

内容的提问来源于stack exchange,提问作者Aidar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 21:39:59