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?
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.
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()
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With