Question about notifications and async

Question about async execution, Swift 6, and strict concurrency (macOS app). I also have limited knowledge of Swift concurrency, but I learn something new every time I explore it.

I have two solutions for observing notifications:

  • Combine publisher and
  • adding observers to the NotificationCenter.

Xcode is set to Swift 6 language mode and strict concurrency checking, and compiles without warnings and runs as expected.

But, why does closure for NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable complain about main actor isolated property can't be mutated from a nonisolated context, while Combine publisher doesn't when updating properties from within the closures?

Both classes run on the main thread. Below are two snippets of code:

// Combine Publisher
NotificationCenter.default.publisher(
  for: NSNotification.Name.NSFileHandleDataAvailable)
  .sink { [self] _ in
     let data = outHandle.availableData
                if data.count > 0 {
                    ...
                    }
                    outHandle.waitForDataInBackgroundAndNotify()
                }
  }.store(in: &subscriptons)

The closure for NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable) requires an async closure to update properties to compile and run.

// Observers
var notificationsfilehandle: NSObjectProtocol?
....
notificationsfilehandle =
    NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable,
                                                   object: nil, queue: nil)
        { _ in
            Task {
                await self.datahandle(pipe)
            }
        }


func datahandle(_ pipe: Pipe) async  { ... }

You can stream notifications in structured way, like:

for await notification in NotificationCenter.default.notifications(named: NSNotification.Name.NSFileHandleDataAvailable) {
     /// do stuff
}

Not to block it's better to wrap in a Task and store it, like:

// Observers
var notificationsfilehandle: Task<Void, Never>?
....

func subscribe() {
   notificationsfilehandle = Task { 
      for await notification in NotificationCenter.default.notifications(named: NSNotification.Name.NSFileHandleDataAvailable) {
         /// do stuff
      }
   }
}

/// and if you need to clear references
deinit {
   notificationsfilehandle?.cancel()
   notificationsfilehandle = nil
}

As per question, as far as I remember addObserver closure is marked as Sendable, think that's what causing an issue.

Thanks for your reply! I tried your suggestion, but Xcode warned that the closure is Sendable and calling a nonisolated function from an actor boundary isn't allowed. The observer works fine when asynchronous. I'll leave it as it is. Combine is the primary observer, and it works great. This observer was an alternative, but I'll stick with Combine.

Shouldn't the Task block have [weak self]? Otherwise deinit may not be called at all.

It’s hard to say without checking whole code, but yeah, combine should work.

Probably, haven’t touched memory management part. :slightly_smiling_face: