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

SwiftUI异步处理通知授权触发Alert的实现问题求助

问题概述
  • 开发依赖UserNotifications的iOS提醒类App(Swift 5.1 + iOS 13.3),无通知权限则无法正常使用
  • 用户更新Reminder实例触发保存操作时,需重新检查通知权限;未授权则弹出Alert引导前往设置(系统首次权限请求仅弹出一次,需自行触发Alert)
  • 实现时遇到两个核心问题:
    1. 在App的init闭包、异步上下文中修改@State变量触发Alert时,出现「逃逸闭包捕获mutating 'self'参数」编译错误
    2. 重构后出现主线程外修改绑定变量的运行时紫色警告

问题代码
import SwiftUI
import Combine
import UserNotifications

class NotificationManager: NSObject, UNUserNotificationCenterDelegate { // ObservableObject
static let shared = NotificationManager()

    override private init() {}
    
    // #MARK - UNNotificationCenterDelegate methods
    func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
        print(center)
    }
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        print(response)
        completionHandler()
    }

}

@main
struct ReminderApp: App {
@ObservedObject private var reminder: Reminder
//    @ObservedObject private var notificationManager = NotificationManager.shared

    @MainActor @State var shouldRequestAuthorization: Bool = false
    
    private var cancellables = Set<AnyCancellable>()
    private var persistAction: (() -> Void)? = nil
    
    
    init() {
        let reminder = Reminder.loadOrCreate()
        self._reminder = ObservedObject(wrappedValue: reminder)
        
        let notificationCenter = UNUserNotificationCenter.current()
        notificationCenter.delegate = NotificationManager.shared
        notificationCenter.removeAllDeliveredNotifications() // TODO: trigger this more
        
        func requestAuthorization(completion: @escaping (Bool) -> ()) async throws {
            // TODO: Not re-appearing after first refusal, link user to settings in Alert..
            Task {
                let authorized = try await notificationCenter.requestAuthorization(options: [.sound, .alert])
                completion(authorized)
            }
        }
        
        self.persistAction = {
            try! reminder.save()
    
            // Escaping closure captures mutating self parameter
            Task { @MainActor in
                try await requestAuthorization { authorized in
                    self.shouldRequestAuthorization = authorized
                }
            }
            
            notificationCenter.removeAllPendingNotificationRequests()
    
            if reminder.enabled {
                // Schedule notifications...
            }
        }
        
        
        reminder
            .objectDidChange
            .sink(receiveValue: persistAction!)
            .store(in: &cancellables)

//
//        notificationManager
//            .objectDidChange
//            .sink(receiveValue: ...)
//            .store(in: &cancellables)

    }
    
    
    @Environment(\.scenePhase) private var scenePhase
    var body: some Scene {
        WindowGroup {
            HomeView()
                .environmentObject(reminder)
                .padding(Edge.Set(arrayLiteral: [.horizontal, .vertical]), 50)
                .alert(isPresented: $shouldRequestAuthorization) {
                    let settingsButton = Alert.Button.default(Text("Go to Settings")) {
                        guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else {
                            return
                        }
    
                        if UIApplication.shared.canOpenURL(settingsUrl) {
                            UIApplication.shared.open(settingsUrl, completionHandler: { (success) in
                                print("Settings opened: \(success)") // Prints true
                            })
                        }
                    }
                    
                    return Alert(title: Text("Notifications Permission Needed"),
                          message: Text("Our app needs this to send reminders"),
                          primaryButton: settingsButton,
                          secondaryButton: .cancel())
                }
        }
        .onChange(of: scenePhase) { phase in
            if phase == .background {
                // Perform cleanup when all scenes within
                // MyApp go to the background.
            }
        }
    }

}

解决方案

1. 重构NotificationManager为ObservableObject

将权限状态管理逻辑封装到NotificationManager中,利用@Published变量控制Alert显示,避免在App结构体中直接处理异步逻辑:

import SwiftUI
import Combine
import UserNotifications

class NotificationManager: NSObject, ObservableObject, UNUserNotificationCenterDelegate {
    static let shared = NotificationManager()
    
    @Published var shouldShowPermissionAlert = false
    
    override private init() {
        super.init()
        UNUserNotificationCenter.current().delegate = self
    }
    
    // 检查并处理通知权限状态
    func checkNotificationAuthorization() async throws {
        let status = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus
        switch status {
        case .notDetermined:
            // 首次请求权限,未授权则触发Alert
            let authorized = try await UNUserNotificationCenter.current().requestAuthorization(options: [.sound, .alert])
            if !authorized {
                DispatchQueue.main.async {
                    self.shouldShowPermissionAlert = true
                }
            }
        case .denied, .provisional, .ephemeral:
            // 已拒绝或临时权限,直接触发Alert
            DispatchQueue.main.async {
                self.shouldShowPermissionAlert = true
            }
        case .authorized:
            // 已授权,无需处理
            break
        @unknown default:
            break
        }
    }
    
    // UNNotificationCenterDelegate实现
    func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {}
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        completionHandler()
    }
}

2. 调整App结构体逻辑

遵循SwiftUI生命周期规范,移除init中的异步闭包,改用onChange监听Reminder变化,确保所有UI操作在主线程执行:

@main
struct ReminderApp: App {
    @StateObject private var reminder = Reminder.loadOrCreate()
    @StateObject private var notificationManager = NotificationManager.shared
    
    private var cancellables = Set<AnyCancellable>()
    @Environment(\.scenePhase) private var scenePhase
    
    var body: some Scene {
        WindowGroup {
            HomeView()
                .environmentObject(reminder)
                .padding([.horizontal, .vertical], 50)
                .alert(isPresented: $notificationManager.shouldShowPermissionAlert) {
                    Alert(
                        title: Text("Notifications Permission Needed"),
                        message: Text("Our app needs this to send reminders"),
                        primaryButton: .default(Text("Go to Settings")) {
                            guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return }
                            if UIApplication.shared.canOpenURL(settingsUrl) {
                                UIApplication.shared.open(settingsUrl)
                            }
                        },
                        secondaryButton: .cancel()
                    )
                }
                .onAppear {
                    // App启动时检查权限
                    Task {
                        try await notificationManager.checkNotificationAuthorization()
                    }
                }
        }
        .onChange(of: reminder.objectDidChange) { _ in
            // Reminder更新时触发保存和权限检查
            try! reminder.save()
            UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
            
            if reminder.enabled {
                // 这里添加通知调度逻辑
            }
            
            // 重新检查权限
            Task {
                try await notificationManager.checkNotificationAuthorization()
            }
        }
        .onChange(of: scenePhase) { phase in
            if phase == .background {
                // 后台清理逻辑
            }
        }
    }
}

关键修正点

  • 将NotificationManager改为ObservableObject,用@Published变量管理Alert状态,避免在App的init中直接操作@State
  • 权限检查逻辑移至NotificationManager内部,所有修改UI状态的操作通过DispatchQueue.main.async确保在主线程执行
  • 移除init中的persistAction闭包,改用onChange(of: reminder.objectDidChange)监听Reminder变化,解决逃逸闭包捕获self的问题
  • 使用@StateObject替代@ObservedObject初始化核心实例,符合SwiftUI的生命周期管理规范

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 09:34:52