SwiftUI akadálymentesítés útmutató 2026: VoiceOver, Dynamic Type és iOS 26

Gyakorlati SwiftUI akadálymentesítési útmutató iOS 26-ra: VoiceOver, Dynamic Type, Reduce Motion, Custom Rotor és automatizált Swift Testing auditok, mind kóddal.

SwiftUI Akadálymentesítés (iOS 26)

Frissítve: 2026. július 11.

A SwiftUI akadálymentesítés nem utólag ráragasztott réteg, hanem a nézethierarchia szerves része. Minden View automatikusan generál egy accessibility elemet, amit a .accessibilityLabel, .accessibilityHint, .accessibilityValue és .accessibilityAddTraits módosítókkal formázunk a VoiceOver, a Dynamic Type és a Reduce Motion körülményeire. Ebben az útmutatóban végigmegyünk az iOS 26 új Accessibility Reader módján, az @AccessibilityFocusState fókuszkezelésen, a Custom Rotor akciókon és a Swift Testing alapú akadálymentesítési auditokon. Mindegyikhez futtatható kód is jár.

Őszintén szólva, én is akkor kezdtem el komolyan foglalkozni ezzel, amikor az egyik appom App Store review-t bukott a kicsi tap targetek miatt. Azóta CI-ben futtatom az auditokat, és ez a cikk pontosan azt a workflow-t tükrözi.

  • A SwiftUI-ban minden nézet alapból hozzáférhető a VoiceOver számára. A mi feladatunk a helyes accessibilityLabel és accessibilityTraits beállítása, nem az elemek utólagos létrehozása.
  • A Dynamic Type kezeléséhez a @ScaledMetric property wrappert és a Font.TextStyle alapú fontokat használjuk. A fix méretű .system(size: 14) a leggyakoribb hiba.
  • Az iOS 26 új Accessibility Reader rendszerszintű mód bármely szövegblokkot olvashatóvá tesz. Az .accessibilityTextContentType(.narrative) segít a rendszernek helyesen csoportosítani.
  • A @Environment(\.accessibilityReduceMotion) és a .accessibilityReduceMotion módosító együtt biztosítja, hogy az animációink ne okozzanak vestibuláris rosszullétet.
  • Az XCTest és a Swift Testing keretrendszer performAccessibilityAudit() hívása kiszúrja a hiányzó címkéket, alacsony kontrasztot és apró tap targeteket a CI-ben.
  • A Custom Rotor és az Accessibility Rotor Actions lehetővé teszik, hogy a VoiceOver felhasználók egy egyedi listán navigáljanak (pl. csak a kiemelt hozzászólásokon).

VoiceOver alapok: label, hint, value, traits

A VoiceOver négy információt vár minden interaktív elemtől: mi ez (label), mit csinál, ha aktiválom (hint), mi az aktuális értéke (value) és milyen szerepe van a felületen (traits). Ha csak egy ikonra teszünk egy Button-t címke nélkül, a VoiceOver a rendszer által generált leírást olvassa fel. Ez gyakran „Gomb" vagy „Kép", ami használhatatlan. A helyes minta egyszerű: a Label szemantikájú komponensek automatikusan felszedik a szöveget, az ikonos gombokra viszont explicit label kell.

struct FavoriteButton: View {
    @Binding var isFavorite: Bool
    let title: String

    var body: some View {
        Button {
            isFavorite.toggle()
        } label: {
            Image(systemName: isFavorite ? "heart.fill" : "heart")
                .foregroundStyle(isFavorite ? .pink : .secondary)
        }
        .accessibilityLabel(isFavorite ? "Eltávolítás a kedvencekből" : "Hozzáadás a kedvencekhez")
        .accessibilityHint("Kettős koppintás a \(title) kedvenc állapotának váltásához")
        .accessibilityValue(isFavorite ? "kedvenc" : "nem kedvenc")
        .accessibilityAddTraits(.isButton)
    }
}

Vegyük észre, hogy a label igét használ (mit csinál a gomb most, ha megnyomom), a hint viszont a művelet következményét írja le. A traits segíti a VoiceOver felhasználót abban, hogy tudja, milyen gesztust vár a rendszer. Például az .isHeader trait miatt a rotoron „Címsor" kategóriában jelenik meg az elem. Amikor több Text egy sorban logikailag összetartozik (pl. „100 Ft", ahol az összeg és a devizanem együtt jár), a .accessibilityElement(children: .combine) egyetlen olvasható egységgé fűzi őket. A gyakori hiba, amikor mindent .accessibilityHidden(true)-val takarunk el. Ilyenkor a VoiceOver felhasználó gyakorlatilag vak a felületre.

Dynamic Type és ScaledMetric a gyakorlatban

A Dynamic Type az iOS azon képessége, hogy a felhasználó a Beállításokban módosíthatja a szövegméretet, a legkisebb xSmall-tól az akadálymentes accessibilityExtraExtraExtraLarge-ig. Ha a fontot .system(size: 14) formában rögzítjük, a szöveg nem fog skálázódni. A helyes megközelítés: a beépített Font.TextStyle-okat (.body, .headline, .title) használjuk, amelyek automatikusan követik a felhasználó beállítását. Egyedi méretekhez a @ScaledMetric property wrappert vetjük be, ami egy fix alaphoz képest arányosan skálázza az értéket. Például egy avatar képméretet vagy egy padding értéket.

struct ArticleCard: View {
    @ScaledMetric(relativeTo: .body) private var avatarSize: CGFloat = 44
    @ScaledMetric(relativeTo: .body) private var spacing: CGFloat = 12
    @Environment(\.dynamicTypeSize) private var typeSize

    var body: some View {
        HStack(spacing: spacing) {
            Circle()
                .fill(.tint.gradient)
                .frame(width: avatarSize, height: avatarSize)

            VStack(alignment: .leading) {
                Text("Swift Concurrency 2026")
                    .font(.headline)
                Text("5 perces olvasás")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            if typeSize < .accessibility1 {
                Image(systemName: "chevron.right")
                    .foregroundStyle(.tertiary)
            }
        }
        .dynamicTypeSize(...DynamicTypeSize.accessibility3)
    }
}

A példa két fontos mintát mutat. Először: a @Environment(\.dynamicTypeSize) ellenőrzésével a chevron ikont elrejtjük a nagyon nagy szövegméreteknél, mert horizontálisan már nem férne el. Inkább kényszerítsük az elrendezést egy ViewThatFits-be, ami automatikusan váltja a vízszintes és függőleges elrendezést. Másodszor: a .dynamicTypeSize(...DynamicTypeSize.accessibility3) plafont ad. Ez akkor hasznos, ha egy elemre (pl. navigációs sáv) tényleg nem lehet átfogóan skálázni a designt. Ne használd a teljes appon, csak a kritikusan méretkorlátos komponenseknél. A Dynamic Type működését a szimulátorban a Command + Shift + A Environment Overrides pane-nel tudjuk gyorsan próbálgatni.

Reduce Motion, Reduce Transparency és Increase Contrast

A vestibuláris rendellenességgel élő felhasználók számára az agresszív mozgás (parallax, teljes képernyős zoom, gyors slide-in) fejfájást és rosszullétet okoz. A rendszer beállításokban aktiválható „Mozgás csökkentése" opció beolvasható a @Environment(\.accessibilityReduceMotion) változóból, és minden animációt előre eldöntött módon egyszerűsítenünk kell. Nem elég egy opacity fade. A mozgásirány és a nagy elmozdulás is problémás.

struct HeroCard: View {
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @Environment(\.accessibilityReduceTransparency) private var reduceTransparency
    @State private var appeared = false

    var body: some View {
        VStack {
            Text("Új tananyag").font(.largeTitle.bold())
            Text("SwiftUI akadálymentesítés")
        }
        .padding(24)
        .background {
            if reduceTransparency {
                Color(.systemBackground)
            } else {
                .ultraThinMaterial
            }
        }
        .scaleEffect(appeared ? 1 : (reduceMotion ? 1 : 0.94))
        .opacity(appeared ? 1 : 0)
        .onAppear {
            withAnimation(reduceMotion ? .linear(duration: 0.2) : .spring(response: 0.55, dampingFraction: 0.75)) {
                appeared = true
            }
        }
    }
}

Két kritikus döntést hoztunk. reduceMotion esetén elhagyjuk a scale effektet, és lecseréljük a spring animációt egy 200 ms-os lineáris opacity fade-re, ami vestibulárisan biztonságos. A reduceTransparency-nél lecseréljük az .ultraThinMaterial-t egy plain Color(.systemBackground)-ra, mert a homályos réteg megnehezíti a szöveg olvashatóságát a legkontrasztosabb módban. Ugyanígy figyeljük a colorSchemeContrast-ot (amikor a felhasználó az „Increase Contrast" opciót aktiválja): ilyenkor a szegélyeket vastagabbra tesszük, és a másodlagos szövegszínt közelebb visszük a fő szövegszínhez. A Liquid Glass tervezési nyelv útmutatónkban részletesebben körüljárjuk, hogyan viselkedik a rendszeranyag a csökkentett átlátszóság alatt.

Fókuszkezelés az AccessibilityFocusState-tel

A VoiceOver fókusz kezelése a legalulértékeltebb SwiftUI accessibility API. Az @AccessibilityFocusState property wrapper egy Boolean vagy Hashable enum értékkel köti össze a fókuszt egy nézettel, így amikor pl. egy űrlap érvényesítése hibázik, a hangolvasót programozottan a hibás mezőre ugraszthatjuk. Ez ugyanaz a minta, mint a normál @FocusState, de a VoiceOver kurzorral dolgozik, nem a hardveres billentyűzet fókusszal.

enum ProfileField: Hashable {
    case name, email, error
}

struct ProfileForm: View {
    @State private var name = ""
    @State private var email = ""
    @State private var errorMessage: String?
    @AccessibilityFocusState private var focus: ProfileField?

    var body: some View {
        Form {
            TextField("Név", text: $name)
                .accessibilityFocused($focus, equals: .name)

            TextField("E-mail", text: $email)
                .accessibilityFocused($focus, equals: .email)

            if let errorMessage {
                Text(errorMessage)
                    .foregroundStyle(.red)
                    .accessibilityFocused($focus, equals: .error)
                    .accessibilityAddTraits(.isStaticText)
            }

            Button("Mentés") { submit() }
        }
    }

    private func submit() {
        if email.isEmpty {
            errorMessage = "Az e-mail mező kötelező."
            focus = .error
        } else if name.isEmpty {
            focus = .name
        } else {
            errorMessage = nil
        }
    }
}

Amikor a felhasználó megnyomja a „Mentés"-t üres e-maillel, a hibaüzenet nem csak megjelenik, hanem a VoiceOver azonnal rá is ugrik, és felolvassa a szöveget. Ez ugyanaz a UX minta, amit a látó felhasználók a piros keretből azonnal érzékelnek. Több hibánál egy prioritási sorrendet érdemes tartani (általában felülről lefelé). Egy apró trükk: a hibaüzenet Text-jét explicit .isStaticText trait-tel látjuk el, hogy a VoiceOver ne interaktív elemként kezelje. Ha a hibaüzenet szövege dinamikus (pl. hálózati hiba után), akkor érdemes még egy .accessibilityLiveRegion(.polite)-ot is hozzáadni, hogy a rendszer automatikusan bejelentse a változást.

Egyedi Custom Rotor és Rotor Actions

A VoiceOver Rotor egy virtuális gomb, amit a felhasználó két ujjal forgatva választhat kategóriákat: „Címsorok", „Linkek", „Űrlapmezők" és így tovább. A .accessibilityRotor módosítóval saját kategóriát adhatunk hozzá. Egy hír-appban a „Kiemelt cikkek" rotoron a felhasználó egyetlen mozdulattal ugorhat végig a kiemelt tartalmakon, kihagyva a hirdetéseket és a szekció fejléceket.

struct FeedView: View {
    let articles: [Article]

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 16) {
                ForEach(articles) { article in
                    ArticleRow(article: article)
                        .id(article.id)
                        .accessibilityRotorEntry(id: article.id, in: "featured")
                }
            }
        }
        .accessibilityRotor("Kiemelt cikkek") {
            ForEach(articles.filter(\.isFeatured)) { article in
                AccessibilityRotorEntry(article.title, id: article.id, in: "featured")
            }
        }
    }
}

A Rotor Actions egy másik szuperhasznos minta: a VoiceOver felhasználó egy gesztussal (két ujjas forgatás plusz fel/le pöccintés) egy elem egyedi akcióit is elérheti. Például egy hozzászólásnál a „Kedvelés", „Megválaszolás", „Jelentés" akciókat. Ezt a .accessibilityAction(named:) módosítóval kötjük fel, és ezek az akciók kizárólag a VoiceOver felhasználóknak jelennek meg. Ez rendkívül fontos elektronikus kereskedelmi és közösségi appokban, ahol a swipe-akciók (pl. törlés, kedvelés) elrejtve maradnának. A SwiftUI ScrollView útmutatónk részletezi, hogyan viselkedik a scrollTargetLayout az akadálymentesítéssel.

accessibilityRepresentation és komplex vezérlők

Néha egy egyedi vezérlő (saját rajzolású slider, gesztus-alapú picker) nem ismerhető fel a rendszer által. Az accessibilityRepresentation(representation:) módosító arra való, hogy egy „proxy" nézetet adjunk a VoiceOvernek: a képernyőn a mi egyedi UI-nk látszik, de a VoiceOver egy szabványos SwiftUI Slider-t vagy Picker-t „lát", és a szabványos accessibility trait-eket használja. Ez teljesen szétválasztja a vizuális megjelenést a szemantikától.

struct CustomVolumeControl: View {
    @Binding var value: Double
    let range: ClosedRange<Double> = 0...1

    var body: some View {
        CircularKnob(value: $value)
            .gesture(DragGesture().onChanged { updateValue(from: $0) })
            .accessibilityRepresentation {
                Slider(value: $value, in: range) {
                    Text("Hangerő")
                }
            }
    }

    private func updateValue(from drag: DragGesture.Value) {
        // custom knob math
    }
}

Mostantól a VoiceOver felhasználó a szabvány „koppintás fel/le" gesztussal állítja a hangerőt, és hallja a százalékos értéket, pont úgy, mint egy standard slidernél. A vizuális réteg viszont megmarad a te egyedi körkörös vezérlődnek. Ez a minta ideális játékok, kreatív alkalmazások és bármilyen egyedi grafikai UI esetén. Fontos: mindig a legszorosabban illeszkedő szabvány komponenst választd. Egy egyedi „csillagos értékelés" vezérlőhöz jobb egy Picker egyediszámú opcióval, mint egy Slider.

Mik az iOS 26 akadálymentesítési újdonságai?

Az iOS 26 három nagyobb rendszerszintű újítást hozott, amikkel a SwiftUI fejlesztőknek foglalkozniuk kell. Először: az Accessibility Reader egy rendszerszintű fókuszált olvasási mód, ami bármilyen szövegblokkot előhív egy zavartalan, testreszabott olvasási felületen. Ahhoz, hogy a rendszer helyesen ismerje fel a szöveg típusát, a .accessibilityTextContentType() módosítóval jelöljük meg: .narrative hosszabb cikkeknek, .sourceCode a kódrészleteknek, .plain pedig alapértelmezésnek. Ez segít az Accessibility Readernek helyesen hangosítani a szöveget és az eltérő címsorokat.

ScrollView {
    Text(article.body)
        .accessibilityTextContentType(.narrative)
        .accessibilityHeading(.h1)

    ForEach(article.codeSnippets) { snippet in
        Text(snippet.code)
            .font(.system(.body, design: .monospaced))
            .accessibilityTextContentType(.sourceCode)
    }
}

Másodszor: a Braille Access a rendszerszintű braille-billentyűzet, ami harmadik féltől származó Braille kijelzőkkel is kompatibilis. Ha az appod egyedi szövegbeviteli mezőket használ, gondoskodj róla, hogy a TextField-jeid pontos accessibilityLabel-lel és textContentType (pl. .emailAddress) beállítással rendelkezzenek. A Braille Access ezek alapján adja a kontextust. Harmadszor: az iOS 26 kiterjeszti a Zoom Action támogatását, amit a VoiceOver-felhasználók a képek és térképek nagyítására használhatnak. A saját map view-nkban ezt a .accessibilityZoomAction { direction in ... }-nel implementáljuk. A hivatalos Apple Human Interface Guidelines Accessibility fejezete részletezi a rendszerszintű beállítások listáját.

Hogyan tesztelhetem az akadálymentességet SwiftUI-ban?

Az akadálymentesítés tesztelése három szintet ölel fel: a fejlesztői gépen az Accessibility Inspector (Xcode → Open Developer Tool), a szimulátoron a VoiceOver aktiválása (Beállítások → Akadálymentesség), és a CI-ben automatizált Accessibility Audit tesztek. Az Inspector Audit funkciója egy kattintással ellenőrzi a képernyőt kontraszt, dinamikus típus, hit target méret és címke szempontból. Az utolsó appban, amit App Store-ba küldtem, ez a tesztkészlet fogott ki egy hibás accessibilityLabel-t 40 perccel a build indítása előtt (megmenekültünk egy újabb review körtől).

import Testing
@testable import MyApp

@Suite("ProfileView Accessibility")
struct ProfileAccessibilityTests {
    @Test("A profil képernyő minden auditot átmegy")
    func performsAccessibilityAudit() async throws {
        let app = XCUIApplication()
        app.launch()
        app.buttons["Profil"].tap()

        try app.performAccessibilityAudit(for: [
            .contrast,
            .dynamicType,
            .elementDetection,
            .hitRegion,
            .textClipped,
            .trait
        ])
    }
}

A performAccessibilityAudit(for:) egy XCUIApplication-on futtatható metódus, amely az XCTest keretrendszerben elérhető, de a Swift Testing @Test makróval is kombinálható. Az egyes audit típusok (.contrast, .dynamicType, .hitRegion, .trait) pontosan azt ellenőrzik, amit a nevük sugall. Például a .hitRegion hibát dob, ha egy interaktív elem 44×44 pontnál kisebb tap targettel rendelkezik. A Swift Testing útmutatónkban részletesen tárgyaljuk a modern XCTest → Swift Testing migrációt. A performAccessibilityAudit hivatalos dokumentációja tartalmazza a támogatott auditok teljes listáját.

A gyakorlatban futtasd az auditokat CI-ben minden PR-nél. Így a regressziók (pl. valaki elfelejt egy accessibilityLabel-t) még a merge előtt kiderülnek. A szemi-automatikus mértéket egészítsd ki manuális VoiceOver tesztekkel a legfontosabb user flow-kra: bejelentkezés, keresés, vásárlás. Kapcsold be a szimulátoron a VoiceOver-t (Beállítások → Akadálymentesség → VoiceOver), és próbáld végigcsinálni a flow-t kizárólag gesztusokkal. Ez a legjobb sanity check, amit ismerek.

Gyakori kérdések

Miért fontos az akadálymentesség iOS alkalmazásokban?

Az App Store ökoszisztémájában több mint 1 milliárd akadálymentesítést használó felhasználó van. Az akadálymentesítés az App Store Review Guidelines része, jogilag kötelező egyes régiókban (pl. EU European Accessibility Act 2025-től), és a Human Interface Guidelines alaptémája. Emellett az akadálymentes appok egyszerűbben tesztelhetők, gyorsabban bővíthetők, és jellemzően jobb UI-val rendelkeznek minden felhasználó számára.

Hogyan kombinálhatok több nézetet egyetlen accessibility elemmé?

Használd a .accessibilityElement(children: .combine) módosítót a szülő nézeten. Ez a gyerekek label-jeit egyetlen olvasható stringgé fűzi. A .contain mód ellenben megőrzi a gyerekek külön elemekként való navigálhatóságát, de csoportosítja őket egy szemantikus konténerbe (hasznos pl. egy CardView-nál, ahol a fejléc és a body külön-külön is elérhető kell legyen).

Mi a különbség az accessibilityLabel és az accessibilityHint között?

A label azt mondja meg, mi az elem, például „Kedvenc" vagy „Bezárás". A hint azt közli, mi történik, ha aktiválod, például „Kettős koppintással hozzáadja a kedvencekhez". A VoiceOver a label-t azonnal olvassa, a hint-et pedig egy rövid szünet után, csak akkor, ha a felhasználó nem lép tovább.

Mit tehetek, ha egy egyedi vezérlőt a VoiceOver nem ismer fel?

Két megközelítés van. Az első: állítsd be manuálisan az accessibilityLabel, accessibilityValue, accessibilityTraits és accessibilityAdjustableAction értékeket. A második: használd az accessibilityRepresentation(representation:)-t, amivel egy standard SwiftUI komponenst (pl. Slider-t) adsz proxy-ként a VoiceOvernek. A második ajánlott, mert kevesebb kódot igényel és a jövőbeli iOS verziókban is stabilabb.

Kötelező a Dynamic Type támogatása az App Store-ban?

Nem kifejezetten kötelező, de az App Review kifejezetten figyeli, és az európai European Accessibility Act 2025-től számos kereskedelmi appra kötelezettséget ír elő. A Dynamic Type figyelmen kívül hagyása negatív review-kat és rossz retenciót eredményez. Rövid szabály: soha ne használj fix méretű fontot, mindig Font.TextStyle-t vagy @ScaledMetric-et.

Ava Thompson
A Szerzőről Ava Thompson

SwiftUI engineer focused on declarative animations and accessibility. Will fight you about navigation stacks.