Почему UIAccessibility.post (уведомление: .announcement, аргумент: arg) не объявляется голосом?

При использовании Voice Over в iOS вызов UIAccessibility.post(notification:argument:) для объявления ошибки поля на самом деле не сообщает об ошибке.

У меня есть кнопка отправки, и при фокусировке на кнопке голос зачитывает заголовок кнопки, как и следовало ожидать. При нажатии кнопки голос зачитывает заголовок снова. Когда нажата кнопка отправки, я выполняю некоторую проверку и, когда есть ошибка поля, я пытаюсь объявить об этом, позвонив:

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

Интересно, что если я остановлюсь на точке останова в отладчике, произойдет объявление. Когда я не останавливаюсь на точке останова, объявления не происходит.

Уведомление публикуется в основном потоке, и, если оно похоже на NotificationCenter.default, я предполагаю, что оно обрабатывается в том же потоке, в котором оно было опубликовано. Я попытался отправить вызов в основную очередь, хотя он уже находится в основном потоке, и это, похоже, тоже не работает.

Единственное, что я могу думать, это то, что уведомление публикуется и просматривается до того, как закадровый голос закончит чтение заголовка кнопки отправки, и уведомление об объявлении не прерывает текущий голос.

Я был бы очень признателен за любую помощь в этом.


person brandenesmith    schedule 04.04.2019    source источник


Ответы (5)


Это, по общему признанию, хакерское решение, но я смог предотвратить то, что системное объявление превзошло мое собственное, отправив сообщение в основной поток с небольшой задержкой:

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  UIAccessibility.post(notification: .announcement, argument: "<Your text>")
}
person tedrothrock    schedule 07.11.2019
comment
Вопрос, лежащий в основе этого решения, заключается в подходящем значении задержки, которое должно быть адаптировано к скорости речи в соответствии со словами, которые нужно прочитать ???? ... и я не говорю о i18n, который добавляет еще один уровень проблем. ???? - person XLE_22; 25.03.2021

Проблема может возникнуть из-за того, что системе необходимо принять управление во время появления ошибки в поле, и в этом случае любое настроенное уведомление VoiceOver отменяется.

Я написал ответ о проблемах. с постановкой в ​​очередь нескольких уведомлений VoiceOver, которые могут помочь вам понять текущую ситуацию.

Ваше уведомление работает с точкой останова, потому что вы откладываете его, и система работает в это время: между вашим уведомлением и работой системы нет совпадения.

Простым решением может быть реализация небольшой задержки перед отправкой вашего уведомления, но задержка зависит от скорости речи, поэтому это временное решение. ????

Ваш механизм повторных попыток продуман и может быть улучшен в рамках цикла из нескольких повторных попыток в случае большого количества перехватов системы. ????

person XLE_22    schedule 07.04.2019

Я могу заставить это работать, используя механизм повтора, когда я регистрируюсь как наблюдатель 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!) всегда (если я не остановился на точке останова). Я до сих пор не знаю, почему первый пост всегда терпит неудачу.

person brandenesmith    schedule 13.12.2019

Другой способ - использовать вместо этого .screenChanged и передать метку ошибки, как:

UIAccessibility.post(notification: .screenChanged, argument: errorLabel)
person Amr    schedule 12.06.2020

Если кто-то использует 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()
            })
        }
    }
}
person YOG    schedule 24.03.2021