Skip to content

Instantly share code, notes, and snippets.

@khanlou
Last active October 18, 2022 04:32
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save khanlou/112cbb13ee2c776aa343bfc204f78259 to your computer and use it in GitHub Desktop.
Save khanlou/112cbb13ee2c776aa343bfc204f78259 to your computer and use it in GitHub Desktop.
This ScrollView has a modifier called `onScroll`, which is updated when scrolls occur.
struct ContentView: View {
@State var scrollOffset: CGPoint = .zero
var body: some View {
ObservableScrollView {
Text("Hello, world!")
.foregroundColor(self.scrollOffset.y == 0 ? .blue : .red)
}
.onScroll { self.scrollOffset = $0 }
}
}
struct ScrollOffsetPreferenceKey: PreferenceKey {
typealias Value = CGPoint
static var defaultValue = CGPoint.zero
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
value = nextValue()
}
}
struct ObservableScrollView<Content> : View where Content : View {
var content: Content
var axes: Axis.Set
var showsIndicators: Bool
init(_ axes: Axis.Set = .vertical, showsIndicators: Bool = true, @ViewBuilder content: () -> Content) {
self.content = content()
self.axes = axes
self.showsIndicators = showsIndicators
}
var body: some View {
GeometryReader { outerGeometry in
ScrollView(self.axes, showsIndicators: self.showsIndicators) {
ZStack(alignment: self.axes == .vertical ? .top : .leading) {
GeometryReader { innerGeometry in
Color.clear
.preference(key: ScrollOffsetPreferenceKey.self, value: CGPoint(x: (outerGeometry.frame(in: .global).minX - innerGeometry.frame(in: .global).minX), y: (outerGeometry.frame(in: .global).minY - innerGeometry.frame(in: .global).minY)))
}
VStack {
self.content
}
}
}
}
}
}
extension ObservableScrollView {
func onScroll(_ onScroll: @escaping (CGPoint) -> ()) -> some View {
self.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: onScroll)
}
}
@4leyam
Copy link

4leyam commented May 11, 2020

thank you for sharing, will give it a try 👍

@khanlou
Copy link
Author

khanlou commented May 11, 2020

Fixed a bug, with inspiration from https://medium.com/@maxnatchanon/swiftui-how-to-get-content-offset-from-scrollview-5ce1f84603ec who uses a similar technique

@palanishankar07
Copy link

Thanks for sharing!!!

I did a quick test but found that any layout change causing the infinite loop. for example, offset ObservableScrollView Y position or image height based on offset. is there a way to fix the call back only for content scroll not the frame change?

struct ContentView: View {
@State var scrollOffset: CGPoint = .zero

var body: some View {
    VStack {
        //1
        Image("logo_stjc")
            .resizable()
            .aspectRatio(contentMode: .fill)
            .clipped()
            .frame(height: 400)
        //2
        VStack {
            Image(systemName: "trash")
                .frame(width: 50.0, height: 50.0)
        }
        .frame(width: 200, height: 100)
        //3
        ObservableScrollView {
            VStack(spacing: 20) {
                HStack(spacing: 20){
                    //some vie
                }
            }
        }.onScroll { self.scrollOffset = $0 }
    }
}

}

@khanlou
Copy link
Author

khanlou commented May 25, 2020

@palanishankar07 to be honest, i haven’t been able to get this code to work reliably, so I’ve kind of abandoned it. I’m not getting the body var get called when the user scrolls. Let me know if you have better luck with it!

@ForceGT
Copy link

ForceGT commented Mar 2, 2021

If anyone has been looking for a solution I found this really convenient to use
It has all kinds of customisation available, like making the scrollview a grid scrollview , tracking scroll offset putting a header, adjusting padding etc
It is a brilliant solution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment