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

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 ↗

Transcript all transcripts

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 swift · at 1:54 ↗
import SwiftUI

struct FilterButton: View {
    @State 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 swift · at 2:31 ↗
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 swift · at 3:43 ↗
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 swift · at 5:13 ↗
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 swift · at 5:46 ↗
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 swift · at 6:15 ↗
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 swift · at 6:56 ↗
struct ZoomingImageView: View {
    @State private var zoomValue = 1.0
    @State 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 swift · at 7:18 ↗
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 swift · at 7:43 ↗
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 swift · at 10:10 ↗
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 swift · at 10:46 ↗
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 swift · at 12:21 ↗
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 swift · at 13:35 ↗
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