swiftswift-concurrencyswift6

Observing voiceover changes on Mac - Swift 6 migration issues


I am listening to voiceover changes through NSWorkspace KVO and here is my code

public final class MyContext: ObservableObject {
    public weak var actionHandler: ActionHandler?
    public weak var dataProvider: DataProvider?

    @Published public private(set) var isVoiceoverEnabled: Bool
    @objc private var voiceOverObserver: NSKeyValueObservation?

    public init(
        actionHandler: ActionHandler,
        dataProvider: DataProvider
    ) {
        self.actionHandler = actionHandler
        self.dataProvider = dataProvider
        self.isVoiceoverEnabled = NSWorkspace.shared.isVoiceOverEnabled
        self.voiceOverObserver = NSWorkspace.shared.observe(\.isVoiceOverEnabled) { [weak self] _, _ in
            self?.updateVoiceOverState()
        }
    }

    @objc
    private func updateVoiceOverState() {
        if isVoiceoverEnabled != NSWorkspace.shared.isVoiceOverEnabled {
            isVoiceoverEnabled = NSWorkspace.shared.isVoiceOverEnabled
        }
    }
}

I am in the process of migrating the app to Swift 6 and I am having some trouble fixing the issues in this class.

The compiler complains that the class is not Sendable Capture of 'self' with non-sendable type 'MyContext?' in a @Sendable closure

Swift 6 concurrency requires that everyhing needs to be isolated to an actor or sendable across different contexts (please correct me if I am wrong) So, this error is telling me that the callback block can be called from any context and I either have to isolate it to MainActor or make the class Sendable.

I tried to make the class Sendable but ran into issues since I have weak vars defined.

When I try to isolate the class to MainActor I run into a different issue Call to main actor-isolated instance method 'updateVoiceOverState()' in a synchronous nonisolated context

What am I missing here? How do I fix this?

I am trying to learn Swift concurrency in this process and any help here would be very much appreciated!


Solution

  • Making the whole class bounds to any global actors is an approach, for example @MainActor:

    @MainActor
    public final class MyContext: ObservableObject {
        public init(...) {
            ...
            self.voiceOverObserver = NSWorkspace.shared.observe(\.isVoiceOverEnabled) { [weak self] _, _ in
                Task { @MainActor in
                    self?.updateVoiceOverState()
                }
            }
        }
    }