Accessibility iOS SwiftUI
Introduction Previously, I posted about Accessibility for UIKit. The idea behind this post is to find differences between UIKit Accessibility and SwiftUI features. Similarities: Both UIKit and SwiftUI have accessibilityLabel and accessibilityHints APIs. Differences: To use dynamic type for fonts, you need additional modifiers in SwiftUI. struct ScaledFont: ViewModifier { @Environment(\.sizeCategory) var sizeCategory var name: String var size: Double func body(content: Content) -> some View { let scaledSize = UIFontMetrics.default.scaledValue(for: size) return content.font(.custom(name, size: scaledSize)) } } extension View { func scaledFont(name: String, textSize size: Double) -> some View { return self.modifier(ScaledFont(name: name, size: size)) } } To step over elements in a list, you need to add .accessibilityElement(children: .combine) to each row in SwiftUI. struct FruitCaloriesCounter: View { var body: some View { NavigationView { List(fruits) { fruit in FruitRow(fruit: fruit) .accessibilityElement(children: .combine) } .navigationTitle("Fruits Calories Counter") .accessibilityElement(children: .contain) .navigationBarTitleDisplayMode(.inline) } } } In UIKit, you can insert and remove accessibilityTraits depending on the button state: if button.isSelected { button.accessibilityTraits.insert(.header) } else { button.accessibilityTraits.remove(.header) } In SwiftUI, you need to pass .accessibilityAddTraits(selected ? [.isSelected, .isButton] : .isButton) to one modifier. Button(action: { selected.toggle() }) { Image(systemName: selected ? "star.fill" : "star") .frame(width: 44, height: 44) .accessibilityLabel("favourite") .accessibilityHint(selected ? "removes favourite" : "makes favourite") .accessibilityAddTraits(selected ? [.isSelected, .isButton] : .isButton) } .buttonStyle(.plain) Complete Sample import SwiftUI let fruits = [ Fruit(name: "Apple", calories: 52), Fruit(name: "Banana", calories: 89), Fruit(name: "Orange", calories: 47), Fruit(name: "Pineapple", calories: 50), Fruit(name: "Strawberry", calories: 32) ] struct Fruit: Identifiable { var id: String { name } let name: String let calories: Int } struct FruitRow: View { @State private var selected = false let fruit: Fruit var body: some View { HStack(spacing: 8) { VStack(alignment: .leading, spacing: 8) { Text(fruit.name) .scaledFont(name: "Helvetica", textSize: 20) .accessibilityLabel(fruit.name) Text("\(fruit.calories) per 100g") .scaledFont(name: "Helvetica", textSize: 15) .accessibilityLabel("\(fruit.calories) calories per 100 grams") } Spacer() Button(action: { selected.toggle() }) { Image(systemName: selected ? "star.fill" : "star") .frame(width: 44, height: 44) .accessibilityLabel("favourite") .accessibilityHint(selected ? "removes favourite" : "makes favourite") .accessibilityAddTraits(selected ? [.isSelected, .isButton] : .isButton) } .buttonStyle(.plain) } } } struct FruitCaloriesCounter: View { var body: some View { NavigationView { List(fruits) { fruit in FruitRow(fruit: fruit) .accessibilityElement(children: .combine) } .navigationTitle("Fruits Calories Counter") .accessibilityElement(children: .contain) .navigationBarTitleDisplayMode(.inline) } } } struct ContentView: View { var body: some View { FruitCaloriesCounter() } } #Preview { ContentView() } struct ScaledFont: ViewModifier { @Environment(\.sizeCategory) var sizeCategory var name: String var size: Double func body(content: Content) -> some View { let scaledSize = UIFontMetrics.default.scaledValue(for: size) return content.font(.custom(name, size: scaledSize)) } } extension View { func scaledFont(name: String, textSize size: Double) -> some View { return self.modifier(ScaledFont(name: name, size: size)) } } Thank you for reading! 馃槉