Accessibility design for Mac Catalyst
At a glance
| Item | Summary |
|---|---|
| Purpose | Adapt a UIKit coffee browser for keyboard, VoiceOver, semantic groups, focus groups, and Mac Catalyst conventions. |
| Architecture | Small UIKit MVC app with value-type view models and controller-owned screen state. |
| Main pattern | A cell delegate sends interaction intent to the list controller, which owns mutation and reapplies a diffable snapshot. |
| Boundary | One app target; there is no extension, service layer, or reusable framework target. |
Project structure
RoastedBeans/
├── Model/ # Coffee, Status, RBViewModel, sample data
├── RBListViewController.swift
├── RBCell.swift
├── RBDetailViewController.swift
├── RBStatusView.swift
└── AppDelegate.swift / SceneDelegate.swift
The folders separate data shapes from UIKit screens, but they do not create independent modules.
Overall architecture
flowchart LR
Scene["App / scene delegates"] --> List["RBListViewController"]
Sample["SampleData"] --> VM["RBViewModel values"]
VM --> List
List --> Cell["RBCell"]
List --> Detail["RBDetailViewController"]
Cell --> AX["Accessibility labels and custom actions"]
Detail --> AX2["Semantic and focus groups"]
Reference code
RoastedBeans/RBListViewController.swift:56 — the list maps its controller-owned models into cells (abridged).
datasource = UITableViewDiffableDataSource(
tableView: tableView,
cellProvider: { [self] table, index, _ in
let data = self.tableData[index.row]
cell.configure(for: data)
cell.delegate = self
return cell
}
)Ownership and state
classDiagram
RBListViewController *-- RBViewModel : tableData
RBListViewController *-- UITableViewDiffableDataSource
RBListViewController --> RBCell : configures
RBCell o-- RBCellActionProtocol : weak delegate
RBDetailViewController *-- RBViewModel : screen copy
Ownership evidence
RoastedBeans/RBCell.swift:9 and RoastedBeans/RBCell.swift:22 — the child reports actions without owning the controller.
protocol RBCellActionProtocol: AnyObject {
func didIncreaseRating(cell: RBCell)
func didToggleFavorite(cell: RBCell)
}
weak var delegate: RBCellActionProtocol?The list controller is the mutation authority for tableData; the detail controller receives and mutates its own value copy. There is no shared persistence layer.
Class and protocol design
RBListViewController and RBDetailViewController are final UIKit subclasses. RBViewModel is a Hashable struct suitable for diffable identity. The app-defined RBCellActionProtocol: AnyObject is a narrow protocol-oriented boundary for cell events; RBListViewController adopts it in an extension at RoastedBeans/RBListViewController.swift:96.
Access control
| Symbol | Access | Why it fits |
|---|---|---|
tableData, datasource |
private |
Only the list controller may mutate and publish its snapshot (RoastedBeans/RBListViewController.swift:11). |
| Cell outlets and formatting helpers | private |
Storyboard wiring and presentation details stay inside RBCell (RoastedBeans/RBCell.swift:24). |
delegate |
internal, weak | The sibling controller must assign it; weak ownership avoids a cell/controller cycle. |
| App types | internal by default | A single app target has no public API surface. |
No public or fileprivate API is needed. private is used for implementation details; default internal access joins files within the target.
Logic ownership and placement
| Logic | Owner | Placement reason |
|---|---|---|
| Rating/favorite mutation and snapshot refresh | RBListViewController |
It owns collection state and table coordination. |
| Cell accessibility value/actions | RBCell |
They derive directly from cell presentation (RoastedBeans/RBCell.swift:147). |
| Detail semantic groups and focus identifiers | RBDetailViewController |
They describe that screen’s custom hierarchy (RoastedBeans/RBDetailViewController.swift:60). |
| Sample construction | SampleData |
Static fixture generation stays next to model types (RoastedBeans/Model/RBViewModel.swift:105). |
Design patterns
| Pattern | Evidence | Tradeoff |
|---|---|---|
| Delegate | Cell action protocol and weak reference | Keeps reusable cell interaction-free, with some controller coupling through RBCell. |
| Diffable data source | Snapshot reapplied after mutation | Stable identity and simple UI refresh; the controller remains state-heavy. |
| View model value | RBViewModel combines domain and display state |
Convenient for a sample, but detail changes do not flow back automatically. |
| Controller coordinator | List creates and shows detail | Appropriate for two screens; no separate router abstraction. |
Naming conventions
- The
RBprefix scopes app-specific UIKit types (RBCell,RBStatusView,RBViewModel). - Event methods start with
did...; configuration methods start withconfigure...orsetup.... - Model nouns are singular; collections use plural names such as
coffees,statuses, andlocationsAvailable.
Architecture takeaways
- Put mutation in the controller that owns the diffable snapshot.
- Use a weak, class-bound delegate for reusable child-view actions.
- Keep accessibility semantics next to the custom presentation they describe.
- This is a compact single-target teaching app, not a reusable Catalyst architecture package.
Source map
| Source | Role |
|---|---|
RoastedBeans/Model/RBViewModel.swift:10 |
Value view model and identity |
RoastedBeans/RBListViewController.swift:136 |
Favorite mutation and snapshot |
RoastedBeans/RBCell.swift:187 |
Accessibility custom actions |
RoastedBeans/RBDetailViewController.swift:141 |
Custom navigation semantic group |
RoastedBeans/AppDelegate.swift:9 |
App entry point |