SwiftUI ScrollView iOS 26: scrollTransition, scrollPosition és scrollTargetLayout mesterfokon

Lépésről lépésre vezetlek végig az iOS 26 ScrollView újdonságain: scrollTransition fázisok, programozott pozíció, scrollTargetLayout, contentMargins, paginált görgetés és teljesítményoptimalizálás valós kódpéldákkal.

SwiftUI ScrollView iOS 26: Útmutató 2026

Bevallom, évekig tartott, mire megbarátkoztam azzal, hogy a SwiftUI ScrollView nem csak egy buta görgethető doboz. Az iOS 17 óta lassú forradalom zajlik a háttérben, és iOS 26 alatt végre érettségit ír olyan modifier-csapat, mint a scrollTransition, a scrollPosition, a scrollTargetLayout és a contentMargins. Ebben az útmutatóban konkrét, 2026-ban is működő mintákon keresztül mutatom meg, hogyan építhetsz olyan görgetési élményt, amilyet korábban csak UIKit-tel és kézzel hegesztett UIScrollViewDelegate implementációval volt esélyed összehozni.

Miért érdemes most újragondolni a ScrollView-t iOS 26 alatt?

A régi ScrollView főleg arra volt jó, hogy kirakja a tartalmat — a pozíció lekérdezéséhez GeometryReader és preferenceKey akrobatika kellett. Őszintén? Ez minden komolyabb projektben fájdalom volt. Az új API-k a következő problémákat oldják meg natívan:

  • Vizuális effektek görgetés közben — egyetlen modifierrel skálázás, fakulás és parallax élmény.
  • Programozott pozícionálás — pontos görgetés egy item ID-ra, edge-re vagy offsetre, kétirányú binding-gal.
  • Pattogás és pagination — natív snap viselkedés a scrollTargetLayout és scrollTargetBehavior párossal.
  • Biztonságos margók — a contentMargins szépen szétválasztja a tartalmi és a görgetősáv-margókat.

A 2026-os iOS 26.x és a Liquid Glass design nyelv mellett ezek az API-k kifejezetten hasznosak. A görgetést érzékelő üveghatások ugyanis csak akkor működnek igazán jól, ha a ScrollView részletesen tudósít a saját fázisairól.

1. Alap ScrollView felépítése iOS 26-ban

Mielőtt belevetnénk magunkat az új modifierekbe, érdemes egy pillanatra megállni az alapoknál. A ScrollView belsejébe tett LazyVStack ma is a leghatékonyabb minta — csak a látható elemeket példányosítja, és az új API-k nagy része is ezen a párosításon keresztül működik a legjobban:

struct FeedView: View {
    let posts: [Post]

    var body: some View {
        ScrollView(.vertical) {
            LazyVStack(spacing: 16) {
                ForEach(posts) { post in
                    PostCard(post: post)
                }
            }
            .scrollTargetLayout()
        }
        .contentMargins(.horizontal, 16, for: .scrollContent)
    }
}

Két dologra figyelj. A scrollTargetLayout() a belső konténerre kerül (nem a ScrollView-ra — ez egy nagyon gyakori melléfogás), a contentMargins pedig a .scrollContent régiót célozza, így a görgetősáv megőrzi a saját margóit.

Mikor használj LazyVStack-et és mikor sima VStack-et?

Ha a tartalom 30 elem alatti, és nincs benne nehéz AsyncImage vagy chart, a sima VStack akár gyorsabb is lehet — a layout nem inkrementális, így nincs külön példányosítási overhead. 50 elem felett viszont (különösen képek mellett) a LazyVStack jelentős memória- és CPU-előnyt ad. Ráadásul item-ID alapú pontos pozíciót csak így kapsz.

2. scrollTransition: vizuális effektek a görgetés fázisaihoz

A scrollTransition(_:axis:transition:) modifier (iOS 17+) a látókörbe lépő és onnan kilépő nézetekre alkalmaz folyamatosan animált effekteket. Az iOS 26-ban a VisualEffect protokoll végre stabilizálódott, és lényegesen több modifier használható benne biztonságosan.

A három transition fázis

A closure egy ScrollTransitionPhase értéket kap. Három állapot létezik:

  • .identity — a nézet teljesen látható, érték: 0.
  • .topLeading — felülről vagy bal szélről közelít, érték: -1.
  • .bottomTrailing — alulra vagy jobb szélre tart, érték: 1.

Klasszikus „halványodj és zsugorodj" effekt

ScrollView {
    LazyVStack(spacing: 20) {
        ForEach(items) { item in
            ItemRow(item: item)
                .scrollTransition(.animated.threshold(.visible(0.3))) { content, phase in
                    content
                        .opacity(phase.isIdentity ? 1 : 0.4)
                        .scaleEffect(phase.isIdentity ? 1 : 0.85)
                        .blur(radius: phase.isIdentity ? 0 : 4)
                }
        }
    }
}

A .threshold(.visible(0.3)) azt jelenti, hogy a transition akkor zárul le, amikor a nézet 30%-a látható. Ez gyakorlatilag elvágja azt a kínos „hirtelen ugrás" érzést a felső és alsó vágódásnál.

Aszimmetrikus transition: csak a felső szélen animálj

Néha tényleg csak az egyik irányban szeretnél effektet — mondjuk a fejlécbe tűnő kártyáknál. A .animated helyett megadhatsz külön topLeading és bottomTrailing viselkedést a fázis vizsgálatával:

.scrollTransition(axis: .vertical) { content, phase in
    content
        .opacity(phase == .topLeading ? 0 : 1)
        .offset(y: phase == .topLeading ? -40 : 0)
}

Pontos vezérlés a phase.value-val

Folyamatos parallax-szerű effekthez közvetlenül a phase.value (-1.0 … 1.0) értékét használd:

.scrollTransition { content, phase in
    content
        .rotation3DEffect(
            .degrees(phase.value * 25),
            axis: (x: 1, y: 0, z: 0)
        )
        .offset(y: phase.value * 30)
}

Fontos megkötés: a scrollTransition closure-ben csak olyan modifier használható, amely nem változtatja meg a tartalom méretét. Tehát nem támogatott pl. a .font() vagy a .padding(). A fordító ezt nem mindig kapja el — csak futás közben fognak ignorálódni a változtatások (én ezen már elvesztegettem egy fél délutánt egy WWDC után, szóval most te is okos lehetsz a károm árán).

3. scrollPosition: programozott görgetés és pozíció-binding

Az iOS 17-ben bevezetett scrollPosition(id:) az iOS 18 óta általánosabbá vált, az iOS 26-ban pedig egy új ScrollPosition érték köré szerveződik, ami egyszerre tárolja az ID-t, az edge-et és az offsetet. Lényegében: végre nem kell három különböző API között zsonglőrködni.

Pozíció figyelése item-ID alapján

struct ChapterReader: View {
    let chapters: [Chapter]
    @State private var visibleChapterId: Chapter.ID?

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 24) {
                ForEach(chapters) { chapter in
                    ChapterView(chapter: chapter)
                        .id(chapter.id)
                }
            }
            .scrollTargetLayout()
        }
        .scrollPosition(id: $visibleChapterId)
        .navigationTitle(visibleChapterId.flatMap { id in
            chapters.first(where: { $0.id == id })?.title
        } ?? "Olvasó")
    }
}

A binding minden olyan görgetésnél frissül, amikor a leadingmost nézet azonosítója megváltozik. Tökéletes a fejléc szinkronban tartására.

Programozott ugrás konkrét elemre

Button("Ugrás az 5. fejezetre") {
    withAnimation(.smooth) {
        visibleChapterId = chapters[4].id
    }
}

Edge és offset alapú pozícionálás

Az iOS 26 alatt a teljes ScrollPosition értéket is használhatod:

@State private var position = ScrollPosition(edge: .top)

ScrollView {
    // ...
}
.scrollPosition($position)

Button("Vissza a tetejére") {
    withAnimation(.snappy) {
        position.scrollTo(edge: .top)
    }
}

A scrollTo(edge:), a scrollTo(id:), és a scrollTo(point:) között a felhasználói szándék szerint válassz — nincs „univerzális helyes" választás, mindegyiknek megvan a maga ideális használata.

4. scrollTargetLayout és scrollTargetBehavior: natív paginálás

Talán a legtöbbet kihagyott trükk a görgetési viselkedés deklaratív leírása. A scrollTargetBehavior(.paging) teljes oldalas pattogást ad, a .viewAligned pedig minden gyermek elemre szépen ráigazodik a görgetés végén.

ScrollView(.horizontal) {
    LazyHStack(spacing: 12) {
        ForEach(stories) { story in
            StoryCard(story: story)
                .frame(width: 280, height: 360)
        }
    }
    .scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
.contentMargins(.horizontal, 24, for: .scrollContent)
.scrollIndicators(.hidden)

Ez a kombináció az App Store kártyás listáihoz hasonló élményt ad — körülbelül tíz sor kódból. UIKit-ben ugyanez egyedi UICollectionViewLayout alosztályt igényelt volna, ami (őszintén) önmagában egy hét meló.

Egyéni snap-viselkedés

Ha a .viewAligned nem elég, implementálhatsz saját ScrollTargetBehavior-t:

struct StepSnap: ScrollTargetBehavior {
    let step: CGFloat

    func updateTarget(_ target: inout ScrollTarget, context: TargetContext) {
        let snapped = (target.rect.minY / step).rounded() * step
        target.rect.origin.y = snapped
    }
}

ScrollView { /* ... */ }
    .scrollTargetBehavior(StepSnap(step: 120))

5. contentMargins és safeAreaPadding: tiszta layout-elválasztás

A contentMargins az egyik legfontosabb újdonság, ha pontos elrendezést akarsz. A klasszikus .padding() magát a ScrollView-t (igen, az indikátorokkal együtt) eltolja, ami ronda. A contentMargins ellenben külön kezeli a tartalmi régiót és a görgetősávot:

ScrollView {
    content
}
.contentMargins(.top, 12, for: .scrollContent)
.contentMargins(.top, 0, for: .scrollIndicators)

Ezzel a tartalom 12 ponttal lejjebb kezdődik, de a görgetősáv pontosan a felső szélnél indul. Apró trükk, de a végeredményen sokat dob.

6. onScrollGeometryChange: pontos görgetésfigyelés

Az iOS 18-ban bevezetett onScrollGeometryChange(for:of:action:) modifierrel pontos offset, méret vagy láthatóság alapján reagálhatsz görgetésre — anélkül, hogy a GeometryReader drága frame-mérését használnád:

ScrollView {
    content
}
.onScrollGeometryChange(for: Bool.self) { geometry in
    geometry.contentOffset.y > 80
} action: { _, isPastThreshold in
    withAnimation(.snappy) {
        showCondensedHeader = isPastThreshold
    }
}

A trükk itt: a for: paraméter egy Equatable származtatott érték, így az action closure csak akkor fut le, ha a számított érték ténylegesen változott. Ez óriási teljesítményelőny minden frame-rendelte futtatáshoz képest — különösen ProMotion eszközökön számít.

7. Teljesítményoptimalizálás iOS 26-ban

A görgetés a leggyakoribb felhasználói művelet — a 120 Hz-es ProMotion kijelzők (8.3 ms / frame) miatt minden milliszekundum számít. Pár olyan dolog, amit én is rendre elfelejtek:

  • Részesítsd előnyben a transzformokat a layout helyett. A .scaleEffect() és az .opacity() a fő szálon kívül fut, míg a .padding() új layoutot kényszerít.
  • Stabil id a ForEach-ben — anélkül a SwiftUI minden görgetésnél újraépíti a struktúrát. Itt fordult már elő, hogy egy „véletlen" UUID() hívás 60 fps-ről 28-ra rántott le egy listát.
  • Drawing group nehéz Canvas-hoz — a .drawingGroup() Metal-alapú renderelést kényszerít.
  • onScrollGeometryChange > preferenceKey — a deklaratív API maximum egyszer fut frame-enként.
  • Animáció scope-olása — kerüld a .animation(.default) globális használatát; használj inkább .animation(.smooth, value: state) formát.

8. Hozzáférhetőség: ne hagyd ki a Reduce Motion ellenőrzést

A scrollTransition alapból akkor is animál, ha a felhasználó kikapcsolta a mozgáshatásokat. Tartsd tiszteletben a beállítást — nem opcionális:

@Environment(\.accessibilityReduceMotion) private var reduceMotion

ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item: item)
                .scrollTransition { content, phase in
                    content
                        .opacity(reduceMotion ? 1 : (phase.isIdentity ? 1 : 0.5))
                        .scaleEffect(reduceMotion ? 1 : (phase.isIdentity ? 1 : 0.92))
                }
        }
    }
}

9. Hibakeresés: gyakori csapdák

  • scrollPosition nem frissül — biztosan rátetted a scrollTargetLayout()-ot a belső LazyVStack-re? A ScrollView-on nem fog működni. (Ezt szinte mindenkinél láttam, aki most kezdi.)
  • scrollTransition villog — a closure-ben olyan modifier van, ami a méretet befolyásolja (pl. padding). Cseréld scaleEffect-re.
  • contentMargins nem látszik — meggyőződtél-e róla, hogy nem .frame-et adsz a ScrollView-ra? A frame felülírja a margók hatását.
  • Lassú görgetés AsyncImage-zsel — explicit cache vagy onAppear alapú prefetch kell; az iOS 26 nem cache-eli alapból.

10. Mintapélda: olvasói felület scrollTransition + scrollPosition kombinációval

Na, akkor rakjuk össze az egészet. Az alábbi minta kombinálja a fázis-alapú effekteket, a programozott pozíciót és a görgetésfigyelést — gyakorlatilag egy kerek olvasói nézet alapja:

struct ArticleReader: View {
    let blocks: [ContentBlock]
    @State private var position = ScrollPosition(edge: .top)
    @State private var showJumpToTop = false

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 16) {
                ForEach(blocks) { block in
                    BlockView(block: block)
                        .scrollTransition(.animated.threshold(.visible(0.2))) { content, phase in
                            content
                                .opacity(phase.isIdentity ? 1 : 0.5)
                                .blur(radius: phase.isIdentity ? 0 : 6)
                        }
                }
            }
            .scrollTargetLayout()
        }
        .scrollPosition($position)
        .contentMargins(.horizontal, 20, for: .scrollContent)
        .onScrollGeometryChange(for: Bool.self) { geo in
            geo.contentOffset.y > 600
        } action: { _, isFar in
            withAnimation(.snappy) { showJumpToTop = isFar }
        }
        .overlay(alignment: .bottomTrailing) {
            if showJumpToTop {
                Button {
                    withAnimation(.smooth) {
                        position.scrollTo(edge: .top)
                    }
                } label: {
                    Image(systemName: "arrow.up")
                        .font(.title2)
                        .padding(14)
                        .background(.ultraThinMaterial, in: .circle)
                }
                .padding(20)
                .transition(.scale.combined(with: .opacity))
            }
        }
    }
}

GYIK — Gyakori kérdések a SwiftUI ScrollView iOS 26 alatt

Mit ad az iOS 26 ScrollView az iOS 17/18-hoz képest?

Az iOS 26 stabilizálja a VisualEffect protokollt, kibővíti a ScrollPosition értéket (egységes ID/edge/offset kezelés), és bevezeti a @Animatable makrót, ami a scroll-vezérelt egyedi animációknál sokat lefarag a boilerplate-ből. Teljesítmény oldalon az onScrollGeometryChange opcionális egyszer-frame koaleszcenciát kapott.

Hogyan érek el sima paginálást SwiftUI-ban a TabView nélkül?

Tedd a tartalmat egy LazyHStack-be a ScrollView-on belül, alkalmazz scrollTargetLayout()-ot a stack-re, majd a ScrollView-ra scrollTargetBehavior(.paging)-et. Ez a TabView-nál rugalmasabb (nem csak teljes oldalakra korlátozódik), és minden iOS 17+ készüléken működik.

Miért nem működik a scrollTransition padding modifierrel?

A scrollTransition closure csak olyan effekteket fogad, amelyek nem módosítják a layoutot — ez a VisualEffect protokoll megkötése. A padding, a frame, vagy a font változtatása újrarendezné a tartalmat, ami megakadná a görgetést. Cseréld scaleEffect, offset vagy opacity modifierre.

Hogyan tartom szinkronban a fejlécet a látható elemmel?

Használj scrollPosition(id:)-t egy State bindingra, és a fejlécben olvasd ki ezt az ID-t. A binding automatikusan frissül a leadingmost nézet váltásakor. Ha a fejléc-frissítés „ugrik", csomagold withAnimation(.smooth)-ba.

Mikor használjak GeometryReadert ScrollView-on belül 2026-ban?

Szinte soha. Az onScrollGeometryChange, scrollPosition és scrollTransition trió 95%-os lefedettséget ad az új projektekben. GeometryReader csak akkor indokolt, ha egyedi méretű háttérréteget kell pontosan a ScrollView tartalmához igazítani — például egy parallax kép esetén, ami a teljes tartalmi régiót takarja.

Összegzés

Az iOS 26 SwiftUI ScrollView API-jai elhozták azt a kifejezőerőt, amelyre évek óta vártunk. A négy modifier — a scrollTransition, a scrollPosition, a scrollTargetLayout és a contentMargins — együtt használva olyan görgetési élményt ad, amilyet korábban csak custom UIKit kóddal lehetett elérni.

Az én javaslatom: kezdj a LazyVStack + scrollTargetLayout alapokkal, építsd rá a pozíció-bindingot, majd finomítsd a scrollTransition effekteket. És kérlek — tényleg kérlek — tartsd tiszteletben az accessibilityReduceMotion beállítást. Az effekt, ami valakit szédít, nem cool, csak rossz UX.

A Szerzőről Priya Raghavan

Priya spent six years at Instacart building the iOS shopper app, where she led the migration from UIKit to SwiftUI across 80+ screens and cut crash-free sessions from 99.2% to 99.87%. Before that, she was a contractor at a Bay Area design studio shipping App Store apps for two Fortune 500 retail clients. She focuses on practical SwiftUI architecture - what holds up when you have 12 engineers committing to the same codebase, not just toy MVVM examples. Her recent work involves The Composable Architecture, Swift concurrency migration audits, and reducing main-thread hangs on older devices like the iPhone XR that enterprise fleets still ship. Priya runs a small consultancy in Oakland and occasionally speaks at try! Swift NYC. She has been writing Swift since the Objective-C bridging days of 2015.