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.
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.

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ésscrollTargetBehaviorpárossal. - Biztonságos margók — a
contentMarginsszé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
ida 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éldscaleEffect-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 vagyonAppearalapú 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.

