Accessibility with VoiceOver support

Sofia Martinez Jan 2026
1 tab
import SwiftUI

struct AccessiblePostCard: View {
    let post: Post
    @State private var isLiked = false

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text(post.title)
                .font(.headline)

            Text(post.body)
                .font(.body)
                .lineLimit(3)

            HStack {
                Button(action: toggleLike) {
                    Image(systemName: isLiked ? "heart.fill" : "heart")
                        .foregroundColor(isLiked ? .red : .gray)
                }
                .accessibilityLabel(isLiked ? "Unlike post" : "Like post")
                .accessibilityHint("Double tap to \(isLiked ? "remove" : "add") like")

                Text("\(post.likesCount) likes")
                    .accessibilityHidden(true) // Included in button label

                Spacer()

                Text(post.createdAt, style: .relative)
                    .font(.caption)
                    .accessibilityLabel("Posted \(post.createdAt, style: .relative)")
            }
        }
        .padding()
        .background(Color.white)
        .cornerRadius(12)
        .shadow(radius: 2)
        .accessibilityElement(children: .contain)
        .accessibilityLabel("Post by \(post.author.name)")
        .accessibilityValue(post.title)
    }

    private func toggleLike() {
        isLiked.toggle()

        // Announce change to VoiceOver
        UIAccessibility.post(
            notification: .announcement,
            argument: isLiked ? "Post liked" : "Post unliked"
        )
    }
}

struct AccessibleFormView: View {
    @State private var username = ""
    @State private var email = ""
    @State private var agreedToTerms = false
    @Environment(\.accessibilityReduceMotion) var reduceMotion

    var body: some View {
        Form {
            Section {
                TextField("Username", text: $username)
                    .accessibilityLabel("Username")
                    .accessibilityHint("Enter your username")

                TextField("Email", text: $email)
                    .keyboardType(.emailAddress)
                    .accessibilityLabel("Email address")
                    .accessibilityHint("Enter your email address")
            }

            Section {
                Toggle(isOn: $agreedToTerms) {
                    Text("I agree to the terms and conditions")
                }
                .accessibilityLabel("Agree to terms")
                .accessibilityValue(agreedToTerms ? "Agreed" : "Not agreed")
            }

            Section {
                Button("Submit") {
                    submitForm()
                }
                .disabled(username.isEmpty || email.isEmpty || !agreedToTerms)
                .accessibilityLabel("Submit form")
                .accessibilityHint(formIsValid ? "Double tap to submit" : "Complete all fields to enable")
            }
        }
        .navigationTitle("Sign Up")
    }

    private var formIsValid: Bool {
        !username.isEmpty && !email.isEmpty && agreedToTerms
    }

    private func submitForm() {
        // Submit logic
        UIAccessibility.post(
            notification: .announcement,
            argument: "Form submitted successfully"
        )
    }
}

struct DynamicTypeExample: View {
    @Environment(\.sizeCategory) var sizeCategory

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Title")
                .font(.title)

            Text("This text automatically scales with user's Dynamic Type settings")
                .font(.body)

            // Custom scaling
            Text("Custom scaled text")
                .font(.system(size: scaledSize))
        }
        .padding()
    }

    private var scaledSize: CGFloat {
        switch sizeCategory {
        case .extraSmall, .small, .medium:
            return 14
        case .large, .extraLarge:
            return 16
        case .extraExtraLarge, .extraExtraExtraLarge:
            return 18
        default:
            return 20
        }
    }
}
1 file · swift Explain with highlit

Accessible iOS apps work for all users, including those with disabilities. VoiceOver reads UI elements, requiring proper labels and hints. I set .accessibilityLabel() for non-text elements like images and buttons, describing what they represent. .accessibilityHint() explains what happens when activated. .accessibilityValue() provides dynamic content like slider positions. Grouping related elements with .accessibilityElement(children: .combine) creates logical reading order. .accessibilityHidden() hides decorative elements from VoiceOver. Dynamic Type respects user text size preferences. Testing with VoiceOver enabled catches issues. Accessibility isn't optional—it's required by law in many jurisdictions and expands your audience.