为啥 UIAccessibility.post(notification: .announcement, argument: "arg") 没有通过语音宣布?

Posted

技术标签:

【中文标题】为啥 UIAccessibility.post(notification: .announcement, argument: "arg") 没有通过语音宣布?【英文标题】:Why is UIAccessibility.post(notification: .announcement, argument: "arg") not announced in voice over?为什么 UIAccessibility.post(notification: .announcement, argument: "arg") 没有通过语音宣布? 【发布时间】:2019-04-04 18:01:22 【问题描述】:

ios 中使用 Voice Over 时,调用 UIAccessibility.post(notification:argument:) 来宣布字段错误实际上并不会宣布错误。

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

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

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

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

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

我非常感谢您对此的任何帮助。

【问题讨论】:

【参考方案1】:

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

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

【讨论】:

这个解决方案背后的问题是应该根据要读出的单词适应语速的延迟的适当值?...我不是说 i18n又增加了一层问题。 ?【参考方案2】:

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

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

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

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

您的重试机制很智能,并且可以在多次重试的循环内进行改进,以防多次系统接管。 ?

【讨论】:

【参考方案3】:

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

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

【讨论】:

这将使 VO 将焦点移至页面中的第一个元素。【参考方案4】:

我也遇到了同样的问题,所以我借鉴了@brandenesmith 关于通知队列的想法并编写了一个小助手类。

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)
        
    
    

【讨论】:

【参考方案5】:

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

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

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

代码如下:

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!) 总是(除非我在断点处停止)。我仍然不知道为什么第一个帖子总是失败。

【讨论】:

【参考方案6】:

如果有人使用 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()
            )
        
    

【讨论】:

以上是关于为啥 UIAccessibility.post(notification: .announcement, argument: "arg") 没有通过语音宣布?的主要内容,如果未能解决你的问题,请参考以下文章

为啥(n = 0)== 0?

为啥 iTextSharp 阅读页面 1..N 而不是 N?

为啥对于输出流,'\n' 优于 "\n"?

为啥插入 ListMap O(n)?

为啥选择排序的复杂度不是 n 阶乘?

为啥偶数 N 比奇数 N 花费更长的时间?