5

With the new modifier scrollTargetBehavior(.paging), ScrollViews now get paging behavior. It works great but I've yet to find a way to get the currently displayed view. I've tried using the onAppear on each view, but it doesn't correlate to when the view is displayed, and it's only called once. If you go back to the page it won't be called again.

The view named PagingScrollView contains a GeometryReader to set the frame of each of the displayed child views. The view takes in a function to get the child views to be displayed.

Here is a sample complete with the preview modifier, you can just copy and paste it to xcode.

import SwiftUI

struct PagingScrollView<Content: View>: View {
    var pageCount: Int
    var viewForPage: (Int) -> Content

    var body: some View {
        GeometryReader { geo in
            ScrollView (.horizontal) {
                HStack (spacing: 0) {
                    ForEach(0..<pageCount, id:\.self) { index in
                        viewForPage(index)
                            .frame(width: geo.size.width, height: geo.size.height)
                    }
                }
                .scrollTargetLayout()
            }
            .scrollTargetBehavior(.paging)
        }
    }
}

struct PreviewView: View {
    var body: some View {
        PagingScrollView(pageCount: 10, viewForPage: getView(index:))
    }

    func getView(index: Int) -> some View {
        return Text("View: \(index)")
    }

}

#Preview {
    PreviewView()
}

Is there a way to get the currently displayed view?

2 Answers 2

9

Use the scrollPosition modifier. Be sure to give each view in the HStack an id. Then, create a state variable scrolledID (for example) and add the .scrollPosition(id: $scrolledID) modifier on the ScrollView. In your case:

struct PagingScrollView<Content: View>: View {
    var pageCount: Int
    var viewForPage: (Int) -> Content
    @State var scrolledID: Int?  //  <----

    var body: some View {
        GeometryReader { geo in
            ScrollView (.horizontal) {
                HStack (spacing: 0) {
                    ForEach(0..<pageCount, id:\.self) { index in
                        viewForPage(index)
                            .frame(width: geo.size.width, height: geo.size.height)
                            .id(index)  //  <----
                    }
                }
                .scrollTargetLayout()
            }
            .scrollTargetBehavior(.paging)
            .scrollPosition(id: $scrolledID)  //  <----
        }
    }
}

This modifier can be used independently of scrollTargetBehavior.

Sign up to request clarification or add additional context in comments.

3 Comments

Nice one. This is better than my solution, I'll remove it!
Thank you! This works but for some reason I don't understand, I needed to change the HStack to LazyHStack to make it work. More info here: stackoverflow.com/questions/77164152/…
It didn't work with me.
2

This is what worked with me, considering the scroll view is taking the whole screen width:

struct PagingScrollView<Content: View>: View {
    var pageCount: Int
    var viewForPage: (Int) -> Content
    @State var offsetX: CGFloat = 0.0
    
    var currentPage: Int {
        return Int(round(-offsetX / UIScreen.main.bounds.width))
    }
    
    var body: some View {
        GeometryReader { geo in
            ScrollView (.horizontal) {
                HStack (spacing: 0) {
                    ForEach(0..<pageCount, id:\.self) { index in
                        viewForPage(index)
                            .frame(width: geo.size.width, height: geo.size.height)
                    }
                }
                .read(offsetX: $offsetX)
            }
            .scrollTargetBehavior(.paging)
        }
    }
}

extension View {
    func read(offsetX: Binding<CGFloat>) -> some View {
        self
            .background(
                GeometryReader { geo in
                    Color.clear
                        .preference(key: ViewOffsetXKey.self, value: geo.frame(in: .global).minX)
                }
                    .onPreferenceChange(ViewOffsetXKey.self) { minX in
                        let diff = abs(offsetX.wrappedValue - minX)
                        if diff > 1.0 {
                            offsetX.wrappedValue = minX
                            print("readOffsetX: \(offsetX.wrappedValue)")
                        }
                    }
            )
    }
}

struct ViewOffsetXKey: PreferenceKey {
    static var defaultValue: CGFloat = 0

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

1 Comment

I made just a few changes @State var currentPage: CGFloat = 0.0 Then in the On preference change I updated it like so .onPreferenceChange(ViewOffsetXKey.self) { minX in let width = WKInterfaceDevice.current().screenBounds.width let newOffsetX = minX let newPage = abs(round(newOffsetX / width)) if abs(offsetX.wrappedValue - newOffsetX) > 1.0 { offsetX.wrappedValue = newOffsetX currentPage.wrappedValue = newPage } }

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.