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))
}
}