Last active
December 16, 2020 17:35
-
-
Save pteasima/2891a39f2b1923fd254c0d7884367a1e to your computer and use it in GitHub Desktop.
SwiftUI onScroll
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Yes, very outdated. I havent used this recently, but I worry if setting the binding value from the GeometryReader might cause a "setting state from body" warning. If you have any trouble, you should use a custom PreferenceKey (I didnt no this at the time)
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
thanks for the code snippet, it's a bit outdated but the logic is there 👍