1

I need to know which element is currently become focused by accessibility focus engine. In a simple case, let' say I have come cell with a couple of label. I need to know (i.e. print on console, or doing other stuff) which object is currently focused. Them same if I had a view with a lot of subviews, of different type. I think I should use elementFocusedNotification, and as told in docs, use the key UIAccessibilityElementFocusedKeyElement. I think I should pass the object in the method, but how?.

among others, I looked here, here and here for info, but cannot find solutions to know which element is currently focused.

in my cellForRowAt indexPath:

in the cell:

override func accessibilityElementDidBecomeFocused() {
        NotificationCenter.default.post(
            name: NSNotification.Name(rawValue: UIAccessibility.elementFocusedNotification.rawValue),
            object: self.subviews.first(where: {$0.isFocused}),
            userInfo: ["UIAccessibilityElementFocusedKeyElement": "hello there"]
        )
    }

in the controller's viewDidLoad:

    NotificationCenter.default.addObserver(
        self,
        selector: #selector(self.doSomething(_:)),
        name: NSNotification.Name(rawValue: UIAccessibility.elementFocusedNotification.rawValue),
        object: nil
    )

in the same controller

@objc func catchNotification(_ notification: Notification) {
    //print("subscribed, notification: \(notification)")

    if let myNotification1 = notification.userInfo?["UIAccessibilityElementFocusedKeyElement"] {
        print("+++", myNotification1)
    }

   }    
mfaani
  • 33,269
  • 19
  • 164
  • 293
biggreentree
  • 1,633
  • 3
  • 20
  • 35
  • What's in your 'in my cellForRowAt indexPath:' ? You forgot to write that down – mfaani Mar 07 '20 at 21:14
  • I _think_ you can observe the changes of focused items. Try observing `UIAccessibilityVoiceOverStatusDidChangeNotification` and then see what's inside its `userInfo` – mfaani Mar 07 '20 at 21:28

2 Answers2

1

First of all cell will never be focused.

  1. VoiceOver focus single accessibility element and do not inform it's superviews.

So if you want to use accessibilityElementDidBecomeFocused() you will have to override UILabel

  1. UIAccessibility.elementFocusedNotification is send by system, you probably should never post it by yourself

  2. Going back to your problem. I suppose that you have following situation:

ViewController
  TableView
    Cell 1
      Label 1_1
      Label 1_2
      Label 1_3
      View 1_1
    Cell 2
      Label 2_1
      Label 2_2
      Label 2_3
      View 2_1
    Cell 3
      ...

and whenever VO focus Label_i_* or View_i_* you want to inform yours controller that i-th element was focused.

My approach will be as follow:

  • every accessibility element should know which element is displaying
  • view controller catches every notification, checks if it was send by TableView's subview and if so then it get element from it

The code will be as follow:

class MyViewController ... {
    private var VOFocusChanged: NSObjectProtocol?

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        VOFocusChanged = NotificationCenter.default.addObserver(
            forName: UIAccessibility.elementFocusedNotification,
            object: nil,
            queue: OperationQueue.main
        ) {
            [weak self] (notification: Notification) in
            self?.handleVONotification(notification)
        }
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        
        // there is no reason to observe changes 
        // if we know that view is no longer visible
        if let VOFocusChanged: NSObjectProtocol = VOFocusChanged {
            NotificationCenter.default.removeObserver(VOFocusChanged)
        }
    }
   
    private func handleVONotification(_ notification: Notification) {
        if let focusedView: UIView = notification.userInfo?[
                UIAccessibility.focusedElementUserInfoKey
            ] as? UIView {

            if focusedView.isGrandson(of: view) {
                if let fView: ViewWithElement = focusedView as? ViewWithElement {
                    elementsWasFocused(fView.element)
                }
            }
        }

    }
    
    private func elementsWasFocused(_ element: T) {
        // add your logic here
    }
 
}

extension UIView {
    func isGrandson(of view: UIView) -> Bool {
        if superview === view {
            return true
        }
        return superview?.isGrandson(of: view) ?? false
    }
}


protocol ViewWithElement {
    var element: T { get }
}

// Use this instead of label inside UITableViewCell 
class MyLabel: UILabel, ViewWithElement {

    func display(element: T) {
        _element = element
        text = ...
        accessibilityLabel = ...
    }
    // MARK: ViewWithElement
    private var _element: T!
    
    var element: T { return _element }
}

Pikacz
  • 431
  • 1
  • 5
  • 10
0

This isn't a direct answer to your question, nor I'm certain if it all works as I say. Because Apple hides a lot of details of how the accessibility engine works, but still may help solve your problem from a completely different angle and the sense that you don't need to know what's the current focus.

Take a look at a few of Apple's own apps. e.g. the Settings App. Only the entire cell is accessible. The labels are not.

The solution is to either:

  • based on your labels override the accessibilityLabel of the cell and then set isAccessibilityElement to false for your labels. For more on that see Making Your iOS App Accessible. Scroll down to 'Enhance the Accessibility of Table Views'
  • just manually set the accessibilityLabel when necessary and then set isAccessibilityElement to false for your labels. Apparently this solution would require you to reset it if the label gets updated.
  • if you set a view or text for your accessoryView then I believe UITableViewCell will take its value into consideration as well. For more on that see here

Note: A tableviewcell's accessibilityLabel will default to its labels's text.

mfaani
  • 33,269
  • 19
  • 164
  • 293