SwiftUI中@Published/ObservableObject变量变更后视图未动态更新问题
根本原因定位
- 线程死锁导致最新权限状态读取失败
你编写的同步获取通知设置的方法,依赖信号量阻塞当前线程等待系统回调返回。而UNUserNotificationCenter的所有回调默认都会派发到主队列执行。在权限请求完成的回调流程中,你通过DispatchQueue.main.sync把任务切到主线程后,立刻执行带阻塞等待逻辑的获取设置方法,此时主线程正卡在信号量等待逻辑上,永远无法执行主队列上派发的设置获取回调,最终只能超时返回旧的权限状态,根本拿不到用户完成授权操作后的最新值。
这套同步阻塞写法在冷启动初始化阶段,因为主队列任务量少偶尔能正常返回值;但在系统权限弹窗交互完成的场景下,主队列正处理弹窗相关的系统任务,必然触发死锁。 - 手动重写objectWillChange导致属性绑定失效
你尝试手动补充事件触发逻辑时,在类内部重新声明了let objectWillChange = ObservableObjectPublisher()。这会直接覆盖ObservableObject协议对@Published属性的自动事件绑定能力——原本@Published属性值变更时会自动触发系统默认的objectWillChange事件通知视图刷新,重写后虽然你在willSet里手动补充了send逻辑,但受前面死锁问题影响,属性根本没有被赋入新值,自然无法触发视图更新。 - 阻塞式读取系统异步API的写法本身不符合开发规范
用信号量、线程休眠阻塞等待系统异步回调的写法,既会造成主线程无响应卡顿,也会在特定队列场景下触发不可预期的死锁,完全没必要为了同步读值牺牲稳定性。
可行的修复方案
彻底去掉所有阻塞式的同步读值逻辑,全链路使用系统API原生的异步回调完成状态读取与更新,核心调整点:
- 移除手动重写的objectWillChange逻辑,保留
@Published+ObservableObject的原生绑定实现即可 - 去掉信号量、线程休眠这类阻塞逻辑,把通知设置的读取改成全异步流程
- 权限请求回调触发后,预留极短延迟再拉取设置,避免系统未完成权限状态同步时读到旧值
修复后的核心代码参考:
class TestViewModel: ObservableObject { @Published var notificationSettings: UNNotificationSettings? init() { // 初始化时直接异步拉取权限状态 fetchNotificationSettings() } func requestNotificationPermissions() { let permissionsToRequest: UNAuthorizationOptions = [.alert, .sound, .carPlay, .announcement, .badge] UNUserNotificationCenter.current().requestAuthorization(options: permissionsToRequest) { [weak self] _, _ in DispatchQueue.main.async { // 预留50ms等待系统完成权限状态本地同步 DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { self?.fetchNotificationSettings() } } } } // 全异步的权限状态读取方法 func fetchNotificationSettings() { UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in // 系统回调不一定在主线程,切主线程更新@Published属性 DispatchQueue.main.async { self?.notificationSettings = settings } } } }
配套的视图代码调整(处理设置值未加载完成的状态):
struct ContentView: View { @EnvironmentObject var theViewModel: TestViewModel var body: some View { VStack { if let settings = theViewModel.notificationSettings { Text(verbatim: "Notifications are: \(settings.authorizationStatus)") .padding() .onTapGesture { if settings.authorizationStatus == .notDetermined { theViewModel.requestNotificationPermissions() } } } else { Text("加载通知权限状态中...") .padding() } } } }
关键避坑提示:永远不要在后台队列通过
DispatchQueue.main.sync切回主线程后,再执行带阻塞等待逻辑的方法。这种写法在主队列被其他系统任务占用时,会直接触发应用卡死,是iOS开发中的经典死锁场景。
内容的提问来源于stack exchange,提问作者Jordan
相关产品推荐
相关产品推荐

