Innehållsförteckning
Lär dig implementera tillgänglighet i iOS-appar med UIAccessibility API och SwiftUI. Denna guide ger dig konkreta kodexempel, tekniska patterns och best practices för att bygga appar som fungerar perfekt med VoiceOver och andra hjälpmedel.
Grundläggande UIAccessibility API
UIAccessibility är det centrala ramverket för att göra iOS-appar tillgängliga. Varje UIView har accessibility-properties som du kan konfigurera för att beskriva elementet för hjälpmedel som VoiceOver.
Accessibility Element eller inte?
Det första beslutet du måste fatta för varje view är om den ska vara ett accessibility element. Som standard är de flesta UIKit-komponenter som UIButton och UILabel accessibility elements, medan containers som UIView inte är det.
// UIKit
imageView.isAccessibilityElement = true
decorativeImageView.isAccessibilityElement = false
// SwiftUI
Image("profile")
.accessibilityHidden(false)
Image("decorative-pattern")
.accessibilityHidden(true)
Dekorativa element som bakgrundsbilder, dividers och ren layout-struktur ska inte vara accessibility elements. De skapar bara brus för VoiceOver-användare. Fokusera på innehåll och interaktiva element som faktiskt förmedlar information eller funktionalitet.
Accessibility Labels – Beskriva vad elementet är
En accessibility label beskriver vad ett element är eller innehåller. Detta är den viktigaste accessibility-propertyn och bör alltid vara närvarande för alla tillgängliga element.
// UIKit - Enkel label
addButton.accessibilityLabel = "Lägg till artikel"
// UIKit - Dynamisk label baserad på state
likeButton.accessibilityLabel = isLiked ? "Avmarkera som favorit" : "Markera som favorit"
// SwiftUI
Button(action: addArticle) {
Image(systemName: "plus")
}
.accessibilityLabel("Lägg till artikel")
// SwiftUI - Dynamisk
Button(action: toggleLike) {
Image(systemName: isLiked ? "heart.fill" : "heart")
}
.accessibilityLabel(isLiked ? "Avmarkera som favorit" : "Markera som favorit")
Skriv labels som substantiv eller korta fraser, inte fullständiga meningar. Undvik att inkludera elementtypen eftersom VoiceOver redan meddelar det baserat på traits. Säg ”Lägg till artikel” inte ”Knapp för att lägga till artikel”.
Labels ska vara på samma språk som resten av appen och bör vara korta nog att förstå snabbt men tillräckligt beskrivande för att vara meningsfulla utan visuell kontext.
Accessibility Hints – Förklara vad som händer
En hint ger ytterligare information om vad som händer när användaren interagerar med elementet. Hints är valfria och ska bara användas när resultatet av interaktionen inte är uppenbart från labeln.
// UIKit
shareButton.accessibilityLabel = "Dela"
shareButton.accessibilityHint = "Öppnar delningsalternativ för denna artikel"
// SwiftUI
Button("Dela") {
showShareSheet()
}
.accessibilityLabel("Dela artikel")
.accessibilityHint("Öppnar delningsalternativ")
Skriv hints som kompletta meningar som börjar med ett verb i presens: ”Öppnar”, ”Spelar”, ”Skickar”, ”Navigerar till”. VoiceOver läser hints efter en kort paus, så användare som inte vill vänta kan navigera vidare innan hintet läses.
Använd hints sparsamt. Om din label redan är tillräckligt beskrivande behövs ingen hint. För en knapp med labeln ”Skicka meddelande” är resultatet redan uppenbart.
Accessibility Traits – Definiera elementtyp och state
Traits beskriver vilken typ av element det är och vilket tillstånd det är i. UIKit-komponenter har automatiskt rätt traits, men när du bygger custom views måste du sätta dem explicit.
// UIKit - Basic traits
customButton.accessibilityTraits = .button
headerLabel.accessibilityTraits = .header
// UIKit - Kombinera flera traits
selectedButton.accessibilityTraits = [.button, .selected]
disabledButton.accessibilityTraits = [.button, .notEnabled]
// SwiftUI
Text("Huvudrubrik")
.accessibilityAddTraits(.isHeader)
CustomToggle(isOn: $isEnabled)
.accessibilityAddTraits(.isButton)
.accessibilityAddTraits(isEnabled ? .isSelected : [])
Vanliga traits inkluderar .button, .link, .header, .image, .searchField, .selected, .notEnabled och .adjustable. Rätt traits är kritiska för att VoiceOver ska kommunicera elementets natur korrekt.
För custom controls som beter sig som standardkomponenter, använd samma traits som standardkomponenten använder. En custom knapp ska ha .button trait även om den är byggd från grunden.
Accessibility Value – Visa current state
Value används för element vars tillstånd kan ändras eller som har ett värde som kan justeras. Detta är särskilt viktigt för sliders, switches och progressbars.
// UIKit - Slider
volumeSlider.accessibilityValue = "\(Int(volumeSlider.value))%"
// UIKit - Switch
notificationsSwitch.accessibilityValue = notificationsSwitch.isOn ? "På" : "Av"
// SwiftUI - Slider
Slider(value: $volume, in: 0...100)
.accessibilityLabel("Volym")
.accessibilityValue("\(Int(volume))%")
// SwiftUI - Toggle
Toggle("Notifikationer", isOn: $notificationsEnabled)
.accessibilityValue(notificationsEnabled ? "På" : "Av")
UIKit standardkomponenter som UISwitch och UISlider sätter sina egna values automatiskt, men om du bygger custom controls måste du uppdatera value manuellt när state ändras.
Custom Controls och Accessibility
När du bygger custom UI-komponenter är det ditt ansvar att implementera all accessibility-funktionalitet. Standardkomponenter ger dig mycket gratis, men custom controls kräver explicit arbete.
Göra en custom view till en accessibility element
En basic custom view är som standard inte ett accessibility element. Du måste aktivera det och sätta alla relevanta properties.
class CustomButton: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupAccessibility()
}
private func setupAccessibility() {
isAccessibilityElement = true
accessibilityLabel = "Custom knapp"
accessibilityTraits = .button
accessibilityHint = "Aktiverar special funktion"
}
// Hantera activation
override func accessibilityActivate() -> Bool {
// Utför knappens action
performButtonAction()
return true
}
}
Metoden accessibilityActivate() kallas när användaren dubbelklickar på elementet med VoiceOver. Returnera true om aktivering lyckades, annars false. Detta ger feedback till användaren om deras action hade effekt.
Custom containers med flera element
När du har en container med flera subelement som alla ska vara tillgängliga separat, måste du hantera accessibility-hierarkin korrekt.
class ArticleCell: UITableViewCell {
let titleLabel = UILabel()
let authorLabel = UILabel()
let favoriteButton = UIButton()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupAccessibility()
}
private func setupAccessibility() {
// Gör cellen själv icke-tillgänglig
isAccessibilityElement = false
// Exponera individuella element
titleLabel.isAccessibilityElement = true
authorLabel.isAccessibilityElement = true
favoriteButton.isAccessibilityElement = true
// Eller gruppera om det är lämpligt
// accessibilityElements = [titleLabel, authorLabel, favoriteButton]
}
}
Använd accessibilityElements array för att explicit definiera ordningen element ska läsas i. Detta är användbart när den visuella layouten inte matchar den logiska läsordningen.
Implementera custom actions
Custom actions låter användare utföra flera operationer på ett element utan att navigera till separata knappar. Detta håller gränssnittet rent och effektiviserar VoiceOver-navigation.
class MessageCell: UITableViewCell {
func setupAccessibility(with message: Message) {
isAccessibilityElement = true
accessibilityLabel = "\(message.sender), \(message.preview)"
accessibilityTraits = .button
// Lägg till custom actions
accessibilityCustomActions = [
UIAccessibilityCustomAction(
name: "Svara",
target: self,
selector: #selector(reply)
),
UIAccessibilityCustomAction(
name: "Vidarebefordra",
target: self,
selector: #selector(forward)
),
UIAccessibilityCustomAction(
name: "Radera",
target: self,
selector: #selector(delete)
)
]
}
@objc private func reply() -> Bool {
delegate?.replyToMessage()
return true
}
@objc private func forward() -> Bool {
delegate?.forwardMessage()
return true
}
@objc private func delete() -> Bool {
delegate?.deleteMessage()
return true
}
}
VoiceOver-användare får tillgång till custom actions genom rotor-menyn. Detta är mycket mer effektivt än att ha tre separata knappar som de måste navigera genom för varje meddelande.
SwiftUI Accessibility Modifiers
SwiftUI har ett deklarativt approach till accessibility som känns mer naturligt än UIKits imperativa stil. Alla accessibility properties sätts via modifiers på views.
Grundläggande accessibility i SwiftUI
SwiftUI-komponenter som Text, Button och Image har automatic accessibility, men du kan och bör anpassa den för bättre upplevelse.
// Basic accessibility
Button(action: saveDocument) {
Image(systemName: "square.and.arrow.down")
}
.accessibilityLabel("Spara dokument")
.accessibilityHint("Sparar dina ändringar till iCloud")
// Kombinera flera modifiers
Toggle("Mörkt läge", isOn: $darkModeEnabled)
.accessibilityLabel("Aktivera mörkt läge")
.accessibilityValue(darkModeEnabled ? "På" : "Av")
.accessibilityHint("Växlar mellan ljust och mörkt färgschema")
SwiftUI gör det enkelt att skapa dynamiska accessibility labels och values baserat på state med standard Swift-syntax.
Gruppera accessibility elements
När flera views bör läsas som en enhet kan du gruppera dem med accessibilityElement(children:).
// Gruppera flera text views
HStack {
Text("Temperatur:")
Text("\(temperature)°C")
.font(.headline)
}
.accessibilityElement(children: .combine)
// VoiceOver läser: "Temperatur: 22°C"
// Ignorera children helt
VStack {
Image("profile")
Text(userName)
Text(userTitle)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(userName), \(userTitle)")
.combine slår samman alla barn till ett element med concatenerad text. .ignore gör att barnen inte är tillgängliga alls och låter dig sätta en custom label på containern.
Custom accessibility actions i SwiftUI
SwiftUI har ett ännu enklare sätt att definiera custom actions än UIKit.
struct EmailRow: View {
let email: Email
var body: some View {
HStack {
Text(email.subject)
Spacer()
Text(email.date)
}
.accessibilityElement()
.accessibilityLabel("\(email.subject), från \(email.sender)")
.accessibilityAction(named: "Svara") {
replyToEmail()
}
.accessibilityAction(named: "Vidarebefordra") {
forwardEmail()
}
.accessibilityAction(named: "Arkivera") {
archiveEmail()
}
}
}
Varje .accessibilityAction() modifier lägger till en ny action i VoiceOver rotor-menyn. Detta är mycket renare kod än UIKit motsvarigheten.
Accessibility sortPriority
När VoiceOvers fokusordning inte följer den logiska eller visuella ordningen kan du justera det med accessibilitySortPriority.
VStack {
// Denna bör läsas sist trots att den är först i koden
Text("Extra information")
.accessibilitySortPriority(0)
// Huvudinnehållet läses först
Text("Huvudrubrik")
.accessibilitySortPriority(2)
Text("Underrubrik")
.accessibilitySortPriority(1)
}
Högre värden läses före lägre värden. Default priority är 0. Använd detta sparsamt och endast när den naturliga ordningen verkligen är fel.
Dynamiska uppdateringar och notifications
När innehåll ändras dynamiskt i din app måste du informera VoiceOver så att användare blir medvetna om förändringen.
Layout changed notifications
När element läggs till, tas bort eller flyttas i en vy ska du skicka en layout changed notification.
// UIKit
func addNewMessage(_ message: Message) {
messageViews.append(createMessageView(message))
UIAccessibility.post(
notification: .layoutChanged,
argument: messageViews.last
)
}
// SwiftUI
@State private var messages: [Message] = []
var body: some View {
ScrollView {
ForEach(messages) { message in
MessageView(message: message)
}
}
.onChange(of: messages.count) { _ in
if let lastMessage = messages.last {
UIAccessibility.post(
notification: .layoutChanged,
argument: lastMessage.id
)
}
}
}
Argument-parametern (optional) specifierar vilket element fokus ska flyttas till. Om du inte anger argument förblir fokus där det var.
Screen changed notifications
När hela skärmen ändras dramatiskt, som vid navigation till en ny vy, använd screen changed notification.
// UIKit - I viewDidAppear
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIAccessibility.post(
notification: .screenChanged,
argument: mainHeadingLabel
)
}
// SwiftUI
struct DetailView: View {
var body: some View {
VStack {
Text("Detaljer")
.font(.largeTitle)
.accessibilityAddTraits(.isHeader)
}
.onAppear {
UIAccessibility.post(
notification: .screenChanged,
argument: nil
)
}
}
}
Screen changed notification är starkare än layout changed och bör användas för major transitions som modala presentationer eller navigationstransitioner.
Announcement notifications
För att läsa upp ett meddelande utan att ändra fokus, använd announcement notification.
// UIKit
func saveDocument() {
performSave()
UIAccessibility.post(
notification: .announcement,
argument: "Dokument sparat"
)
}
// SwiftUI
Button("Spara") {
saveDocument()
UIAccessibility.post(
notification: .announcement,
argument: "Dokument sparat till iCloud"
)
}
Announcements är perfekta för feedback som ”Objekt raderat”, ”Inställning ändrad” eller ”Uppladdning klar”. Använd dem för viktig information som användaren behöver veta om men som inte kräver interaktion.
Live regions för frequently updating content
För innehåll som uppdateras kontinuerligt, som timers eller live scores, markera det som frequently updating.
// UIKit
timerLabel.accessibilityTraits = .updatesFrequently
// SwiftUI
Text(timeRemaining)
.accessibilityAddTraits(.updatesFrequently)
Detta talar om för VoiceOver att innehållet ändras ofta och hjälpmedlet kan optimera hur det hanterar uppdateringarna.
Textfält och formulär
Tillgängliga formulär är kritiska för många appar, särskilt e-handel, bank och kommunikationstjänster som omfattas av LPTT.
Labels för textfält
Textfält måste ha tydliga labels som förklarar vad användaren ska fylla i. Placeholder-text är inte tillräcklig eftersom den försvinner när användaren börjar skriva.
// UIKit - Med UILabel
let emailLabel = UILabel()
emailLabel.text = "E-postadress"
let emailField = UITextField()
emailField.placeholder = "[email protected]"
emailField.accessibilityLabel = "E-postadress"
// SwiftUI - Automatiskt med native TextField
TextField("E-postadress", text: $email)
.accessibilityLabel("E-postadress")
.accessibilityHint("Ange din e-postadress för inloggning")
SwiftUI TextField har redan bra accessibility om du använder den första parametern för label. UIKit kräver explicit accessibilityLabel.
Felmeddelanden och validation
När validation misslyckas måste felmeddelanden vara tydliga och tillgängliga för skärmläsare.
// UIKit
func validateEmail() {
if !isValidEmail(emailField.text) {
errorLabel.text = "Ogiltig e-postadress"
errorLabel.isHidden = false
// Informera VoiceOver
UIAccessibility.post(
notification: .announcement,
argument: "Fel: Ogiltig e-postadress. Kontrollera formatet."
)
// Uppdatera field's accessibility
emailField.accessibilityValue = emailField.text
emailField.accessibilityHint = "Fel: Ogiltig e-postadress"
}
}
// SwiftUI
@State private var emailError: String?
TextField("E-postadress", text: $email)
.accessibilityLabel("E-postadress")
.accessibilityValue(email)
.accessibilityHint(emailError ?? "Ange din e-postadress")
.onChange(of: email) { newValue in
validateEmail(newValue)
}
if let error = emailError {
Text(error)
.foregroundColor(.red)
.onAppear {
UIAccessibility.post(
notification: .announcement,
argument: "Fel: \(error)"
)
}
}
Felmeddelanden ska vara specifika och beskriva hur användaren kan åtgärda problemet. ”Ogiltigt format” är för vagt – ”E-postadressen måste innehålla @” är mycket bättre.
Formulärstruktur och gruppering
Långa formulär ska grupperas logiskt så att VoiceOver-användare enkelt kan förstå strukturen och navigera effektivt.
// UIKit - Använd headers för sektioner
let sectionHeader = UILabel()
sectionHeader.text = "Kontaktinformation"
sectionHeader.accessibilityTraits = .header
// SwiftUI - Använd Section i Form
Form {
Section(header: Text("Kontaktinformation")) {
TextField("Namn", text: $name)
TextField("E-post", text: $email)
TextField("Telefon", text: $phone)
}
Section(header: Text("Adress")) {
TextField("Gatuadress", text: $street)
TextField("Postnummer", text: $zipCode)
TextField("Stad", text: $city)
}
}
Headers med .header trait gör det enkelt för VoiceOver-användare att hoppa mellan sektioner med rotor-navigation.
Obligatoriska fält
Kommunicera tydligt vilka fält som är obligatoriska, både visuellt och för skärmläsare.
// UIKit
nameField.accessibilityLabel = "Namn, obligatoriskt"
// Eller
nameField.accessibilityLabel = "Namn"
nameField.accessibilityHint = "Detta fält är obligatoriskt"
// SwiftUI
TextField("Namn", text: $name)
.accessibilityLabel("Namn, obligatoriskt fält")
// Alternativt med trait
TextField("Namn", text: $name)
.accessibilityLabel("Namn")
.accessibilityValue(name.isEmpty ? "Obligatoriskt, inte ifyllt" : name)
Överväg att använda samma konvention genomgående i appen så användare lär sig mönstret.
Navigation och fokushantering
Korrekt fokushantering är kritisk för god VoiceOver-upplevelse, särskilt vid navigation, modala presentationer och dynamiskt innehåll.
Flytta fokus vid navigation
När användaren navigerar till en ny skärm ska fokus flyttas till ett meningsfullt element, vanligtvis huvudrubriken eller första interaktiva elementet.
// UIKit - I viewDidAppear
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Flytta fokus till huvudrubriken
UIAccessibility.post(
notification: .screenChanged,
argument: titleLabel
)
}
// SwiftUI
struct ArticleDetailView: View {
@State private var focusedElement: Bool = false
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text(article.title)
.font(.largeTitle)
.accessibilityAddTraits(.isHeader)
.accessibilityFocused($focusedElement)
Text(article.content)
}
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
focusedElement = true
}
}
}
}
Delay i .asyncAfter ger navigationstransitionen tid att slutföras innan fokus flyttas.
Modal presentations
När en modal presenteras måste fokus flytta till modalen och vara ”fångad” där tills modalen stängs.
// UIKit
func presentModal() {
let modalVC = ModalViewController()
present(modalVC, animated: true) {
UIAccessibility.post(
notification: .screenChanged,
argument: modalVC.closeButton
)
}
}
// SwiftUI
.sheet(isPresented: $showingSheet) {
ModalView()
.onAppear {
UIAccessibility.post(
notification: .screenChanged,
argument: nil
)
}
}
När modalen stängs ska fokus returnera till elementet som öppnade den. UIKit gör detta automatiskt för vanliga presentationer, men custom transitions kan kräva manuell hantering.
Fokusordning i custom layouts
För komplex layout där den visuella ordningen inte matchar view-hierarkin kan du explicit definiera fokusordning.
// UIKit
override var accessibilityElements: [Any]? {
get {
return [
titleLabel,
subtitleLabel,
primaryButton,
secondaryButton,
footerLabel
]
}
set {
// No-op
}
}
// SwiftUI - Använd accessibilitySortPriority
VStack {
Text("Footer")
.accessibilitySortPriority(1)
Text("Title")
.accessibilitySortPriority(5)
Text("Subtitle")
.accessibilitySortPriority(4)
Button("Action") { }
.accessibilitySortPriority(3)
}
Fokusordning ska följa logisk läsordning, inte nödvändigtvis visuell layout. Tänk på hur en skärmläsaranvändare vill konsumera informationen.
Bilder och media
Bilder, ikoner och mediainnehåll kräver speciell uppmärksamhet för att vara tillgängliga.
Informativa bilder
Bilder som förmedlar information måste ha beskrivande accessibility labels.
// UIKit
productImage.isAccessibilityElement = true
productImage.accessibilityLabel = "Blå t-shirt med vit logotype"
productImage.accessibilityTraits = .image
// SwiftUI
Image("product")
.accessibilityLabel("Blå t-shirt med vit logotype")
Beskriv vad som är visuellt relevant för sammanhanget. En produktbild behöver beskriva produkten, medan en profilbild kanske bara behöver säga ”Profilbild för [namn]”.
Dekorativa bilder
Dekorativa bilder som inte tillför information ska döljas från VoiceOver.
// UIKit
decorativePattern.isAccessibilityElement = false
// SwiftUI
Image("background-pattern")
.accessibilityHidden(true)
Dividers, bakgrundsmönster, dekorativa ikoner och ren layout-grafik ska alltid vara dolda.
Ikoner i knappar
När en knapp bara innehåller en ikon måste labeln beskriva knappens funktion, inte ikonen.
// UIKit
let shareButton = UIButton()
shareButton.setImage(UIImage(systemName: "square.and.arrow.up"), for: .normal)
shareButton.accessibilityLabel = "Dela artikel" // Inte "Dela-ikon"
// SwiftUI
Button(action: shareArticle) {
Image(systemName: "square.and.arrow.up")
}
.accessibilityLabel("Dela artikel")
Fokusera på vad användaren uppnår, inte vad ikonen föreställer.
Video och audio
Videor och ljudfiler måste ha tillgängliga kontroller och, där det är relevant, textning eller transkriptioner.
// UIKit - AVPlayer
playerViewController.accessibilityLabel = "Videospelare"
playerViewController.accessibilityHint = "Dubbeltryck för att spela eller pausa"
// Kontroller måste vara tillgängliga
playButton.accessibilityLabel = isPlaying ? "Pausa" : "Spela"
volumeSlider.accessibilityLabel = "Volym"
För video som omfattas av LPTT kan textning vara obligatoriskt beroende på innehållstyp. Se till att AVFoundation’s stöd för closed captions är aktiverat.
Tabeller och listor
Tabeller och listor är vanliga komponenter som kräver särskild uppmärksamhet för god accessibility.
UITableView accessibility
UITableView har inbyggt bra accessibility-stöd, men cells behöver konfigureras korrekt.
class ArticleCell: UITableViewCell {
let titleLabel = UILabel()
let authorLabel = UILabel()
let dateLabel = UILabel()
func configure(with article: Article) {
titleLabel.text = article.title
authorLabel.text = article.author
dateLabel.text = article.dateString
// Kombinera till en accessibility label
isAccessibilityElement = true
accessibilityLabel = """
\(article.title), av \(article.author), \
publicerad \(article.dateString)
"""
accessibilityTraits = .button
accessibilityHint = "Dubbeltryck för att läsa artikeln"
}
}
Genom att göra hela cellen till ett accessibility element istället för att exponera varje label separat blir navigationen mycket snabbare för VoiceOver-användare.
SwiftUI List
SwiftUI List har automatisk accessibility om du använder den korrekt.
List(articles) { article in
ArticleRow(article: article)
.accessibilityElement(children: .combine)
.accessibilityLabel("\(article.title), \(article.author)")
.accessibilityHint("Öppnar artikeln")
}
.combine slår samman alla textelement i raden till en label, vilket är mycket mer effektivt än att navigera genom varje individuellt textelement.
Headers i listor
Section headers hjälper VoiceOver-användare navigera långa listor.
// UIKit
func tableView(_ tableView: UITableView,
viewForHeaderInSection section: Int) -> UIView? {
let header = UITableViewHeaderFooterView()
header.textLabel?.text = sectionTitles[section]
header.accessibilityTraits = .header
return header
}
// SwiftUI
List {
ForEach(groupedArticles.keys.sorted(), id: \.self) { category in
Section(header: Text(category)) {
ForEach(groupedArticles[category]!) { article in
ArticleRow(article: article)
}
}
}
}
VoiceOver-användare kan hoppa mellan headers med rotor-navigation, vilket gör navigation i långa listor mycket snabbare.
Gester och interaktion
iOS-appar använder ofta gester för interaktion, men alla funktioner måste också vara tillgängliga utan komplexa gester.
Alternativ till komplexa gester
Funktioner som kräver pinch, rotation eller multitouch-gester måste ha alternativ som fungerar med VoiceOver och andra hjälpmedel.
// Dåligt - Bara pinch-to-zoom
let pinchGesture = UIPinchGestureRecognizer(
target: self,
action: #selector(handlePinch)
)
imageView.addGestureRecognizer(pinchGesture)
// Bättre - Pinch OCH knappar
let pinchGesture = UIPinchGestureRecognizer(
target: self,
action: #selector(handlePinch)
)
imageView.addGestureRecognizer(pinchGesture)
let zoomInButton = UIButton()
zoomInButton.accessibilityLabel = "Zooma in"
zoomInButton.addTarget(self, action: #selector(zoomIn), for: .touchUpInside)
let zoomOutButton = UIButton()
zoomOutButton.accessibilityLabel = "Zooma ut"
zoomOutButton.addTarget(self, action: #selector(zoomOut), for: .touchUpInside)
Knappar eller andra standardkontroller ska alltid finnas som alternativ till gester.
Adjustable trait för värden
För element vars värde kan justeras, använd .adjustable trait så VoiceOver-användare kan ändra värdet med svep upp/ner-gester.
// UIKit - Custom slider
class CustomSlider: UIView {
var value: Int = 50
override init(frame: CGRect) {
super.init(frame: frame)
isAccessibilityElement = true
accessibilityTraits = .adjustable
accessibilityLabel = "Ljusstyrka"
updateAccessibilityValue()
}
override func accessibilityIncrement() {
value = min(100, value + 10)
updateAccessibilityValue()
UIAccessibility.post(
notification: .announcement,
argument: accessibilityValue
)
}
override func accessibilityDecrement() {
value = max(0, value - 10)
updateAccessibilityValue()
UIAccessibility.post(
notification: .announcement,
argument: accessibilityValue
)
}
private func updateAccessibilityValue() {
accessibilityValue = "\(value)%"
}
}
// SwiftUI
Slider(value: $brightness, in: 0...100)
.accessibilityLabel("Ljusstyrka")
.accessibilityValue("\(Int(brightness))%")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
brightness = min(100, brightness + 10)
case .decrement:
brightness = max(0, brightness - 10)
@unknown default:
break
}
}
Detta ger VoiceOver-användare ett naturligt sätt att justera värden utan att behöva använda komplexa gester.
Prestanda och tillgänglighet
Tillgänglighetsfunktioner kan påverka prestanda om de inte implementeras korrekt, särskilt i listor med många element eller när innehåll uppdateras ofta.
Lazy loading av accessibility information
För stora datasets, beräkna accessibility information lazy när element blir synliga istället för att förbereda allt i förväg.
// UIKit - I cellForRowAt
func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "ArticleCell",
for: indexPath
) as! ArticleCell
let article = articles[indexPath.row]
cell.configure(with: article)
// Sätt accessibility endast för synliga cells
cell.isAccessibilityElement = true
cell.accessibilityLabel = article.accessibilityDescription
return cell
}
Detta är särskilt viktigt för listor med tusentals element där att förbereda all accessibility information i förväg skulle vara resurs-intensivt.
Undvik onödiga notifications
Skicka bara accessibility notifications när de verkligen behövs. För innehåll som uppdateras mycket frekvent kan konstanta announcements bli irriterande.
// Dåligt - Announcement för varje uppdatering
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.updateCounter()
UIAccessibility.post(
notification: .announcement,
argument: "\(self.counter)"
)
}
// Bättre - Använd updatesFrequently trait
counterLabel.accessibilityTraits = .updatesFrequently
counterLabel.accessibilityValue = "\(counter)"
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.updateCounter()
// VoiceOver hanterar uppdateringar automatiskt
}
För live-uppdaterande innehåll är traits ofta bättre än manuella announcements.
Testning och debugging
Systematisk testning är avgörande för att säkerställa att din accessibility-implementation faktiskt fungerar.
Accessibility Inspector
Använd Xcodes Accessibility Inspector för att inspektera och debugga accessibility-trädet.
// Tips: Lägg till debug-kod som hjälper dig förstå accessibility-trädet
#if DEBUG
extension UIView {
func printAccessibilityTree(level: Int = 0) {
let indent = String(repeating: " ", count: level)
if isAccessibilityElement {
print("\(indent)[\(type(of: self))] \(accessibilityLabel ?? "no label")")
}
for subview in subviews {
subview.printAccessibilityTree(level: level + 1)
}
}
}
#endif
Kör denna metod från din view controller för att se hela accessibility-hierarkin i konsolen.
Unit tester för accessibility
Skriv unit tester som verifierar att kritiska element har korrekt accessibility.
func testLoginButtonAccessibility() {
let loginButton = viewController.loginButton
XCTAssertTrue(loginButton.isAccessibilityElement)
XCTAssertEqual(loginButton.accessibilityLabel, "Logga in")
XCTAssertTrue(loginButton.accessibilityTraits.contains(.button))
XCTAssertNotNil(loginButton.accessibilityHint)
}
func testFormFieldLabels() {
let emailField = viewController.emailTextField
let passwordField = viewController.passwordTextField
XCTAssertEqual(emailField.accessibilityLabel, "E-postadress")
XCTAssertEqual(passwordField.accessibilityLabel, "Lösenord")
XCTAssertTrue(passwordField.isSecureTextEntry)
}
Automatiserade tester fångar regressions och säkerställer att accessibility förblir en prioritet genom hela utvecklingscykeln.
UI-tester med VoiceOver
XCUITest kan simulera VoiceOver-interaktioner för end-to-end tester.
func testArticleFlowWithVoiceOver() {
let app = XCUIApplication()
app.launch()
// Hitta element via accessibility identifiers
let articleCell = app.cells["article-cell-0"]
XCTAssertTrue(articleCell.exists)
// Verifiera accessibility label
XCTAssertEqual(
articleCell.label,
"Breaking News, av Jane Doe, publicerad idag"
)
// Simulera VoiceOver activate
articleCell.tap()
// Verifiera att detail-vyn är tillgänglig
let articleTitle = app.staticTexts["article-title"]
XCTAssertTrue(articleTitle.exists)
}
Kombinera flera teststrategier för bästa täckning: unit tester för komponenter, UI-tester för flows, och manuell testning med VoiceOver för användarupplevelse.
Best practices sammanfattat
Efter att ha gått igenom all teknisk implementation, här är de viktigaste best practices att komma ihåg:
Använd standardkomponenter när det är möjligt. De har redan bra accessibility.
Testa tidigt och ofta med VoiceOver. Vänta inte till slutet av projektet.
Skriv beskrivande labels som förklarar vad element är, inte hur de ser ut.
Implementera custom actions för att ge kraftfull funktionalitet utan att röra gränssnittet.
Hantera fokus aktivt vid navigation och dynamiska uppdateringar.
Gruppera relaterat innehåll så VoiceOver-användare inte måste navigera genom onödigt många element.
Testa på riktiga enheter med olika skärmstorlekar och iOS-versioner.
Skriv automatiserade tester för att förhindra accessibility-regressions.
Dokumentera accessibility-beslut i din kod så teamet förstår varför saker gjorts på ett visst sätt.
Lär av användare med funktionsnedsättningar – deras feedback är ovärderlig.
Relaterade resurser
För mer information om iOS-tillgänglighet:
Testa med VoiceOver på iOS – Praktisk guide för manuell testning med VoiceOver.
Designa tillgängliga iOS-appar – Designriktlinjer för färg, typografi och layout.
iOS tillgänglighet översikt – Allmän översikt över iOS accessibility-funktioner.
Apples officiella dokumentation:
Med denna kunskap har du verktygen att bygga iOS-appar som fungerar perfekt för alla användare och uppfyller LPTT:s krav.