使用async await从init观察Notification Center是否可行?

3
我希望使用异步等待API处理异步序列,观察NotificationCenter中的.EKEventStoreChanged通知。以下代码可以正常工作,但我不确定在init()中创建任务(因此观察更改)是否是一个好主意,或者是否有更好的选择。我有点担心这是对init()的误用,因为现在,如果我正确地看到它,init()必须在整个ObservableObject的生命周期内运行,但init()只是在对象创建期间运行的。
class CalendarState: ObservableObject {
    // ...
    
    init() {
       // ...
        
        Task{
            await observeCalendar()
        }
    }

    func observeCalendar() async {
        for await _ in NotificationCenter.default.notifications(named: .EKEventStoreChanged) {
            await loadEvents(date: date)
    }
}

任何帮助、技巧或经验都受欢迎 - 谢谢 :D

看起来你知道答案 :) - Asperi
好的,谢谢@Asperi :D 有没有更好的地方调用observeCalendar()?它应该观察整个生命周期。 - hri
在我看来,这是一种可憎的做法。更好的选择是使用NotificationCenter的发布者来处理可能的多个事件。 - cora
完成一个任务似乎并不是很好:你可以将该类添加为观察者或创建一个发布者。 - Ptit Xav
谢谢您的建议!这里不使用Task的具体原因是什么?我试图在没有Combine的情况下进行开发,并认为利用新的异步API将是一个很好的选择:D - hri
1
使用异步序列notifications没有任何问题。(他们把它放在那里是有原因的。LOL。) 但Task的问题在于,您选择了退出结构化并发,因此必须自行手动执行cancel - Rob
1个回答

9
在 SwiftUI 中,.task 修改器将在视图被移除时自动取消。因此,由于结构化并发,通知的 AsyncSequence 也将被取消:
struct DetailsView: View {
    var body: some View {
        Button("Dismiss") {
            ...
        }
        .task {
            await addNotificationsHandler()
        }
    }

    func addNotificationsHandler() async {
        for await notification in NotificationCenter.default.notifications(named: .foo) {
            foo(with: notification)
        }
    }
}

在UIKit中,你必须更加小心,因为异步序列不会自动取消。因此,你需要跟踪该AsyncSequenceTask并在视图消失时cancel它:
private var notificationsTask: Task<(), Never>?

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    notificationsTask = Task {
        await addNotificationsHandler()
    }
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)

    notificationsTask?.cancel()
}

func addNotificationsHandler() async {
    for await notification in NotificationCenter.default.notifications(named: .foo) {
        foo(with: notification)
    }
}

底线是,在上述的UIKit示例中,你必须手动取消循环遍历AsyncSequenceTask。在SwiftUI中,.task修饰符会自动为你取消,因此AsyncSequence将被自动取消。
底线是,如果你在Task内开始循环遍历AsyncSequence,那么请确保在完成后cancelTask。根据定义,Task不遵循结构化并发性,因此你必须自己处理取消操作。
如果您有多个通知需要观察,可以使用任务组。例如,在SwiftUI中,以下是一个示例,观察应用程序何时变为非活动状态以及何时变为活动状态:
struct SampleView: View {
    @StateObject var viewModel = SampleViewModel()

    var body: some View {
        VStack {  }
        .task {
            await viewModel.addNotificationsHandlers()
        }
    }
}

@MainActor
class SampleViewModel: ObservableObject {
    func addNotificationsHandlers() async {
        await withTaskGroup(of: Void.self) { group in
            group.addTask { await self.addBecomeActiveHandler() }
            group.addTask { await self.addResignActiveHandler() }
        }
    }

    func addBecomeActiveHandler() async {
        for await notification in NotificationCenter.default.notifications(named: UIApplication.didBecomeActiveNotification) {
            handleBecomeActive(for: notification)
        }
    }

    func addResignActiveHandler() async {
        for await notification in NotificationCenter.default.notifications(named: UIApplication.willResignActiveNotification) {
            handleResignActive(for: notification)
        }
    }

    func handleBecomeActive(for notification: Notification) {  }

    func handleResignActive(for notification: Notification) {  }
}

或者在UIKit中:

private var notificationsTask: Task<(), Never>?

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    addNotificationsHandlers()
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)

    removeNotificationsHandlers()
}

func addNotificationsHandlers() {
    notificationsTask = Task {
        await withTaskGroup(of: Void.self) { group in
            group.addTask { await self.addBecomeActiveHandler() }
            group.addTask { await self.addResignActiveHandler() }
        }
    }
}

func removeNotificationsHandlers() {
    notificationsTask?.cancel()
}

func addBecomeActiveHandler() async {
    for await notification in NotificationCenter.default.notifications(named: UIApplication.didBecomeActiveNotification) {
        handleBecomeActive(for: notification)
    }
}

func addResignActiveHandler() async {
    for await notification in NotificationCenter.default.notifications(named: UIApplication.willResignActiveNotification) {
        handleResignActive(for: notification)
    }
}

func handleBecomeActive(for notification: Notification) {  }

func handleResignActive(for notification: Notification) {  }

感谢您的回答@Rob!很高兴听到使用通知异步序列的方法是有效的 - 我正在使用SwiftUI,因此任务会自动取消。我已经尝试在视图上使用.task修饰符,但我正在为macOS构建一个菜单栏应用程序,它应该开始监听通知,而不必绘制任何视图(这意味着.task {...}没有启动) - 这就是我想到在init()中调用函数的想法的原因。有什么想法可以在整个应用程序生命周期内调用函数以便监听通知? - hri
我不太了解菜单栏应用程序的生命周期,无法回答那个问题,但我很惊讶.task不能正常工作。 - Rob
@Rob 除非我弄错了,在 UIKit 中,您需要多个 Taskasync 函数来观察多个异步通知序列。例如,如果您想要观察 UIScene.didEnterBackgroundNotification 的通知,则无法在 addNotificationsHandler() 中添加另一个 for await,因为它只能监视 for await ... foo 序列。 - Stuart Breckenridge
是的,您将拥有单独的NotificationCenter.Notifications异步序列(因此对于每个通知名称都有一个单独的for-await-in循环)。就我个人而言,我会坚持使用一个Task,但让它创建一个任务组(例如,对于每种通知类型使用withTaskGroupaddTask)。这样,我只需要取消一个任务,所有通知序列都将被取消。简而言之,避免创建比您绝对必须要多的非结构化Task对象。 - Rob
@StuartBreckenridge - 我已经更新了我的答案,展示了如何使用多个异步序列。请注意,你可以有多个Task对象,但我更喜欢使用任务组(这样,当我们取消通知任务时,所有的notifications序列将自动取消)。 - Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接