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?
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.