An accessible app works for everyone — including VoiceOver and Dynamic Type users. Learn accessibility labels, supporting scalable text, and testing with the Accessibility Inspector.
Accessibility makes your app usable by people with visual, motor, or other needs — and it is both the right thing to do and often a legal requirement. The primary tool is VoiceOver, the screen reader that speaks the interface; it relies on each element having a meaningful accessibility label. SwiftUI gives standard controls good defaults, but custom views and icon-only buttons need labels you provide.
import SwiftUI
struct FavoriteButton: View {
@Binding var isFavorite: Bool
var body: some View {
Button { isFavorite.toggle() } label: {
Image(systemName: isFavorite ? "heart.fill" : "heart")
}
// an icon-only button is silent to VoiceOver without a label:
.accessibilityLabel(isFavorite ? "Remove from favorites" : "Add to favorites")
.accessibilityAddTraits(.isButton)
}
}
// standard SwiftUI controls are labeled automatically; custom/icon views need
// an explicit accessibilityLabel so VoiceOver can announce them.Dynamic Type lets users choose their preferred text size system-wide, and your app should honor it so text stays readable. Using the built-in text styles (.body, .headline) instead of fixed point sizes makes text scale automatically. Supporting Dynamic Type — and not truncating or clipping at large sizes — is one of the highest-impact accessibility wins.
struct ArticleView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Headline").font(.headline) // scales with Dynamic Type
Text("Body text that grows with the user's chosen size.")
.font(.body) // NOT .system(size: 14) — that won't scale
}
.padding()
// test at large sizes: text should reflow, not truncate or clip.
}
}
// prefer semantic text styles (.body/.title) over fixed sizes so text scales
// with the user's setting. Verify layouts survive the largest sizes.Beyond labels, accessibility traits tell assistive tech what an element is (a button, a header, selected), and grouping combines related elements so VoiceOver reads them as one unit instead of piecemeal. Well-chosen traits and grouping turn a technically-labeled screen into one that is actually pleasant to navigate non-visually.
struct ProductRow: View {
let name: String
let price: String
var body: some View {
HStack {
Text(name)
Spacer()
Text(price)
}
// group so VoiceOver reads "Book, $12" as ONE element, not two:
.accessibilityElement(children: .combine)
.accessibilityHint("Double tap to view details")
}
}
// traits (.isButton, .isHeader, .isSelected) convey ROLE; grouping conveys
// STRUCTURE. Together they make navigation coherent, not just labeled.Accessibility must be verified, not assumed. Turn on VoiceOver and navigate your app with your eyes closed; enable the largest Dynamic Type size and check for clipping; and use Xcode’s Accessibility Inspector to audit elements and catch missing labels or low contrast. Building this check into your process — like the web Accessibility course teaches for the browser — is what keeps an app usable for everyone.
How to actually test it:
1. VoiceOver: Settings > Accessibility > VoiceOver — navigate with the screen
off. Can you reach + understand everything? Are labels meaningful?
2. Dynamic Type: set the largest text size — does anything clip or truncate?
3. Accessibility Inspector (Xcode > Open Developer Tool): audits a running app
for missing labels, small hit targets, low contrast.
4. contrast + color: don't rely on color alone; meet contrast ratios.
Same discipline as the web Accessibility course, applied to iOS. Build the
audit into your workflow, not a one-off at the end.