an0
an0

Reputation: 17530

UIAccessibilityElement focusing issue within a long view inside a UITableViewCell

I have a UITableViewCell containing a view which has some child accessibility elements. The view is quite long, longer that a single screen.

When user flicks left and right to navigate through the child elements, the selected element is out of screen sometimes. When he double taps later he cannot trigger the action for such an element.

Why does it not work? How to make it work correctly?

Why is the selected element out of screen sometimes? How to make it in screen?

Here is the sample code.

  1. When double tap works correctly you will see a log in Xcode console.
  2. It is an over simplified sample. I cannot use separate cells in the real app. Think of it as a rich text view with links, and users use left and right flick to select links.

Upvotes: 1

Views: 460

Answers (1)

XLE_22
XLE_22

Reputation: 5671

Why does it not work? How to make it work correctly?

With VoiceOver OFF ❌, the tap function prints the element once you touch it AND if it's visible because a tap gesture only happens on screen.

With VoiceOver ON ✅, you should think the same way even if the screen reader is able to read out the previous labels that are out of screen: when you double-tap with one finger for an element out of screen, the gesture can't be handled with a GestureRecognizer.
A solution could be the use of the accessibilityActivate method that's triggered when a double-tap occurs on a a11y element.

I suggest to create a label class in which you implement this function:

class myLabel:UILabel {

    init(frame: CGRect, index: Int) {
        super.init(frame: frame)

        text = "\(index)"
        isAccessibilityElement = true
        isUserInteractionEnabled = true

        addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func accessibilityActivate() -> Bool {
        print(text!)
        return true
    }

    @objc func tap(sender: UITapGestureRecognizer) {

        guard sender.state == .recognized else { return }
        print(sender.view!)
    }
}

The LongView class has the following code to define its behavior:

class LongView: UIView {

    override var isAccessibilityElement: Bool {
        get { return false }
        set {   }
    }

    override var accessibilityElementsHidden: Bool {
        get { return false }
        set {   }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        let screenWidth = UIScreen.main.bounds.width
        var x: CGFloat = 20
        var y: CGFloat = 20
        let width: CGFloat = 200
        let height: CGFloat = 40
        let spacing: CGFloat = 20

        for index in 0..<20 {

            let label = myLabel(frame: CGRect(x: x, y: y, width: width, height: height),
                                index: index)
            addSubview(label)

            x += width + spacing
            if x + width > screenWidth {
                y += height + spacing
                x = spacing
            }
        }
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

    override var intrinsicContentSize: CGSize {

        CGSize(width: UIScreen.main.bounds.width,
               height: subviews.map { $0.frame.maxY }.max()! + 20)
    }
}

And the table view cell class is as follows:

class Cell: UITableViewCell {

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        let longView = LongView()

        longView.accessibilityElementsHidden = false
        contentView.addSubview(longView)

        longView.translatesAutoresizingMaskIntoConstraints = false
        longView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
        longView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
        longView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
        longView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}

Now, you can notice that:

  • The GestureRecognizer function will be triggered when VoiceOver is OFF.
  • The one finger double-tap gesture will be handled when VoiceOver is ON on every single label that is present on screen or not. 👍

I'm not sure that all the added lines are necessary to reach your goal but this is the rationale to solve your UIAccessibilityElement focusing issue within a long view inside a UITableViewCell. 🥳🎉🎊

Upvotes: 1

Related Questions