Just published my first Swift article of 2023! 🎉

This one is about how the content offset (or scroll position) of a SwiftUI ScrollView can be observed without requiring any UIKit bridging. Very useful when implementing things like collapsable headers, or when performing other kinds of scroll position-dependent operations 👍

https://www.swiftbysundell.com/articles/observing-swiftui-scrollview-content-offset

Observing the content offset of a SwiftUI ScrollView | Swift by Sundell

How the content offset of a SwiftUI ScrollView can be observed without bridging to UIKit.

Swift by Sundell
@johnsundell This is really helpful, thanks so much! Looking forward to playing with it.
@majelbstoat Cheers, glad you’re finding the article useful 🙂

@johnsundell so, in playing around with it, this didn’t actually work for me until I removed value = nextValue().

I don’t understand why, and it’s possible I was doing something wrong in the downstream usage, but nextValue was always generating a zero (and the inout value was already the correct number).

@majelbstoat Mind sharing your code with me as a Gist or something? Would be happy to take a look to figure out what’s going on 🙂

@johnsundell sure thing 🙂

https://gist.github.com/majelbstoat/88dcdd95ab493677ff789d827daf366f

It’s essentially your exact code, replacing gradients for solid colors, and commenting out that line.

The Preview in the second file should demonstrate the behavioral difference.

CollapsibleHeaderView.swift

GitHub Gist: instantly share code, notes, and snippets.

Gist
@majelbstoat Seems to be a preview bug. Also can’t make it work in when using a preview, but your code runs as expected in the simulator. In general, I’d advise against using previews to build these kinds of interactions, and only use them when developing individual components. It’s very common to run into these kinds of issues otherwise.

@johnsundell Interesting. So, as you say, that test works in simulator, but not in preview.

But, a different view doesn’t work in simulator without removing that line. That’s a screen where the content is wrapped in your AsyncContentView, as it happens ;)

I updated the gist with a TestViews file that shows it happening for TestingAsyncContentView in the simulator.

(Simulator 16.2 on iPhone Pro 14)

@johnsundell @majelbstoat I ran into this exact same issue in the simulator as well, and it only started working when I removed the reduce
@johnsundell This is great John…I just spent a few hours yesterday doing this for a ScrollView. For my use case I have a Grid with GridRows in it and I wanted to track which row was at the top of the view, transforming scroll offset to GridRow index. This was really hard to do in SwiftUI but with a well-placed GeometryReader I have it working well enough for now but it’s customized for my view…wondering if it’s possible to generalize it?
@johnsundell thanks for the interesting article! There is a small typo in ”thanks to its delete protocol” 😊
@shukuyen Fixed, thanks a lot! 🙌
@johnsundell is there any way to set the exact content offset in SwiftUI? I've been stuck with a bug where I need to restore exact scroll position, but it seems like SwiftUI may not be capable of this
@simsaens Unfortunately not, you’ll have to wrap UIScrollView for that. Using ScrollViewReader you can only scroll to a view with a specific ID, not to an exact position.
@johnsundell thank you, good to know I can stop investigating that avenue
@simsaens @johnsundell I haven’t tried yet, but what about adding hidden views for every point, give them an ID, and then use that to scroll to?
@tobiasdm @simsaens I wouldn’t recommend doing that, as that’d make your view graph significantly more complex. Consider a 3000 point tall scroll view, you’d be adding 3000 extra views just for the purpose of being able to scroll to them. At that point, I’d much rather wrap UIScrollView (which I know I said in yesterday’s article that it’s complicated to do, but it’s not *that bad*). In general, I vastly prefer using wrapped UIKit views over hacking SwiftUI.
@johnsundell @tobiasdm @simsaens and what about a single “scroll destination” view, positioned at the place where you want the scroll to be?
@emiliopelaez @johnsundell @tobiasdm in this case the user can scroll anywhere, and I want to restore that position later when they come back to that screen
@emiliopelaez @johnsundell @tobiasdm oh I think I understand, position the view where the user last left the scroll offset. Interesting!
@simsaens @emiliopelaez @tobiasdm Oh yeah, that’s a very interesting solution indeed! Would love to hear how that works out if you end up trying that 😀
@johnsundell I’ve done something like this in the past, but it resulted in really bad scroll performance for complex scroll view content. In short, we profiled it and realized that with every update of the offset, we were causing cascading body re-computations. Do you know if this variant has this problem?
@schrismartin yeah that’s a typical tradeoff with SwiftUI in general, since every time any view state is changed, that view’s body needs to be re-evaluated. So same thing here. Those performance issues can often be addressed, though, using tools like EquatableView, and by ensuring that ObserableObjects aren’t re-instantiated. But for very complex scroll view layouts that also adapt according to the content offset, it might be worth using UIScrollView instead.
@johnsundell can’t seem to get it to work for contentSize, using the same pattern. Works for static content but all I get using ForEach inside the scrollview is (0,0)