Hoppa till huvud innehåll

Allt om LPTT

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.