Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2021 SwiftUI & UI FrameworksAccessibility & Inclusion

WWDC21 · 28 min · SwiftUI & UI Frameworks / Accessibility & Inclusion

SwiftUI Accessibility: Beyond the basics

Go beyond the basics to deliver an exceptional accessibility experience. Learn how to use the new SwiftUI Previews in Xcode to explore the latest accessibility APIs and create fantastic, accessible apps for everyone. Find out how you can customize the automatic accessibility built into SwiftUI to make your own custom controls accessible. Explore best practices and identify where to improve your app’s navigation experience using grouping and focus. And help supercharge navigation for VoiceOver users with the addition of rotors.

Watch at developer.apple.com ↗

Transcript all transcripts

Code shown on screen · 16 snippets

Welcome to the Accessibility Preview swift · at 2:00 ↗
struct ContentView: View {
    var body: some View {
        VStack {
            Text("WWDC 2021")
                .accessibilityAddTraits(.isHeader)

            Text("SwiftUI Accessibility")
            Text("Beyond the Basics")

            Image(systemName: "checkmark.seal.fill")
        }
    }
}
BudgetSlider swift · at 4:30 ↗
struct BudgetSlider: View {
    @Binding var value: Double
    var label: String

    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Text(label)
                Text(value.toDollars()).bold()
            }
            SliderShape(value: value)
                .gesture(DragGesture().onChanged(handle))
        }
    }
}

struct SliderShape: View {
    var value: Double

    private struct BackgroundTrack: View {
        var cornerRadius: CGFloat
        var body: some View {
            RoundedRectangle(
                cornerRadius: cornerRadius,
                style: .continuous
            )
            .foregroundColor(Color(white: 0.2))
        }
    }

    private struct OverlayTrack: View {
        var cornerRadius: CGFloat
        var body: some View {
            RoundedRectangle(
                cornerRadius: cornerRadius,
                style: .continuous
            )
            .foregroundColor(Color(white: 0.95))
        }
    }

    private struct Knob: View {
        var cornerRadius: CGFloat
        var body: some View {
            RoundedRectangle(
                cornerRadius: cornerRadius,
                style: .continuous
            )
            .strokeBorder(Color(white: 0.7), lineWidth: 1)
            .shadow(radius: 3)
        }
    }

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                BackgroundTrack(cornerRadius: geometry.size.height / 2)

                OverlayTrack(cornerRadius: geometry.size.height / 2)
                    .frame(
                        width: max(geometry.size.height, geometry.size.width * CGFloat(value) + geometry.size.height / 2),
                        height: geometry.size.height)

                Knob(cornerRadius: geometry.size.height / 2)
                    .frame(
                        width: geometry.size.height,
                        height: geometry.size.height)
                    .offset(x: max(0, geometry.size.width * CGFloat(value) - geometry.size.height / 2), y: 0)
            }
        }
    }
}

extension Double {
    func toDollars() -> String {
        return "$\(Int(self))"
    }
}
Slider swift · at 5:15 ↗
struct StandardSlider: View {
    @Binding var value: Double
    var label: String

    var body: some View {
        Slider(value: $value, in: 0...1) {
            Text(label) 
        }
    }
}
Accessible BudgetSlider swift · at 5:50 ↗
struct BudgetSlider: View {
    @Binding var value: Double
    var label: String

    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                Text(label)
                Text(value.toDollars()).bold()
            }
            SliderShape(value: value)
                .gesture(DragGesture().onChanged(handle))
                .accessibilityRepresentation {
                    Slider(value: $value, in: 0...1) {
                        Text(label)
                    }
                    .accessibilityValue(value.toDollars())
                }
        }
    }
}

struct SliderShape: View {
    var value: Double

    private struct BackgroundTrack: View {
        var cornerRadius: CGFloat
        var body: some View {
            RoundedRectangle(
                cornerRadius: cornerRadius,
                style: .continuous
            )
            .foregroundColor(Color(white: 0.2))
        }
    }

    private struct OverlayTrack: View {
        var cornerRadius: CGFloat
        var body: some View {
            RoundedRectangle(
                cornerRadius: cornerRadius,
                style: .continuous
            )
            .foregroundColor(Color(white: 0.95))
        }
    }

    private struct Knob: View {
        var cornerRadius: CGFloat
        var body: some View {
            RoundedRectangle(
                cornerRadius: cornerRadius,
                style: .continuous
            )
            .strokeBorder(Color(white: 0.7), lineWidth: 1)
            .shadow(radius: 3)
        }
    }

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                BackgroundTrack(cornerRadius: geometry.size.height / 2)

                OverlayTrack(cornerRadius: geometry.size.height / 2)
                    .frame(
                        width: max(geometry.size.height, geometry.size.width * CGFloat(value) + geometry.size.height / 2),
                        height: geometry.size.height)

                Knob(cornerRadius: geometry.size.height / 2)
                    .frame(
                        width: geometry.size.height,
                        height: geometry.size.height)
                    .offset(x: max(0, geometry.size.width * CGFloat(value) - geometry.size.height / 2), y: 0)
            }
        }
    }
}

extension Double {
    func toDollars() -> String {
        return "$\(Int(self))"
    }
}
NavigationBarView swift · at 7:05 ↗
struct NavigationBarView: View {
    var body: some View {
        HStack {
            Text("Wallet Pal")
                .font(.largeTitle)
                .bold()

            Spacer()

            Button("Edit Budgets", action: { ... })
                .buttonStyle(
                    SymbolButtonStyle(
                        systemName: "slider.vertical.3"))
        }
    }
}

struct SymbolButtonStyle: ButtonStyle {
    let systemName: String

    func makeBody(configuration: Configuration) -> some View {
				Image(systemName: systemName)
            .accessibilityRepresentation { configuration.label }
    }
}
BudgetHistoryGraph swift · at 9:40 ↗
struct Budget: Identifiable {
    var month: String
    var amount: Double

    var id: String { month }
}

struct BudgetHistoryGraph: View {
    var budgets: [Budget]

    var body: some View {
        GeometryReader { proxy in
            VStack {
                Canvas { ctx, size in
                    let inset: CGFloat = 25
                    let insetSize = CGSize(width: size.width, height: size.height - inset * 2)
                    let width = insetSize.width / CGFloat(budgets.count)
                    let max = budgets.map(\.amount).max() ?? 0
                    for n in budgets.indices {
                        let x = width * CGFloat(n)
                        let height = (CGFloat(budgets[n].amount) / CGFloat(max)) * insetSize.height
                        let y = insetSize.height - height
                        let p = Path(
                            roundedRect: CGRect(
                                x: x + 2.5,
                                y: y + inset,
                                width: width - 5,
                                height: height),
                            cornerRadius: 4)
                        ctx.fill(p, with: .color(Color.green))

                        ctx.draw(Text(budgets[n].amount.toDollars()), at: CGPoint(x: x + width / 2, y: y + inset / 2))

                        ctx.draw(Text(budgets[n].month), at: CGPoint(x: x + width / 2, y: y + height + 1.5*inset))
                    }
                }
                .accessibilityLabel("Budget History Graph")
                .accessibilityChildren {
                    HStack {
                        ForEach(budgets) { budget in
                            Rectangle()
                                .accessibilityLabel(budget.month)
                                .accessibilityValue(budget.amount.toDollars())

                        }
                    }
                }

            }
        }
        .padding()
        .background(
            RoundedRectangle(cornerRadius: 16)
                .foregroundColor(Color(white: 0.9)))
        .padding(.horizontal)
    }
}
Composition swift · at 12:30 ↗
// See CompositionExample.swift in the referenced sample project
FriendCellView swift · at 13:50 ↗
struct User: Identifiable {
    var id: Int
    var name: String
    var photo: String
}

struct FriendCellView: View {
    var user: User

    var body: some View {
        ZStack(alignment: .topLeading) {
            VStack(alignment: .center) {
                Image(user.photo)
                Text(user.name)
            }

            Button("Send Challenge", action: { /* ... */ })
                .buttonStyle(
                    SymbolButtonStyle(
                        systemName: "gamecontroller.fill"))
        }
    }
}

struct SymbolButtonStyle: ButtonStyle {
    let systemName: String

    func makeBody(configuration: Configuration) -> some View {
				Image(systemName: systemName)
            .accessibilityRepresentation { configuration.label }
    }
}
FriendsView swift · at 14:50 ↗
struct User: Identifiable {
    var id: Int
    var name: String
    var photo: String
}

struct FriendCellView: View {
    var user: User

    var body: some View {
        ZStack(alignment: .topLeading) {
            VStack(alignment: .center) {
                Image(user.photo)
                Text(user.name)
            }

            Button("Send Challenge", action: { /* ... */ })
                .buttonStyle(
                    SymbolButtonStyle(
                        systemName: "gamecontroller.fill"))
        }
    }
}
      
struct FriendsView: View {
    var users: [User]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(users) { user in
                    FriendCellView(user: user)
                        .onTapGesture { /* ... */ }
                }

                AddFriendButton()

                Spacer()
            }
        }
    }
}
  
struct AddFriendButton: View {
    var body: some View {
        Button(action: { /* ... */ }) {
            Circle()
                .foregroundColor(Color(white: 0.9))
                .frame(width: 50, height: 50)
                .overlay(
                    Image(systemName: "plus")
                        .resizable()
                        .foregroundColor(Color(white: 0.5))
                        .padding(15)
                )
        }
        .buttonStyle(PlainButtonStyle())
    }
}

struct SymbolButtonStyle: ButtonStyle {
    let systemName: String

    func makeBody(configuration: Configuration) -> some View {
				Image(systemName: systemName)
            .accessibilityRepresentation { configuration.label }
    }
}
FriendsView with Containers swift · at 15:10 ↗
struct User: Identifiable {
    var id: Int
    var name: String
    var photo: String
}

struct FriendCellView: View {
    var user: User

    var body: some View {
        ZStack(alignment: .topLeading) {
            VStack(alignment: .center) {
                Image(user.photo)
                Text(user.name)
            }

            Button("Send Challenge", action: { /* ... */ })
                .buttonStyle(
                    SymbolButtonStyle(
                        systemName: "gamecontroller.fill"))
        }
    }
}
      
struct FriendsView: View {
    var users: [User]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(users) { user in
                    FriendCellView(user: user)
                         .accessibilityElement(children: .contain)
                        .onTapGesture { /* ... */ }
                }

                AddFriendButton()

                Spacer()
            }
        }
    }
}
  
struct AddFriendButton: View {
    var body: some View {
        Button(action: { /* ... */ }) {
            Circle()
                .foregroundColor(Color(white: 0.9))
                .frame(width: 50, height: 50)
                .overlay(
                    Image(systemName: "plus")
                        .resizable()
                        .foregroundColor(Color(white: 0.5))
                        .padding(15)
                )
        }
        .buttonStyle(PlainButtonStyle())
    }
}

struct SymbolButtonStyle: ButtonStyle {
    let systemName: String

    func makeBody(configuration: Configuration) -> some View {
				Image(systemName: systemName)
            .accessibilityRepresentation { configuration.label }
    }
}
FriendCellView Sort Priority swift · at 16:20 ↗
struct User: Identifiable {
    var id: Int
    var name: String
    var photo: String
}

struct FriendCellView: View {
    var user: User

    var body: some View {
        ZStack(alignment: .topLeading) {
            VStack(alignment: .center) {
                Image(user.photo)
                Text(user.name)
            }

            Button("Send Challenge", action: { /* ... */ })
                .buttonStyle(
                    SymbolButtonStyle(
                        systemName: "gamecontroller.fill"))
                .accessibilitySortPriority(-1)
        }
    }
}
FriendsView with .combine swift · at 16:55 ↗
struct User: Identifiable {
    var id: Int
    var name: String
    var photo: String
}

struct FriendCellView: View {
    var user: User

    var body: some View {
        ZStack(alignment: .topLeading) {
            VStack(alignment: .center) {
                Image(user.photo)
                Text(user.name)
            }

            Button("Send Challenge", action: { /* ... */ })
                .buttonStyle(
                    SymbolButtonStyle(
                        systemName: "gamecontroller.fill"))
        }
    }
}
      
struct FriendsView: View {
    var users: [User]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack {
                ForEach(users) { user in
                    FriendCellView(user: user)
                        .accessibilityElement(children: .combine)
                        .onTapGesture { /* ... */ }
                }

                AddFriendButton()

                Spacer()
            }
        }
    }
}
  
struct AddFriendButton: View {
    var body: some View {
        Button(action: { /* ... */ }) {
            Circle()
                .foregroundColor(Color(white: 0.9))
                .frame(width: 50, height: 50)
                .overlay(
                    Image(systemName: "plus")
                        .resizable()
                        .foregroundColor(Color(white: 0.5))
                        .padding(15)
                )
        }
        .buttonStyle(PlainButtonStyle())
    }
}

struct SymbolButtonStyle: ButtonStyle {
    let systemName: String

    func makeBody(configuration: Configuration) -> some View {
				Image(systemName: systemName)
            .accessibilityRepresentation { configuration.label }
    }
}
AlertsView Implicit Rotor swift · at 20:30 ↗
struct Alert: Identifiable {
    var id: Int
    var isUnread: Bool
    var isFlagged: Bool
    var subject: String
    var content: String
}

struct AlertsView: View {
    var alerts: [Alert]

    var body: some View {
        VStack {
            ForEach(alerts) { alert in
                AlertCellView(alert: alert)
                    .accessibilityElement(children: .combine)
            }
        }
        .accessibilityElement(children: .contain)
        .accessibilityRotor("Warnings") {
            ForEach(alerts) { alert in
                if alert.isWarning {
                    AccessibilityRotorEntry(alert.title, id: alert.id)
                }
            }
        }
    }
}

struct AlertCell: View {
    var alert: Alert

    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                if alert.isUnread {
                    Circle()
                        .foregroundColor(.blue)
                        .frame(width: 10, height: 10)
                }
                if alert.isFlagged {
                    Image(systemName: "exclamationmark.triangle.fill")
                        .foregroundColor(.orange)
                        .frame(width: 10, height: 10)
                }
                Text(alert.subject)
                    .font(.headline)
                    .fontWeight(.semibold)
                Spacer()
                Text("04/30/21")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            Text(alert.content)
                .lineLimit(3)
        }
        .padding(10)
        .background(
            RoundedRectangle(cornerRadius: 8)
                .foregroundColor(Color(white: 0.9))
        )
    }
}
AlertsView Explicit Rotor swift · at 21:50 ↗
struct Alert: Identifiable {
    var id: Int
    var isUnread: Bool
    var isFlagged: Bool
    var subject: String
    var content: String
}

struct AlertsView: View {
    var alerts: [Alert]
    @Namespace var namespace

    var body: some View {
        VStack {
            ForEach(alerts) { alert in
                VStack {
                    AlertCellView(alert: alert)
                        .accessibilityElement(children: .combine)
                        .accessibilityRotorEntry(id: alert.id, in: namespace)
                    AlertActionsView(alert: alert)
                }
            }
        }
        .accessibilityElement(children: .contain)
        .accessibilityRotor("Warnings") {
            ForEach(alerts) { alert in
                if alert.isWarning {
                    AccessibilityRotorEntry(alert.title, id: alert.id, in: namespace)
                }
            }
        }
    }
}

struct AlertCell: View {
    var alert: Alert

    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                if alert.isUnread {
                    Circle()
                        .foregroundColor(.blue)
                        .frame(width: 10, height: 10)
                }
                if alert.isFlagged {
                    Image(systemName: "exclamationmark.triangle.fill")
                        .foregroundColor(.orange)
                        .frame(width: 10, height: 10)
                }
                Text(alert.subject)
                    .font(.headline)
                    .fontWeight(.semibold)
                Spacer()
                Text("04/30/21")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            Text(alert.content)
                .lineLimit(3)
        }
        .padding(10)
        .background(
            RoundedRectangle(cornerRadius: 8)
                .foregroundColor(Color(white: 0.9))
        )
    }
}
TextEditor Rotors swift · at 22:20 ↗
struct ContentView: View {
    @State var note: Note

    var body: some View {
        TextEditor($text.content)
            .accessibilityRotor("Email Addresses", textRanges: note.addressRanges)
            .accessibilityRotor("Links", textRanges: note.linkRanges)
            .accessibilityRotor("Phone Numbers", textRanges: note.phoneNumberRanges)
    }
}
AlertNotificationView swift · at 24:45 ↗
struct Notification: Equatable {
    enum Priority {
        case low, high
    }
    var content: String
    var priority: Priority
}

struct AlertNotificationView<Content: View>: View {
    @ViewBuilder var content: Content
    @Binding var notification: Notification?
    @AccessibilityFocusState var isNotificationFocused: Bool

    var body: some View {
        ZStack(alignment: .top) {
            content

            if let notification = $notification {
                NotificationBanner(notification: notification)
                    .accessibilityFocused($isNotificationFocused)
            }
        }
        .onChange(of: notification) { notification in
            if notification?.priority == .high {
                isNotificationFocused = true
            } else {
                postAccessibilityNotification()
            }
        }
    }

    func postAccessibilityNotification() {
        guard let announcement = notification?.content else {
            return
        }
        #if os(macOS)
        NSAccessibility.post(
            element: NSApp.accessibilityWindow(),
            notification: .announcementRequested,
            userInfo: [.announcement: announcement])
        #else
        UIAccessibility.post(notification: .announcement, argument: announcement)
        #endif
    }
}

struct NotificationBanner: View {
    @Binding var notification: Notification?
    @State var timer: Timer?
    @AccessibilityFocusState var isNotificationFocused: Bool

    var body: some View {
        if let notification = notification {
            Text(notification.content)
                .accessibilityFocused($isNotificationFocused)
                .onAppear { startTimer() }
                .onDisappear { stopTimer() }
        } else {
            EmptyView()
        }
    }

    func startTimer() {
        timer = Timer.scheduledTimer(
            withTimeInterval: 3,
            repeats: true) { _ in
            if !isNotificationFocused {
                notification = nil
            }
        }
    }

    func stopTimer() {
        timer?.invalidate()
    }
}

Resources