在类中使用@AppStorage是否合规?如何改用UserDefaults实现视图联动?
@AppStorage在View外部使用的问题与风险
用UserDefaults实现SwiftUI视图联动的正确方式
一、@AppStorage在非View类中使用的问题与风险
@AppStorage是SwiftUI专为视图设计的属性包装器,底层基于UserDefaults实现,但直接在View外部(比如你的BiometricsAuthenticator类)使用会存在以下问题:
- 生命周期不匹配:@AppStorage内部依赖SwiftUI视图的环境订阅机制,非View类的实例生命周期往往和视图生命周期不一致(比如单例长期驻留内存),可能导致内部订阅无法正确清理,引发内存泄漏或状态更新失效。
- 状态流向混乱:将状态直接放在工具类中,其他视图依赖该属性时,难以追踪状态变更的触发源,大型项目中会增加维护成本,状态管理变得不透明。
- API兼容性风险:@AppStorage是SwiftUI专属API,苹果未来可能调整其实现逻辑,比如限制仅能在View或特定SwiftUI相关类型中使用,届时你的代码可能出现兼容性问题。
- 线程安全隐患:你在
evaluatePolicy的后台回调中直接修改appBlocked,而@AppStorage未明确处理跨线程访问的线程安全问题,可能引发数据竞争,导致UI更新异常。
二、用UserDefaults实现视图联动的正确方式
核心思路是将UserDefaults的状态封装到可观察的状态管理类中,让状态变更能被SwiftUI视图感知到。以下是两种可行方案:
方案1:基于ObservableObject(兼容iOS13+)
创建一个遵循ObservableObject的状态管理类,封装UserDefaults读写逻辑,通过@Published属性触发视图更新,同时监听UserDefaults的全局变更通知以保证状态同步:
final class AppAuthState: ObservableObject { static let shared = AppAuthState() private let defaults = UserDefaults.standard private let appBlockedKey = "appBlocked" @Published var appBlocked: Bool { didSet { defaults.set(appBlocked, forKey: appBlockedKey) } } private init() { // 从UserDefaults初始化初始值 appBlocked = defaults.bool(forKey: appBlockedKey) // 监听UserDefaults全局变更,避免外部直接修改导致状态不同步 NotificationCenter.default.addObserver( self, selector: #selector(updateFromDefaults), name: UserDefaults.didChangeNotification, object: nil ) } @objc private func updateFromDefaults() { let newValue = defaults.bool(forKey: appBlockedKey) if newValue != appBlocked { appBlocked = newValue } } // 封装状态修改方法,统一入口 func unlockApp() { appBlocked = false } }
在BiometricsAuthenticator中使用该类:
final class BiometricsAuthenticator { private let authState = AppAuthState.shared func authenticate(reason: String) { let context = LAContext() context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { [weak self] success, _ in guard let self = self else { return } if success { // 回到主线程修改状态,保证UI更新线程安全 DispatchQueue.main.async { self.authState.unlockApp() } } } } }
在视图中订阅状态:
struct ContentView: View { @ObservedObject var authState = AppAuthState.shared var body: some View { if authState.appBlocked { Text("应用已锁定") } else { Text("应用已解锁") } } }
方案2:基于@Observable宏(iOS17+)
如果项目支持iOS17及以上,使用@Observable宏可以简化代码,替代ObservableObject和@Published:
@Observable final class AppAuthState { static let shared = AppAuthState() private let defaults = UserDefaults.standard private let appBlockedKey = "appBlocked" var appBlocked: Bool { didSet { defaults.set(appBlocked, forKey: appBlockedKey) } } private init() { appBlocked = defaults.bool(forKey: appBlockedKey) NotificationCenter.default.addObserver( self, selector: #selector(updateFromDefaults), name: UserDefaults.didChangeNotification, object: nil ) } @objc private func updateFromDefaults() { let newValue = defaults.bool(forKey: appBlockedKey) if newValue != appBlocked { appBlocked = newValue } } func unlockApp() { appBlocked = false } }
视图中使用:
struct ContentView: View { @State var authState = AppAuthState.shared var body: some View { if authState.appBlocked { Text("应用已锁定") } else { Text("应用已解锁") } } }
额外注意事项
- 线程安全:所有修改状态的操作必须在主线程执行,因为SwiftUI的视图更新依赖主线程,避免后台线程直接修改导致的异常。
- 避免单例滥用:如果项目需要测试或多实例场景,可以通过依赖注入的方式传递
AppAuthState实例,而非直接使用单例。
内容的提问来源于stack exchange,提问作者arroyot24
相关产品推荐
相关产品推荐

