Mac监听VoiceOver状态变更:Swift 6迁移问题排查
解决Swift 6中NSWorkspace KVO监听VoiceOver状态的Sendable兼容问题
问题背景
通过NSWorkspace的KVO监听VoiceOver状态变化的代码如下:
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 } } }
迁移至Swift 6时遇到两个核心问题:
- 编译器报错:在@Sendable闭包中捕获了非Sendable类型的'self',提示类未遵循
Sendable协议。 - 尝试让类遵循
Sendable时,因存在weak var属性受阻;尝试将类隔离至MainActor时,又报错:在同步非隔离上下文中调用了MainActor隔离的实例方法'updateVoiceOverState()'。
解决方案
方案一:让类遵循Sendable协议
weak var本身不影响Sendable合规性,问题出在actionHandler和dataProvider这两个弱引用的类型未遵循Sendable。按以下步骤修改:
- 让关联协议遵循
Sendable:
protocol ActionHandler: AnyObject, Sendable {} protocol DataProvider: AnyObject, Sendable {}
- 调整
MyContext类,确保所有属性Sendable兼容,同时处理KVO回调的线程问题:
public final class MyContext: ObservableObject, Sendable { public weak var actionHandler: ActionHandler? public weak var dataProvider: DataProvider? @Published public private(set) var isVoiceoverEnabled: Bool 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 guard let self else { return } // KVO回调可能在任意线程触发,需切换到MainActor修改@Published属性 Task { @MainActor in self.updateVoiceOverState() } } } private func updateVoiceOverState() { if isVoiceoverEnabled != NSWorkspace.shared.isVoiceOverEnabled { isVoiceoverEnabled = NSWorkspace.shared.isVoiceOverEnabled } } }
方案二:用MainActor隔离类
如果选择通过MainActor隔离类,需确保KVO回调在MainActor上下文调用方法,同时关联协议仍需遵循Sendable:
- 先让协议遵循
Sendable(同方案一第一步)。 - 调整
MyContext类:
@MainActor public final class MyContext: ObservableObject { public weak var actionHandler: ActionHandler? public weak var dataProvider: DataProvider? @Published public private(set) var isVoiceoverEnabled: Bool 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 guard let self else { return } // 显式切换到MainActor上下文调用隔离方法 Task { @MainActor in self.updateVoiceOverState() } } } private func updateVoiceOverState() { if isVoiceoverEnabled != NSWorkspace.shared.isVoiceOverEnabled { isVoiceoverEnabled = NSWorkspace.shared.isVoiceOverEnabled } } }
关键知识点
- Swift 6中KVO的闭包默认是
@Sendable,捕获的对象必须是Sendable类型或在指定Actor隔离。 @Published属性的修改必须在MainActor线程(ObservableObject的默认行为),而KVO回调可能在后台线程触发,因此必须显式切换线程。- 弱引用的类型只要遵循
Sendable,就不会影响宿主类的Sendable合规性。
内容的提问来源于stack exchange,提问作者rottweiler
相关产品推荐
相关产品推荐

