Search code examples
swifttvosuiaccessibility

tvOS Landmarks with Supplementary Views in a collection view Accessibility


I am trying to copy the flow for Landmark Accessibility from Apple's Movie app. I have tried using a table view with a custom header and a standard header view with my cells having a collection view inside them and a collection view with a supplementary view with another collection view inside the collection view cells.

I added the header views title as an accessibility element when creating them to try and adhere to UIAccessibilityContainer : https://developer.apple.com/documentation/uikit/accessibility/uiaccessibilitycontainer. Which should allow me to conform to the .landmark protocol via the public enum UIAccessibilityContainerType.

Both have failed to allow landmarks to travel from the title of one supplementary view or header to the next supplementary view or header. I originally thought that maybe it was a bug with the protocol for Landmarks in Accessibility, but I notice other apps are also using the Landmark navigation correctly.

Code CollectionView with supplementary view:

struct Content {
let name: String
let color: UIColor

 init(name: String, color: UIColor) {
    self.name = name
    self.color = color
 }
}
class CollecitonViewWithCollectionView: UIViewController {
 let testContent = [Content(name: "AA", color: .red), Content(name: "BB", color: .green), Content(name: "CC", color: .yellow)]

@IBOutlet weak var collectionView: UICollectionView!

override func viewDidLoad() {
    super.viewDidLoad()

    collectionView.accessibilityContainerType = .landmark
}
}

extension CollecitonViewWithCollectionView: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 1
}

func numberOfSections(in collectionView: UICollectionView) -> Int {
    return 10
}

func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
    return false
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as? CollectionViewCell else {
        return UICollectionViewCell()
    }
    cell.content = testContent
    return cell
}

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    guard let headerView = collectionView.dequeueReusableSupplementaryView(
        ofKind: UICollectionElementKindSectionHeader,
        withReuseIdentifier: "HeaderView",
        for: indexPath
        ) as? HeaderView

        else {
            return UICollectionReusableView()
    }

    return headerView
}


}

extension CollecitonViewWithCollectionView: UICollectionViewDelegate {

}

class InnerCollectionTestCell: UICollectionViewCell {
@IBOutlet weak var testLabel: UILabel!

}

class CollectionViewCell: UICollectionViewCell, UICollectionViewDelegate, UICollectionViewDataSource {

@IBOutlet weak var collectionView: UICollectionView!

var content: [Content]?

override func awakeFromNib() {
    super.awakeFromNib()
    collectionView.dataSource = self
    collectionView.delegate = self
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return content?.count ?? 0
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "InnerCollectionTestCell", for: indexPath) as? InnerCollectionTestCell else {
        return UICollectionViewCell()
    }
    cell.testLabel.text = content?[indexPath.row].name ?? "Failed"
    cell.backgroundColor = content?[indexPath.row].color ?? .black
    return cell
}

}


class HeaderView: UICollectionReusableView {
@IBOutlet weak var titleLabel: UILabel!
}

Code TableView with headerView or custom header view:

 class TestCollectionCell: UICollectionViewCell {
    @IBOutlet weak var contentStringLabel: UILabel!
}


class TestCell: UITableViewCell, UICollectionViewDataSource, UICollectionViewDelegate {
@IBOutlet weak var collectionView: UICollectionView!

var content: [Content]?

override func awakeFromNib() {
    super.awakeFromNib()
    collectionView.dataSource = self
    collectionView.delegate = self
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return content?.count ?? 0
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TestCollectionCell", for: indexPath) as? TestCollectionCell else {
        return UICollectionViewCell()
    }
    cell.backgroundColor = content?[indexPath.row].color ?? .black
    cell.contentStringLabel.text = content?[indexPath.row].name ?? "Zzz"
    return cell
}
}



class TableViewWithCollectionView: UIViewController {
 @IBOutlet weak var tableView: UITableView!

 let testContent = [Content(name: "A", color: .red), Content(name: "B", color: .green), Content(name: "C", color: .yellow)]

override func viewDidLoad() {
    super.viewDidLoad()

    let headerNib = UINib(nibName: "TableViewHeaderFooterView", bundle: nil)
     tableView.register(headerNib, forHeaderFooterViewReuseIdentifier:  "TableViewHeaderFooterView")

}

}

extension TableViewWithCollectionView: UITableViewDelegate {

}

extension TableViewWithCollectionView: UITableViewDataSource {

func numberOfSections(in tableView: UITableView) -> Int {
    return 10
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 1
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as? TestCell else {
        return UITableViewCell()
    }
    cell.content = testContent
    return cell
}

func tableView(_ tableView: UITableView, canFocusRowAt indexPath: IndexPath) -> Bool {
    return false
}

   /*func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
tableView.headerView(forSection: section)?.accessibilityTraits |= UInt64(UIAccessibilityContainerType.landmark.rawValue)

    return "Test Without custom header"
    }*/

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "TableViewHeaderFooterView") as? TableHeaderFooterView
    headerView?.tableHeaderTitleLabel.text = "TEST with custom header"
    headerView?.accessibilityTraits |= UInt64(UIAccessibilityContainerType.landmark.rawValue)
    return headerView
}

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return 50
}

}

I know that for headings you need to either set the trait in IB or do:

//set here because Xcode is not giving me the option in IB
accessibilityTraits |= UIAccessibilityTraitHeader

I would assume there is some way like this for Landmarks?


Solution

  • override var accessibilityLabel: String? {
        get { return titleLabel.accessibilityLabel }
        set {}
    }
    
    override var accessibilityTraits: UIAccessibilityTraits {
        get { return UIAccessibilityTraits.header }
        set {}
    }
    
    override var accessibilityContainerType: UIAccessibilityContainerType {
        get { return UIAccessibilityContainerType.landmark }
        set {}
    }
    

    So the issue is that these values are being set inside of the reusable header, apparently this causes issues with the accessibility elements being set correctly and instead they need to be set on the views themself. Add the above code to your HeaderView files.