Published on

iOS 17'da SwiftUI'ning yangi ScrollView modifikatorlari: Paging, containerRelativeFrame, scrollTransition

Authors

iOS 17 SwiftUI'ga ScrollView uchun bir nechta kuchli yangi modifikator qo'shdi. Ular TikTok va Instagram Reels kabi to'liq ekranli paging, App Store'dagi gorizontal pager, va silliq animatsiyali scroll o'tishlar yaratishga imkon beradi — bularning barchasi oldin SwiftUI'da mavjud bo'lmagan edi.

Talab: iOS 17+


Boshlang'ich tuzilma

@available(iOS 17, *)
struct ScrollViewPagingBootcamp: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 30) {
                ForEach(0..<10) { index in
                    Rectangle()
                        .frame(width: 300, height: 300)
                        .overlay(
                            Text("\(index)")
                                .foregroundColor(.white)
                        )
                }
            }
        }
    }
}

1. scrollBounceBehavior — bounce'ni boshqarish

Standart holda scroll view har doim "bouncing" (sakrash) effektiga ega. Faqat kontent scroll view hajmidan katta bo'lganda bouncing bo'lishini xohlasak:

ScrollView {
    ...
}
.scrollBounceBehavior(.basedOnSize)
  • .always — har doim bounce
  • .basedOnSize — faqat kontent scroll view hajmidan katta bo'lsa bounce
  • .automatic — standart (har doim bounce)

Kichik ro'yxatlar uchun .basedOnSize — eng qulay tanlov.


2. scrollTargetLayout + scrollTargetBehavior — to'xtash nuqtasini boshqarish

Standart holda scroll view fizika qonunlariga muvofiq istalgan joyda to'xtaydi. Buni o'zgartirish uchun:

ScrollView {
    VStack(spacing: 30) {
        ForEach(0..<10) { index in
            Rectangle()
                .frame(width: 300, height: 300)
        }
    }
    .scrollTargetLayout()          // qaysi layout bo'yicha hizalanadi
}
.scrollTargetBehavior(.viewAligned) // yoki .paging
  • .viewAligned — scroll to'xtaganda eng yaqin elementning yuqori qismi ekran boshiga hizalanadi
  • .paging — TikTok uslubi: har bir sürükleme bir "sahifaga" o'tadi

3. containerRelativeFrame — TikTok pager (to'liq ekranli)

Paging'ni to'liq ekranli qilish uchun har bir katak scroll view hajmiga to'liq mos bo'lishi kerak:

ScrollView {
    VStack(spacing: 0) {          // spacing = 0 muhim!
        ForEach(0..<10) { index in
            Rectangle()
                .ignoresSafeArea()
                .containerRelativeFrame(.vertical)  // vertikal o'qqa to'ldiradi
                .overlay(
                    Text("\(index)")
                        .foregroundColor(.white)
                )
        }
    }
    .scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
.ignoresSafeArea()               // scroll view ham safe area'ni e'tiborsiz qoldirsind

Muhim: Spacing katakka tashqarida emas, ichida berilishi kerak (padding sifatida), aks holda hizalanish buziladi:

Rectangle()
    .containerRelativeFrame(.vertical)
    .padding(.vertical, 10)       // ichida padding — to'g'ri

4. Gorizontal pager

ScrollView(.horizontal) {
    HStack {
        ForEach(0..<10) { index in
            RoundedRectangle(cornerRadius: 10)
                .frame(width: 300, height: 300)
                .padding(10)                          // spacing ichida
                .containerRelativeFrame(.horizontal)  // gorizontal o'qqa to'ldiradi
                .overlay(
                    Text("\(index)")
                        .foregroundColor(.white)
                )
        }
    }
    .scrollTargetLayout()
}
.scrollTargetBehavior(.paging)   // yoki .viewAligned

.viewAligned bilan containerRelativeFrame olib tashlasak ham, scroll to'xtaganda eng yaqin katakka avtomatik hizalanadi.


5. scrollPosition — dasturiy scroll

@State private var scrollPosition: Int? = nil

VStack {
    Button("Random scroll") {
        withAnimation(.smooth) {
            scrollPosition = (0..<20).randomElement()
        }
    }

    ScrollView(.horizontal) {
        HStack {
            ForEach(0..<20) { index in
                RoundedRectangle(cornerRadius: 10)
                    .frame(width: 300, height: 300)
                    .id(index)                       // ID element bilan mos
            }
        }
        .scrollTargetLayout()
    }
    .scrollPosition(id: $scrollPosition, anchor: .center)
    .scrollTargetBehavior(.viewAligned)
}

id turi scrollPosition turi bilan mos bo'lishi kerak — bu yerda ikkalasi ham Int. Identifiable model ishlatilsa — model turi beriladi.


6. scrollTransition — kataklarning animatsiyali paydo bo'lishi

Kataklarni scroll'da ekranga chiqayotganda animatsiya:

Rectangle()
    .frame(width: 300, height: 300)
    .scrollTransition { content, phase in
        content
            .opacity(phase.isIdentity ? 1 : 0)
            .offset(y: phase.isIdentity ? 0 : -200)
    }

Phase holatlari:

  • .identity — katak to'liq ekranda
  • .topLeading — ekranning yuqori/chap tomonidan kirmoqda
  • .bottomTrailing — ekranning pastki/o'ng tomonidan chiqmoqda

phase.isIdentity — katak to'liq ko'rinadimi?


scrollTransition konfiguratsiyasi

.scrollTransition(.interactive) { content, phase in    // har bir pixel
    content.opacity(phase.isIdentity ? 1 : 0)
}

.scrollTransition(.animated) { content, phase in       // bir marta "yonadi/o'chadi"
    content.opacity(phase.isIdentity ? 1 : 0)
}

.scrollTransition(.identity) { content, phase in       // animatsiya yo'q
    content
}

Threshold — qachon animatsiya boshlansing

.scrollTransition(.interactive(threshold: .visible(0.9))) { content, phase in
    content.opacity(phase.isIdentity ? 1 : 0)
}
  • .visible(0.1) — 10% ko'rinaydigan bo'lishi bilanoq animatsiya boshlanadi
  • .visible(0.9) — 90% ko'rinaydigan bo'lganda animatsiya tugaydi (uzoqroq davom etadi)

Barcha modifikatorlar birgalikda

@available(iOS 17, *)
struct FullExampleView: View {
    @State private var scrollPosition: Int? = nil

    var body: some View {
        VStack {
            Button("Scroll to random") {
                withAnimation(.smooth) {
                    scrollPosition = (0..<10).randomElement()
                }
            }

            ScrollView {
                VStack(spacing: 0) {
                    ForEach(0..<10) { index in
                        Rectangle()
                            .containerRelativeFrame(.vertical)
                            .ignoresSafeArea()
                            .id(index)
                            .scrollTransition(.interactive) { content, phase in
                                content
                                    .opacity(phase.isIdentity ? 1 : 0.3)
                                    .scaleEffect(phase.isIdentity ? 1 : 0.8)
                            }
                            .overlay(
                                Text("\(index)")
                                    .foregroundColor(.white)
                                    .font(.largeTitle)
                            )
                    }
                }
                .scrollTargetLayout()
            }
            .scrollTargetBehavior(.paging)
            .scrollBounceBehavior(.basedOnSize)
            .scrollPosition(id: $scrollPosition)
            .ignoresSafeArea()
        }
    }
}

Xulosa

ModifikatorVazifasi
.scrollBounceBehavior(.basedOnSize)Faqat zarur bo'lganda bounce
.scrollTargetLayout()Layout hizalanish uchun belgilash
.scrollTargetBehavior(.paging)TikTok uslubi paging
.scrollTargetBehavior(.viewAligned)Eng yaqin elementga hizalanish
.containerRelativeFrame(.vertical)Katak to'liq ekranga to'lsin
.scrollPosition(id:)Dasturiy animatsiyali scroll
.scrollTransition { }Ekranga chiqish animatsiyasi

Bu modifikatorlar avval ScrollViewReader, GeometryReader va maxsus kodlar talab qilgan ko'plab UI pattern'larni bir necha qator kodga tushiradi.

Buy mea coffee