2023 Accessibility & Inclusion
WWDC23 · 16 min · Accessibility & Inclusion
Build accessible apps with SwiftUI and UIKit
Discover how advancements in UI frameworks make it easier to build rich, accessible experiences. Find out how technologies like VoiceOver can better interact with your app’s interface through accessibility traits and actions. We’ll share the latest updates to SwiftUI that help you refine your accessibility experience and show you how to keep accessibility information up-to-date in your UIKit apps.
Watch at developer.apple.com ↗Chapters
- 0:00 — Welcome
- 1:30 — Explore the toggle trait
- 2:46 — Discover multi-platform accessibility announcements
- 3:58 — Assign priority to announcements
- 6:36 — Meet the zoom action
- 8:00 — Refine VoiceOver direct touch experiences
- 11:08 — Customize accessibility content shapes in SwiftUI
- 12:48 — Keep accessibility attributes up-to-date in UIKit using block-based setters
Code shown on screen · 13 snippets
Add the accessibility toggle trait
import SwiftUI
struct FilterButton: View {
var filter: Bool = false
var body: some View {
Button(action: { filter.toggle() }) {
Text("Filter")
}
.background(filter ? darkGreen : lightGreen)
.accessibilityAddTraits(.isToggle)
}
} Add the accessibility toggle trait with UIKit
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let filterButton = UIButton(type: .custom)
setupButtonView()
filterButton.accessibilityTraits = [.toggleButton]
view.addSubview(filterButton)
}
} Post an accessibility notification
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
PhotoFilterView
.toolbar {
Button(action: {
AccessibilityNotification.Announcement("Loading Photos View")
.post()
}) {
Text("Photos")
}
}
}
}
} Assign announcement priority
import SwiftUI
struct ZoomingImageView: View {
var defaultPriorityAnnouncement = AttributedString("Opening Camera")
var lowPriorityAnnouncement: AttributedString {
var lowPriorityString = AttributedString("Camera Loading")
lowPriorityString.accessibilitySpeechAnnouncementPriority = .low
return lowPriorityString
}
var highPriorityAnnouncement: AttributedString {
var highPriorityString = AttributedString("Camera Active")
highPriorityString.accessibilitySpeechAnnouncementPriority = .high
return highPriorityString
}
// ...
} Post announcements with priority set
import SwiftUI
struct CameraButton: View {
// ...
var body: some View {
Button(action: {
// Open Camera Code
AccessibilityNotification.Announcement(defaultPriorityAnnouncement).post()
// Camera Loading Code
AccessibilityNotification.Announcement(lowPriorityAnnouncement).post()
// Camera Loaded Code
AccessibilityNotification.Announcement(highPriorityAnnouncement).post()
}) {
Image("Camera")
}
}
}
} Assign announcement priority with UIKit
class ViewController: UIViewController {
let defaultAnnouncement = NSAttributedString(string: "Opening Camera", attributes:
[NSAttributedString.Key.UIAccessibilitySpeechAttributeAnnouncementPriority:
UIAccessibilityPriority.default]
)
let lowPriorityAnnouncement = NSAttributedString(string: "Camera Loading", attributes:
[NSAttributedString.Key.UIAccessibilitySpeechAttributeAnnouncementPriority:
UIAccessibilityPriority.low]
)
let highPriorityAnnouncement = NSAttributedString(string: "Camera Active", attributes:
[NSAttributedString.Key.UIAccessibilitySpeechAttributeAnnouncementPriority:
UIAccessibilityPriority.high]
)
// ...
} Add the accessibility zoom action
struct ZoomingImageView: View {
private var zoomValue = 1.0
var imageName: String?
var body: some View {
Image(imageName ?? "")
.scaleEffect(zoomValue)
.accessibilityZoomAction { action in
let zoomQuantity = "\(Int(zoomValue)) x zoom"
switch action.direction {
case .zoomIn:
zoomValue += 1.0
AccessibilityNotification.Announcement(zoomQuantity).post()
case .zoomOut:
zoomValue -= 1.0
AccessibilityNotification.Announcement(zoomQuantity).post()
}
}
}
} Add the accessibility zoom action with UIKit
import UIKit
class ViewController: UIViewController {
let zoomView = ZoomingImageView(frame: .zero)
let imageView = UIImageView(image: UIImage(named: "tree"))
override func viewDidLoad() {
super.viewDidLoad()
zoomView.isAccessibilityElement = true
zoomView.accessibilityLabel = "Zooming Image View"
zoomView.accessibilityTraits = [.image, .supportsZoom]
zoomView.addSubview(imageView)
view.addSubview(zoomView)
}
} Respond to accessibility zoom actions with UIKit
import UIKit
class ZoomingImageView: UIScrollView {
override func accessibilityZoomIn(at point: CGPoint) -> Bool {
zoomScale += 1.0
let zoomQuantity = "\(Int(zoomValue)) x zoom"
UIAccessibility.post(notification: .announcement, argument: zoomQuantity)
return true
}
override func accessibilityZoomOut(at point: CGPoint) -> Bool {
zoomScale -= 1.0
let zoomQuantity = "\(Int(zoomValue)) x zoom"
UIAccessibility.post(notification: .announcement, argument: zoomQuantity)
return true
}
} Use accessibility direct touch options
import SwiftUI
struct KeyboardKeyView: View {
var soundFile: String
var body: some View {
Rectangle()
.fill(.white)
.frame(width: 35, height: 80)
.onTapGesture(count: 1) {
playSound(sound: soundFile, type: "mp3")
}
.accessibilityDirectTouch(options: .silentOnTouch)
}
} Use accessibility direct touch options with UIKit
import UIKit
class ViewController: UIViewController {
let waveformButton = UIButton(type: .custom)
override func viewDidLoad() {
super.viewDidLoad()
waveformButton.accessibilityTraits = .allowsDirectInteraction
waveformButton.accessibilityDirectTouchOptions = .silentOnTouch
waveformButton.addTarget(self, action: #selector(playTone), for: .touchUpInside)
view.addSubview(waveformButton)
}
} Set the accessibility content shape
import SwiftUI
struct ImageView: View {
var body: some View {
Image("circle-red")
.resizable()
.frame(width: 200, height: 200)
.accessibilityLabel("Red")
.contentShape(.accessibility, Circle())
}
} Update accessibility values using block-based setters with UIKit
import UIKit
class ViewController: UIViewController {
var isFiltered = false
override func viewDidLoad() {
super.viewDidLoad()
// Set up views
zoomView.accessibilityValueBlock = { [weak self] in
guard let self else { return nil }
return isFiltered ? "Filtered" : "Not Filtered"
}
}
} Resources
Related sessions
-
34 min