为什么 UIAccessibility.post(notification: .announcement, argument: "arg") 没有在画外音中宣布?

Why is UIAccessibility.post(notification: .announcement, argument: "arg") not announced in voice over?

在 iOS 中使用 Voice Over 时,调用 UIAccessibility.post(notification:argument:) 宣布字段错误实际上并未宣布错误。

我有一个提交按钮,当聚焦该按钮时,旁白会如您所料朗读按钮标题。按下按钮时,画外音会再次朗读标题。当按下提交按钮时,我正在做一些验证,当出现字段错误时,我试图通过调用来宣布它:

if UIAccessibility.isVoiceOverRunning {
    UIAccessibility.post(notification: .announcement, argument: "my field error")
}

有趣的是,如果我在调试器中的断点处停止,就会发生通知。当我不在断点处停止时,不会发生通知。

通知在主线程上发布,如果像 NotificationCenter.default,我假设它是在发布它的同一个线程上处理的。我试图将调用分派到主 queue,即使它已经在主线程上,但似乎也不起作用。

我唯一能想到的是在画外音阅读完提交按钮标题之前发布并观察通知,并且公告通知不会打断当前画外音。

如果能提供任何帮助,我将不胜感激。

您的问题可能是因为系统需要在字段错误出现期间接管,在这种情况下,任何自定义的 VoiceOver 通知都会被取消。

我写了一篇 关于排队多个 VoiceOver 通知的问题,这可能有助于您了解您当前的情况。

您的通知与断点一起工作,因为您延迟了它并且系统在此期间工作:您的通知与系统工作之间没有重叠。

一个简单的解决方案可能是 在发送您的通知之前实现一个短暂的延迟,但延迟取决于语速,因此这只是一个临时解决方法。

您的重试机制很聪明,在多次系统接管的情况下可以在几次重试的循环中得到改进。

这是一个公认的 hacky 解决方案,但我能够通过稍微延迟分派到主线程来防止系统公告抢占我自己的公告:

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  UIAccessibility.post(notification: .announcement, argument: "<Your text>")
}

我可以使用重试机制让它工作,我注册为 UIAccessibility.announcementDidFinishNotification 的观察者,然后从 userInfo 字典中提取公告和成功状态。

如果成功状态为false,并且通知与我刚刚发送的通知相同,我会再次post通知。这种情况反复发生,直到公告成功。

这种方法显然存在多个问题,包括必须取消注册,如果另一个对象设法 post 相同的公告会发生什么(这在实践中不应该发生,但理论上它可以) ,必须跟踪最后发送的通知等。

代码如下所示:

private var _errors: [String] = []
private var _lastAnnouncement: String = ""

init() {
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(announcementFinished(_:)),
        name: UIAccessibility.announcementDidFinishNotification,
        object: nil
    )
}

func showErrors() {
    if !_errors.isEmpty {
        view.errorLabel.text = _errors.first!
        view.errorLabel.isHidden = false

        if UIAccessibility.isVoiceOverRunning {
            _lastAnnouncement = _errors.first!
            UIAccessibility.post(notification: .announcement, argument: _errors.first!)
        }
    } else {
        view.errorLabel.text = ""
        view.errorLabel.isHidden = true
    }
}

@objc func announcementFinished(_ sender: Notification) {
    guard let announcement = sender.userInfo![UIAccessibility.announcementStringValueUserInfoKey] as? String else { return }
    guard let success = sender.userInfo![UIAccessibility.announcementWasSuccessfulUserInfoKey] as? Bool else { return }

    if !success && announcement == _lastAnnouncement {
        _lastAnnouncement = _errors.first!
        UIAccessibility.post(notification: .announcement, argument: _errors.first!)
    }
}

问题是这个重试机制将始终被使用,因为第一次调用 UIAccessibility.post(notification: .announcement, argument: _errors.first!) 总是(除非我在断点处停止)。我仍然不知道为什么第一个 post 总是失败。

另一种解决方法是改用 .screenChanged 并传递错误标签,如:

UIAccessibility.post(notification: .screenChanged, argument: errorLabel)

如果有人使用 RxSwift,可能下面的解决方案会更合适:

extension UIAccessibility {
    static func announce(_ message: String) -> Completable {
        guard !message.isEmpty else { return .empty() }
        return Completable.create { subscriber in
            let postAnnouncement = {
                DispatchQueue.main.async {
                    UIAccessibility.post(notification: .announcement, argument: message)
                }
            }
            
            postAnnouncement()
            
            let observable = NotificationCenter.default.rx.notification(UIAccessibility.announcementDidFinishNotification)
            return observable.subscribe(onNext: { notification in
                guard let userInfo = notification.userInfo,
                      let announcement = userInfo[UIAccessibility.announcementStringValueUserInfoKey] as? String,
                      announcement == message,
                      let success = userInfo[UIAccessibility.announcementWasSuccessfulUserInfoKey] as? Bool else { return }
                success ? subscriber(.completed) : postAnnouncement()
            })
        }
    }
}

我遇到了同样的问题,所以我采用了@brandenesmith 通知队列的想法并写了一个小帮手 class。

class AccessibilityAnnouncementQueue {
    
    static let shard = AccessibilityAnnouncementQueue()
    
    private var queue: [String] = []

    private init() {
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(announcementFinished(_:)),
                                               name: UIAccessibility.announcementDidFinishNotification,
                                               object: nil)
    }
    
    func post(announcement: String) {
        guard UIAccessibility.isVoiceOverRunning else { return }
        
        queue.append(announcement)
        postNotification(announcement)
    }
    
    
    private func postNotification(_ message: String) {
        let attrMessage: NSAttributedString = NSAttributedString(string: message, attributes: [.accessibilitySpeechQueueAnnouncement: true])
        UIAccessibility.post(notification: .announcement, argument: attrMessage)
    }
    
    @objc private func announcementFinished(_ sender: Notification) {
        guard
            let userInfo = sender.userInfo,
            let firstQueueItem = queue.first,
            let announcement = userInfo[UIAccessibility.announcementStringValueUserInfoKey] as? String,
            let success = userInfo[UIAccessibility.announcementWasSuccessfulUserInfoKey] as? Bool,
            firstQueueItem == announcement
        else { return }
        
        if success {
            queue.removeFirst()
        } else {
            postNotification(firstQueueItem)
        }
    }
    
}