Loading...
Loading...
Implement, review, or improve accessibility in iOS/macOS apps with SwiftUI and UIKit. Use when adding VoiceOver support with accessibility labels, hints, values, and traits; when grouping or reordering accessibility elements; when managing focus with @AccessibilityFocusState; when supporting Dynamic Type with @ScaledMetric; when building custom rotors or accessibility actions; when auditing a11y compliance; or when adapting UI for assistive technologies and system accessibility preferences.
npx skill4agent add dpearson2699/swift-ios-skills ios-accessibility.accessibilityLabel.accessibilityAddTraits@ScaledMetric// Label: the primary description VoiceOver reads
Button(action: { }) {
Image(systemName: "heart.fill")
}
.accessibilityLabel("Favorite")
// Hint: describes the result of activation (read after a pause)
Button("Submit")
.accessibilityHint("Submits the form and sends your feedback")
// Value: the current state for sliders, toggles, progress indicators
Slider(value: $volume, in: 0...100)
.accessibilityValue("\(Int(volume)) percent")accessibilityInputLabelsButton("Go") { }
.accessibilityInputLabels(["Go", "Start", "Begin"]).accessibilityAddTraits.accessibilityRemoveTraits| Trait | Use For |
|---|---|
| Custom tappable views that are not |
| Section headers (enables rotor heading navigation) |
| Elements that navigate to external content |
| Currently selected tab, segment, or radio button |
| Meaningful images |
| Custom toggle controls |
| Trap VoiceOver focus inside a custom overlay |
| Timers, live counters, real-time displays |
| Custom search inputs |
| Elements that begin audio/video playback |
// WRONG: overwrites Button's built-in .isButton trait
Button("Go") { }
.accessibilityTraits(.updatesFrequently)
// CORRECT: adds to existing traits
Button("Go") { }
.accessibilityAddTraits(.updatesFrequently)
// Remove a trait when needed
Text("Not really a header anymore")
.accessibilityRemoveTraits(.isHeader)// .combine: merge children into one VoiceOver element
// VoiceOver concatenates child labels automatically
HStack {
Image(systemName: "person.circle")
VStack {
Text("John Doe")
Text("Engineer")
}
}
.accessibilityElement(children: .combine)
// .ignore: replace children with a completely custom label
HStack {
Image(systemName: "envelope")
Text("inbox@example.com")
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Email: inbox@example.com")
// .contain: keep children individually navigable but logically grouped
VStack {
Text("Order #1234")
Button("Track") { }
}
.accessibilityElement(children: .contain).accessibilityElement(children: .combine).accessibilityRepresentationHStack {
Text("Dark Mode")
Circle()
.fill(isDark ? .green : .gray)
.onTapGesture { isDark.toggle() }
}
.accessibilityRepresentation {
Toggle("Dark Mode", isOn: $isDark)
}// Custom slider control
VStack {
SliderTrack(value: value) // Custom visual implementation
}
.accessibilityRepresentation {
Slider(value: $value, in: 0...100) {
Text("Volume")
}
}HStack { /* custom star rating UI */ }
.accessibilityElement()
.accessibilityLabel("Rating")
.accessibilityValue("\(rating) out of 5 stars")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment: if rating < 5 { rating += 1 }
case .decrement: if rating > 1 { rating -= 1 }
@unknown default: break
}
}.accessibilityAdjustableAction.adjustableMessageRow(message: message)
.accessibilityAction(named: "Reply") { reply(to: message) }
.accessibilityAction(named: "Delete") { delete(message) }
.accessibilityAction(named: "Flag") { flag(message) }PlayerView()
.accessibilityAction(.magicTap) { togglePlayPause() }
.accessibilityAction(.escape) { dismiss() }ZStack {
Image("photo").accessibilitySortPriority(0) // Read third
Text("Credit").accessibilitySortPriority(1) // Read second
Text("Breaking News").accessibilitySortPriority(2) // Read first
}@AccessibilityFocusStateBoolHashablestruct ContentView: View {
@State private var showSheet = false
@AccessibilityFocusState private var focusOnTrigger: Bool
var body: some View {
Button("Open Settings") { showSheet = true }
.accessibilityFocused($focusOnTrigger)
.sheet(isPresented: $showSheet) {
SettingsSheet()
.onDisappear {
// Slight delay allows the transition to complete before moving focus
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(100))
focusOnTrigger = true
}
}
}
}
}enum A11yFocus: Hashable {
case nameField
case emailField
case submitButton
}
struct FormView: View {
@AccessibilityFocusState private var focus: A11yFocus?
var body: some View {
Form {
TextField("Name", text: $name)
.accessibilityFocused($focus, equals: .nameField)
TextField("Email", text: $email)
.accessibilityFocused($focus, equals: .emailField)
Button("Submit") { validate() }
.accessibilityFocused($focus, equals: .submitButton)
}
}
func validate() {
if name.isEmpty {
focus = .nameField // Move VoiceOver to the invalid field
}
}
}.isModalCustomDialog()
.accessibilityAddTraits(.isModal)
.accessibilityAction(.escape) { dismiss() }// Announce a status change (e.g., "Item deleted", "Upload complete")
UIAccessibility.post(notification: .announcement, argument: "Upload complete")
// Partial screen update -- move focus to a specific element
UIAccessibility.post(notification: .layoutChanged, argument: targetView)
// Full screen transition -- move focus to the new screen
UIAccessibility.post(notification: .screenChanged, argument: newScreenView)@ScaledMetric@ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = 24
@ScaledMetric private var spacing: CGFloat = 8
var body: some View {
HStack(spacing: spacing) {
Image(systemName: "star.fill")
.frame(width: iconSize, height: iconSize)
Text("Favorite")
}
}@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
if dynamicTypeSize.isAccessibilitySize {
VStack(alignment: .leading) { icon; textContent }
} else {
HStack { icon; textContent }
}
}dynamicTypeSize.isAccessibilitySizeButton(action: { }) {
Image(systemName: "plus")
.frame(minWidth: 44, minHeight: 44)
}
.contentShape(Rectangle())List(items) { item in
ItemRow(item: item)
}
.accessibilityRotor("Unread") {
ForEach(items.filter { !$0.isRead }) { item in
AccessibilityRotorEntry(item.title, id: item.id)
}
}
.accessibilityRotor("Flagged") {
ForEach(items.filter { $0.isFlagged }) { item in
AccessibilityRotorEntry(item.title, id: item.id)
}
}@Environment(\.accessibilityReduceMotion) var reduceMotion
@Environment(\.accessibilityReduceTransparency) var reduceTransparency
@Environment(\.colorSchemeContrast) var contrast // .standard or .increased
@Environment(\.legibilityWeight) var legibilityWeight // .regular or .boldwithAnimation(reduceMotion ? nil : .spring()) {
showContent.toggle()
}
content.transition(reduceMotion ? .opacity : .slide)// Solid backgrounds when transparency is reduced
.background(reduceTransparency ? Color(.systemBackground) : Color(.systemBackground).opacity(0.85))
// Stronger colors when contrast is increased
.foregroundStyle(contrast == .increased ? .primary : .secondary)
// Bold weight when system bold text is enabled
.fontWeight(legibilityWeight == .bold ? .bold : .regular)// Decorative images: hidden from VoiceOver
Image(decorative: "background-pattern")
Image("visual-divider").accessibilityHidden(true)
// Icon next to text: Label handles this automatically
Label("Settings", systemImage: "gear")
// Icon-only buttons: MUST have an accessibility label
Button(action: { }) {
Image(systemName: "gear")
}
.accessibilityLabel("Settings")AssistiveAccessAssistiveAccessisAccessibilityElement = trueaccessibilityLabel.insert().remove()accessibilityViewIsModal = true.announcement.layoutChanged.screenChanged// UIKit trait modification
customButton.accessibilityTraits.insert(.button)
customButton.accessibilityTraits.remove(.staticText)
// Modal overlay
overlayView.accessibilityViewIsModal = trueaccessibilityCustomContentProductRow(product: product)
.accessibilityCustomContent("Price", product.formattedPrice)
.accessibilityCustomContent("Rating", "\(product.rating) out of 5")
.accessibilityCustomContent(
"Availability",
product.inStock ? "In stock" : "Out of stock",
importance: .high // .high reads automatically with the element
).accessibilityTraits(.isButton).accessibilityAddTraits(.isButton).accessibilityElement(children: .combine).accessibilityLabel("Settings button")Image.accessibilityLabelaccessibilityReduceMotion.font(.system(size: 16)).font(.body)frame(minWidth: 44, minHeight: 44).contentShape().isModal.accessibilityAddTraits(.isModal).accessibilityAddTraitsImage(decorative:).accessibilityHidden(true).accessibilityElement(children: .combine).isModal@ScaledMetricSendableapple-docssearchAppleDocumentationfetchAppleDocumentation/documentation/swiftui/view-accessibility/documentation/swiftui/scaledmetric