Skip to content

Instantly share code, notes, and snippets.

@pteasima
Last active December 16, 2020 17:35
Show Gist options
  • Save pteasima/2891a39f2b1923fd254c0d7884367a1e to your computer and use it in GitHub Desktop.
Save pteasima/2891a39f2b1923fd254c0d7884367a1e to your computer and use it in GitHub Desktop.
SwiftUI onScroll
import SwiftUI
import Combine
struct OnScroll: ViewModifier {
@Binding var offset: CGFloat
//we can have a version with a closure instead of the binding, but that triggers an infinite loop if content depends on the same Store
// var onOffset: (CGFloat) -> ()
func body(content: Content) -> some View {
return VStack {
GeometryReader { geometry -> ForEach<Range<Int>, EmptyView> in
let newOffset = geometry.frame(in: .global).minY
// self.onOffset(newOffset)
// //this can trigger a rerender. Checking for equality breaks the infinite loop.
if self.offset != newOffset { self.offset = newOffset }
// for some reason with EmptyView or any other View the GeometryReader never got called, but with empty ForEach it does
return ForEach(0..<0) { _ -> EmptyView in
assertionFailure()
return EmptyView()
}
}
content
}
}
}
final class Store: BindableObject {
let didChange = PassthroughSubject<(), Never>()
var offset: CGFloat = 0 {
didSet {
print(offset)
didChange.send(())
}
}
}
struct ContentView : View {
var body: some View {
MyView()
.environmentObject(Store())
}
}
struct MyView: View {
@EnvironmentObject var store: Store
var body: some View {
return VStack {
// even though initial value got written into the store, this view never rerendered, so this Text says 0 until a scroll happens
Text("\(store.offset)")
ScrollView(alwaysBounceVertical: true) {
// !!! we need to wrap the modified in something. VStack or single element ForEach work just fine.
// If we use the text directly as the root of the ScrollView's content, it doesnt render.
// Whats super strange is that we cant do this wrapping inside the Modifier.
VStack {
ScrollContent(store: store)
.modifier(OnScroll(offset: self.$store.offset))
// .modifier(OnScroll { self.store.offset = $0 })
}
}
}
}
}
struct ScrollContent: View {
//wtf EnvironmentObject wasnt getting injected here even though it worked fine for the other views
// @EnvironmentObject var store: Store
@ObjectBinding var store: Store
var body: some View {
// !!! make sure not to change the content on scroll, else scrolling gets interrupted
//you can print it all you want
print("I can print offset: \(store.offset)")
//but you cant do this, else scrolling breaks
// return Text("\(store.offset)")
return Text("foo")
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
@khanlou
Copy link

khanlou commented May 11, 2020

I got a version working with preferences here: https://gist.github.com/khanlou/112cbb13ee2c776aa343bfc204f78259

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