问题
I have a UISwitch
inside tableviewcell. The toggle makes a network request. A UIActivityIndicator
replaces the switch until the response completes.
So the UX flow is as such:
off --> loader --> on
Before the label is tapped the accessibility reads as such:
label - value - hint
‘streaming - switch button off - double tap to toggle switch’
My code is as such:
@IBAction func switchToggled(_ sender: Any) {
toggle.isHidden = true
activityIndicatorView.startAnimating()
UIAccessibility.post(notification: .layoutChanged, argument: activityIndicatorView)
I've also disabled accessibility of the cell itself.
The problem is the accessibility will read as such:
$switchValue - activityIndicatorLabel-activityIndicatorValue - switchLabel switchValue
off - streaming - in progress - streaming - switch button on
I’m not sure why accessibility still reads after I’ve set the toggle’s isHidden
to true
and also posted an Accessibility notification 🤷♂️
I want to omit off from above ☝️ and have it read as such:
$activityIndicatorLabel-activityIndicatorValue - switchLabel switchValue
streaming - in progress - streaming - switch button on
FWIW
UIAccessibility.post(notification: .screenChange, argument: activityIndicatorView)
Gets the desired sequence of voice, but then creates a new problem. That is I get the ‘boop beep’ sound and that would confuse a user into thinking they’re in a new screen.
It's quite odd that this notification moves to the correct item, while the other doesn't. I would have expected that both would create identical order but they don't
EDIT:
I've already looked into How to stop Text to Speech when Voiceover is speaking, or vice versa in Swift?
and I'm observing the UIAccessibilityElementFocusedNotification
notification. I get three items in the userInfo
.
▿ Optional<Dictionary<AnyHashable, Any>>
▿ some : 3 elements
▿ 0 : 2 elements
▿ key : AnyHashable("UIAccessibilityFocusedElementKey")
- value : "UIAccessibilityFocusedElementKey"
- value : <UIActivityIndicatorView: 0x113b59350; frame = (794 20; 24 24); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x2811a91e0>>
▿ 1 : 2 elements
▿ key : AnyHashable("UIAccessibilityAssistiveTechnologyKey")
- value : "UIAccessibilityAssistiveTechnologyKey"
- value : UIAccessibilityNotificationVoiceOverIdentifier
▿ 2 : 2 elements
▿ key : AnyHashable("UIAccessibilityUnfocusedElementKey")
- value : "UIAccessibilityUnfocusedElementKey"
- value : <MyModule.MySwitch: 0x113b59540; baseClass = UISwitch; frame = (769 16.5; 51 31); hidden = YES; opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x2811a9120>>
There are two things I don't know how to solve.
- The three events don't come in an orderly manner. It's just one single notification containing all three. I want to cancel the voiceOver for the switch is unfocused. I don't want that to be read.
- The answer still doesn't explain how to cancel the unwanted voiceOver.
回答1:
Unfortunately Voice Over has A LOT of bugs (at the beginning of its existence it was much better but with changes in subsequent iOS versions it got worse). Forcing it to behave differently is a lot of "hacking" and with new system it may again behave in ways you not wish it to. After saying that I will present solution that can be used at least for now.
At start I must say that pausing VoiceOver isn't possible programmatically because it causes crash (check here). As so
UIAccessibility.post(notification: .pauseAssistiveTechnology, argument: UIAccessibility.AssistiveTechnologyIdentifier.notificationVoiceOver)
won't help.
Best way to handle UITableViewCell with UISwitch is to do it in the same fashion as Apple did in iPhone settings (focus Voice Over on whole cell). So your switch should not be accessibility element.
And now the funniest thing to discover - if switch.isUserInteractionEnabled flag is set to true then double tap on cell won't call
tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath)
or
tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
so system will go in default way and will read new value of slider directly after it changes.
But if you will create method to set up newly dequeued cell (inside your custom cell class) and write there something like that
func setup() {
slider.isUserInteractionEnabled = !UIAccessibility.isVoiceOverRunning //if VO is running you can't normally tap slider so it won't affect user experience
NotificationCenter.default.removeObserver(self)
NotificationCenter.default.addObserver(self, selector: #selector(setSwitchAccessibility), name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil)
}
@objc func setSwitchAccessibility() {
slider.isUserInteractionEnabled = !UIAccessibility.isVoiceOverRunning //without VO user should be able to interact with switch directly or / and by selecting cell
}
then your table delegate will get called allowing to do something custom. Observing voice over is necessary to allow handling switch when Voice Over is turned off.
By "something custom" I understand showing activity indicator in willSelectRow and moving focus there by
UIAccessibility.post(notification: .layoutChanged, argument: activityIndicatorView)
and setting accessibility value to nil (set it again to slider accessibility value just before restoring Voice Over focus on cell).
Also - after removing accessibility value from cell Voice Over will read accessibility label of this cell if it exists. In this case you need to temporarily overwrite default accessibilityLabel by custom property of this cell (set to nil) to make it silent using something like that:
var labelToReturn: String? = ""
override func accessibilityActivate() -> Bool {
self.accessibilityValue = nil
labelToReturn = nil
return false
}
override var accessibilityLabel: String? {
get {return labelToReturn == nil ? labelToReturn: super.accessibilityLabel}
set {super.accessibilityLabel = newValue}
}
Remember to change labelToReturn
to something else before removing focus from your activity indicator.
来源:https://stackoverflow.com/questions/60008538/why-isnt-uiaccessibilitynotification-not-moving-to-correct-argument