Is this an appropriate way to observer multiple `NotificationCenter.default.notifications` sequences?

The below code works, but I'm wondering if it is the correct usage...

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    var notificationsTask: Task<(), Never>?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        notificationsTask = Task { await foreground() }
        notificationsTask = Task { await background() }
    }
    
    deinit {
        notificationsTask?.cancel()
    }

    func foreground() async {
        for await _ in NotificationCenter.default.notifications(named: UIScene.willEnterForegroundNotification) {
            print("will enter foreground")
        }
    }
    
    func background() async {
        for await _ in NotificationCenter.default.notifications(named: UIScene.didEnterBackgroundNotification) {
            print("did enter background")
        }
    }

}

That's how I've been doing it in theory, but I moved away from doing it with the async APIs because they delay the call versus the traditional APIs, meaning for these specific APIs (entering foreground/background) they're no longer as accurate.

For instance willEnterForeground is (per docs) supposed to come, well, before foreground is entered, and it does with the old APIs, but not if you use an async sequence, which makes trusting the activationState difficult.

3 Likes

Since you're assigning both tasks to the same property you'd only cancel the second task on deinit and the first one would keep on running. You'll want to store your tasks in an array or two separate properties to make sure that you can cancel them both.

I'm also not entirely sure that you don't have a retain cycle here since your Task has a strong implicit self capture to call and await your foreground() and background() methods. You'll want to make sure those tasks use a [weak self] instead.

2 Likes

I wonder what are the advantages of this new method compared to the older:

    NotificationCenter.default.addObserver(forName: UIScene.willEnterForegroundNotification, object: nil, queue: nil) { [weak self] _ in
        guard let self else { return }
        // ...
    }

which doesn't require having a variable or an explicit cancellation in deinit (although you may still have a variable if you need to cancel notification subscription earlier).

I came across the cancellation issue as I was testing this further. It does seem like the most appropriate way to observe multiple notification sequences is to have multiple tasks.

Yeah multiple tasks is pretty much unavoidable since each task will be "stuck" iterating a sequence

I think it's much tidier in SwiftUI:

.task {
      for await _ in NotificationCenter.default.notifications(named: UIScene.didEnterBackgroundNotification) {
      // do stuff
     }
}
.task {
     for await _ in NotificationCenter.default.notifications(named: UIScene.willEnterForegroundNotification) {
    // do stuff
     }
}

Yep, in SwiftUI it's nicer. I'd further wrap to make it even tidier:

        .observe(UIScene.didEnterBackgroundNotification) {
            // do stuff
        }
        ...

struct NotificationModifier: ViewModifier {
    let name: Notification.Name
    let callback: () -> Void
    
    func body(content: Content) -> some View {
        content
            .task {
                for await _ in NotificationCenter.default.notifications(named: name) {
                    callback()
                }
            }
    }
}

extension View {
    func observe(_ name: Notification.Name, _ callback: @escaping () -> Void) -> some View {
        modifier(NotificationModifier(name: name, callback: callback))
    }
}

For this example, In SwiftUI, you have ScenePhase which means you don't need to deal with asynchronous sequences at all.

For other use cases, such as OP's, unless you'd be happy with the event arriving via the equivalent of a call to DispatchQueue.main.async { ... } (and more than likely a round-trip to some other queue, too), I wouldn't use asynchronous sequences at all. Asynchronous sequences are great tools, and definitely have their place, but in my opinion that place is not for same actor (i.e. @MainActor to @MainActor) event dispatch/observation, especially if timing/ordering or maintaining some local invariant is important.

1 Like