-
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.
Recursos
Vídeos relacionados
WWDC23
WWDC21
- Add rich graphics to your SwiftUI app
- Bring accessibility to charts in your app
- Create accessible experiences for watchOS
- Demystify SwiftUI
- What's new in SwiftUI
WWDC20
-
Buscar neste vídeo...
-
-
2:00 - Welcome to the Accessibility Preview
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") } } } -
4:30 - BudgetSlider
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))" } } -
5:15 - Slider
struct StandardSlider: View { @Binding var value: Double var label: String var body: some View { Slider(value: $value, in: 0...1) { Text(label) } } } -
5:50 - Accessible BudgetSlider
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))" } } -
7:05 - NavigationBarView
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 } } } -
9:40 - BudgetHistoryGraph
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) } } -
12:30 - Composition
// See CompositionExample.swift in the referenced sample project -
13:50 - FriendCellView
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 } } } -
14:50 - FriendsView
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 } } } -
15:10 - FriendsView with Containers
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 } } } -
16:20 - FriendCellView Sort Priority
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) } } } -
16:55 - FriendsView with .combine
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 } } } -
20:30 - AlertsView Implicit Rotor
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)) ) } } -
21:50 - AlertsView Explicit Rotor
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)) ) } } -
22:20 - TextEditor Rotors
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) } } -
24:45 - AlertNotificationView
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() } }
-