You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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时遇到两个核心问题:

  1. 编译器报错:在@Sendable闭包中捕获了非Sendable类型的'self',提示类未遵循Sendable协议。
  2. 尝试让类遵循Sendable时,因存在weak var属性受阻;尝试将类隔离至MainActor时,又报错:在同步非隔离上下文中调用了MainActor隔离的实例方法'updateVoiceOverState()'。

解决方案

方案一:让类遵循Sendable协议

weak var本身不影响Sendable合规性,问题出在actionHandler和dataProvider这两个弱引用的类型未遵循Sendable。按以下步骤修改:

  1. 让关联协议遵循Sendable:
protocol ActionHandler: AnyObject, Sendable {}
protocol DataProvider: AnyObject, Sendable {}
  1. 调整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:

  1. 先让协议遵循Sendable(同方案一第一步)。
  2. 调整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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 22:55:59