应用后台切换后无响应:通知视图定时移除.task逻辑异常
通知定时移除导致应用切回后台后无响应问题排查与解决
问题现象
实现了5秒自动消失的通知视图,常规使用正常,但出现以下异常流程:
- 用户触发操作显示通知;
- 立即切换到其他应用,停留数秒后切回;
- 应用出现无响应冻结。
控制台仅输出两条无关日志:
-[RTIInputSystemClient remoteTextInputSessionWithID:performInputOperation:] perform input operation requires a valid sessionID. inputModality = Keyboard, inputOperation = dismissAutoFillPanel, customInfoType = UIUserInteractionRemoteInputOperations
Snapshotting a view (0x109264080, UIKeyboardImpl) that is not in a visible window requires afterScreenUpdates:YES.
已确认问题根源:注释掉定时移除的.task逻辑后,冻结现象消失。尝试过两种实现方式,问题都存在:
- 定时逻辑放在
addNotification方法内; - 定时逻辑放在
NotificationBoxModifier的.task修饰符中。
相关代码
AppState 实现
class AppState: ObservableObject { static let shared = AppState() @Published var notificationArray: [NotificationItem] = [] func addNotification(_ notification: NotificationItem) { notificationBoxArray.append(notification) } func removeNotification(_ notification: NotificationItem) { self.notificationBoxArray.removeAll { $0.id == notification.id } } }
NotificationBoxModifier 实现
struct NotificationBoxModifier: ViewModifier { func body(content: Content) -> some View { content .frame(maxWidth: .infinity, maxHeight: .infinity) .overlay( ZStack { mainNotificationBoxView() }.animation(.spring(), value: AppState.shared.notificationBoxArray) ) .enableInjection() } #if DEBUG @ObserveInjection var forceRedraw #endif @ViewBuilder func mainNotificationBoxView() -> some View { if AppState.shared.notificationArray.count > 0 { VStack { ForEach(AppState.shared.notificationArray){ notification in NotificationBoxView(notificationText: notification.message, notificationType: notification.type) {} .padding(.bottom,16) .task { do { try await Task.sleep(seconds: 5) await MainActor.run { AppState.shared.removeNotification(notification) } } catch { logger.error("Task.sleep error \(error.localizedDescription)") } } } Spacer() } .transition(.move(edge: .top)) } } }
之前的 addNotification 实现
func addNotification(_ notification: NotificationItem) { notificationBoxArray.append(notification) Task { do { try await Task.sleep(seconds: 5) await MainActor.run { AppState.shared.removeNotification(notification) } } catch { logger.error("Task.sleep error \(error.localizedDescription)") } } }
问题原因
核心问题在于应用进入后台时,Task.sleep或定时器仍在执行,切回前台时任务尝试修改UI状态,但此时应用的UI线程可能处于不稳定状态,或者任务持有了已失效的视图/数据引用,导致UI更新时出现线程冲突或状态不一致。
另外,.task修饰符的生命周期和视图绑定,当应用进入后台,视图的活跃状态发生变化,但任务不会自动暂停,切回前台后任务继续执行并触发@Published数组更新,进而触发动画,此时UI线程可能无法正常处理这些更新,最终导致应用冻结。
解决方案
方案1:使用定时器+应用生命周期监听(推荐)
改用Timer实现定时移除,并监听应用前后台状态,后台时暂停定时器,前台时恢复剩余计时:
import UIKit import Combine class AppState: ObservableObject { static let shared = AppState() @Published var notificationArray: [NotificationItem] = [] private var timers: [UUID: Timer] = [:] private var cancellables = Set<AnyCancellable>() private init() { // 监听应用退到后台事件 NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification) .sink { [weak self] _ in self?.pauseAllTimers() } .store(in: &cancellables) // 监听应用回到前台事件 NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) .sink { [weak self] _ in self?.resumeAllTimers() } .store(in: &cancellables) } func addNotification(_ notification: NotificationItem) { notificationArray.append(notification) // 创建5秒后执行的定时器 let timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in guard let self = self else { return } self.removeNotification(notification) self.timers.removeValue(forKey: notification.id) } timers[notification.id] = timer } func removeNotification(_ notification: NotificationItem) { notificationArray.removeAll { $0.id == notification.id } // 移除对应定时器,避免内存泄漏 if let timer = timers.removeValue(forKey: notification.id) { timer.invalidate() } } private func pauseAllTimers() { // 将定时器触发时间设为遥远未来,实现暂停 for timer in timers.values { timer.fireDate = Date.distantFuture } } private func resumeAllTimers() { // 计算剩余时间,恢复定时器 for (id, timer) in timers { let elapsed = Date().timeIntervalSince(timer.fireDate.addingTimeInterval(-5)) let remaining = max(0, 5 - elapsed) timer.fireDate = Date().addingTimeInterval(remaining) } } }
方案2:优化Task实现,绑定视图与应用生命周期
如果坚持使用Task,需要在视图消失或应用后台时主动取消任务:
struct NotificationBoxView: View { let notification: NotificationItem let onTap: () -> Void @State private var autoRemoveTask: Task<Void, Error>? var body: some View { // 你的通知视图UI HStack { Text(notification.message) // 其他UI元素 } .onAppear { autoRemoveTask = Task { do { try await Task.sleep(seconds: 5) await MainActor.run { AppState.shared.removeNotification(notification) } } catch is CancellationError { // 任务被取消,无需处理 } catch { logger.error("Notification auto-remove task error: \(error.localizedDescription)") } } } .onDisappear { // 视图消失时取消任务 autoRemoveTask?.cancel() } .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { _ in // 应用退后台时取消任务 autoRemoveTask?.cancel() } } }
额外注意事项
- 确保
NotificationItem遵循Identifiable协议,id为唯一值; - 所有修改
@Published数组的操作必须在主线程执行(用MainActor.run包裹); - 避免在任务中持有外部对象的强引用,使用
[weak self]防止内存泄漏; - 动画触发条件尽量精准,避免不必要的UI重绘。
内容的提问来源于stack exchange,提问作者smekers
相关产品推荐
相关产品推荐

