Skip to content

Instantly share code, notes, and snippets.

@siracusa
Last active June 17, 2024 16:47
Show Gist options
  • Save siracusa/137e3fae8a384ac4bda8c56524826d6b to your computer and use it in GitHub Desktop.
Save siracusa/137e3fae8a384ac4bda8c56524826d6b to your computer and use it in GitHub Desktop.
Swift Concurrency Candidate
NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.didLaunchApplicationNotification,
object: nil, queue: nil, using: { [weak self] notification in
self?.doStuff()
})
@danielpunkass
Copy link

Glad to know I’m not missing something obvious, but yeah this is an unfortunate limitation. I wonder if some kind of nasty Swift bitcasting, or an ObjC helper method might be a workaround. This pattern is common enough in my code base that eliminating the clutter of all those “yes this is the main actor” assumeIsolated calls is very attractive.

@danielpunkass
Copy link

I think I got something working. I implemented an ObjC helper:

- (id <NSObject>)_addMainQueueObserverForName:(nullable NSNotificationName)name object:(nullable id)obj usingBlock:(void (NS_SWIFT_UI_ACTOR ^)(NSNotification *notification))block
{
	return [self addObserverForName:name object:obj queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull notification) {
		block(notification);
	}];
}

But for whatever reason the NS_SWIFT_UI_ACTOR wasn't coming through to the Swift side? I'll try to figure out a fix for that. But in the meantime, wrapping the ObjC helper above with a Swift-facing call that just calls through to it:

@MainActor @discardableResult
public func addMainActorObserver(forName name: Notification.Name, object obj: Any? = nil, using block: @escaping @MainActor (Notification) -> Void) -> any NSObjectProtocol {
	return self._addMainQueueObserver(forName: name, object: obj, using: block)
}

Seems to work!

@mattmassicotte
Copy link

Oh well actually yes, you can achieve something similar in Swift:

extension NotificationCenter {
    @MainActor
    public func addMainActorObserver(
        forName name: Notification.Name,
        object obj: Any? = nil,
        block: @escaping @MainActor (Notification) -> Void
    ) -> any NSObjectProtocol {
        // this let's you lie to the compiler!
        let sendableBlock = unsafeBitCast(block, to: (@Sendable (Notification) -> Void).self)

        return self.addObserver(forName: name, object: obj, queue: .main, using: sendableBlock)
    }
}

I'm not sure exactly what's up with the NS_SWIFT_UI_ACTOR. I haven't looked into the clang annotations deeply but they are definitely supposed to work. Please file a bug if something isn't working as it should!

@danielpunkass
Copy link

Perfect, thanks for the bitcast hack! And I'll look into the NS_SWIFT_UI_ACTOR issue and file a bug if it's busted.

@danielpunkass
Copy link

Also for anybody reading along: I think I decided I don't want/need the addMainActorObserver to be @MainActor. It's likely I'll only ever call it from the main thread, but because the method guarantees delivery on the main thread, it can be safely called from anywhere, I think.

@mattmassicotte
Copy link

I think this is correct! But, I also do want to point out that this version makes it possible to pass data unsafely across threads by way of the Notification object. I don't think that's likely though.

@danielpunkass
Copy link

Oh, right. That's a good point. Maybe I should leave it @MainActor after all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment