Enhancing the accessibility of your SwiftUI app
At a glance
| Item | Summary |
|---|---|
| Purpose | Show SwiftUI accessibility labels, actions, containment, rotors, and widget interactions. |
| Architecture | SwiftData-backed SwiftUI app plus a WidgetKit extension sharing model types and App Intents. |
| State strategy | SwiftData owns domain state; views own transient UI state; TripRouter.shared bridges composition requests into app navigation. |
| Boundary | Two executable targets (App and widget bundle) compile selected shared source; the archive has no separate service package. |
Project structure
Sources/
├── App.swift
├── Model/ # SwiftData models and TripRouter
├── Views/ # Trips, beaches, comments, alerts
├── Styles/ # Focused SwiftUI modifiers/styles
└── Widget/ # Widget entry, provider, intents, widget views
The Widget folder is a target boundary; Model and selected view/model code support both processes.
Overall architecture
flowchart LR
App["SwiftUI app target"] --> Content["ContentView / tabs"]
Content --> Trips["TripsView"]
Content --> Beaches["BeachesView"]
App --> Data["SwiftData ModelContainer"]
Widget["Widget extension"] --> Provider["TimelineProvider"]
Widget --> Data
Intent["ComposeIntent / ToggleRatingIntent"] --> Router["TripRouter or SwiftData"]
Trips --> Router
Reference code
Sources/App.swift:11 and Sources/Widget/WidgetBundle.swift:11 — the archive has two entry points (abridged).
@main
struct SwiftUIAccessibilitySampleApp: App {
var body: some Scene {
WindowGroup { ContentView() }
.modelContainer(for: [Trip.self, Beach.self, Contact.self])
}
}
@main
struct SwiftUIAccessibilitySampleWidgetBundle: WidgetBundle {
var body: some Widget { SwiftUIAccessibilitySampleWidget() }
}Ownership and state
classDiagram
SwiftUIAccessibilitySampleApp *-- ModelContainer
TripsView o-- TripRouter : shared bindable
TripRouter o-- Trip : selectedTrip
ComposeIntent --> TripRouter : requestComposition
WidgetView o-- Beach : SwiftData query
ToggleRatingIntent --> ModelContainer : opens process-local context
Ownership evidence
Sources/Model/TripRouter.swift:12 — the main-actor router owns only navigation/composition state.
@Observable @MainActor
final class TripRouter {
static let shared = TripRouter()
var selectedTrip: Trip?
private(set) var compositionRequested: ComposeType?
func requestComposition(for type: ComposeType) {
compositionRequested = type
}
}SwiftData is the durable mutation authority. TripsView observes the router and inserts the requested trip; the widget intent creates its own model container because it may execute outside the app process.
Class and protocol design
Most UI types are small View structs composed by feature. Models adopt framework protocols (Identifiable, Codable, AppEnum, AppIntent, TimelineProvider). The only central reference type is TripRouter, which is final, observable, and main-actor isolated. The private widget Provider adopts TimelineProvider at Sources/Widget/Widget.swift:82.
Access control
| Symbol | Access | Rationale |
|---|---|---|
compositionRequested |
private(set) |
Views can observe requests but only the router can transition them (Sources/Model/TripRouter.swift:17). |
| Nested detail/attachment view structs | private |
They are file-local composition details (Sources/Views/TripsView.swift:41). |
Widget Provider and Entry |
private |
WidgetKit implementation types are not target APIs. |
| Rating conversion extension | fileprivate |
Intent serialization helpers are used only in WidgetIntents.swift (Sources/Widget/WidgetIntents.swift:92). |
| Shared models/intents | internal default | They are shared through target membership, not exported as a public library. |
Logic ownership and placement
| Logic | Owner | Placement reason |
|---|---|---|
| Persistence schema | App and widget configurations | Both processes need the same SwiftData types. |
| Navigation/composition request | TripRouter |
App Intents can request UI work without owning a view. |
| Accessibility action/containment | Feature views | Semantics remain adjacent to their visual composition (Sources/Views/TripsView.swift:57). |
| Rating mutation from widget | ToggleRatingIntent |
The interaction executes in the widget/intent process (Sources/Widget/WidgetIntents.swift:63). |
Design patterns
| Pattern | Evidence | Tradeoff |
|---|---|---|
| Multi-target shared model | App and widget use the same SwiftData types | Consistent schema; each process still has its own runtime container. |
| Router | TripRouter.shared |
Simple cross-entry navigation bridge; global lifetime is explicit. |
| Declarative composition | Feature views and focused style types | Accessibility modifiers remain readable near content. |
| Intent adapter | App Intent parameters translate to router/model operations | Keeps system entry points thin. |
Naming conventions
- Screen roots use feature nouns plus
View(TripsView,BeachesView). - Framework roles use exact suffixes (
WidgetBundle,Provider,Intent,Entry). - Internal presentation components use descriptive role names such as
AttachmentOverlayView.
Architecture takeaways
- Treat app and widget as separate processes even when they share source and storage.
- Let App Intents translate requests; keep mutation in the router or data context that owns the state.
- Use narrow private SwiftUI components to keep accessibility modifiers close to their content.
- This archive shares source by target membership; it does not demonstrate a public modular package.
Source map
| Source | Role |
|---|---|
Sources/Views/ContentView.swift:10 |
App tab composition |
Sources/Views/TripsView.swift:14 |
Navigation and accessibility examples |
Sources/Widget/Widget.swift:13 |
Widget configuration |
Sources/Widget/WidgetIntents.swift:23 |
Composition bridge intent |
Sources/Model/Beach.swift:45 |
Shared SwiftData model state |