How do I maintain a stable scroll position when inserting items above in a ScrollView?

As the title says, I am not sure how to properly build an inverted ScrollView where I can safely insert items above my data ("prepend") without everything jumping around.

My current code is essentially this:

@State private var scrollPosition = ScrollPosition(idType: Message.ID.self)
private func onMessageDidScrollIntoView(_ id: Message.ID) {
let indexOfVisibleMessage = /* ... */
if indexOfVisibleMessage < 10 {
fetchOlderMessages()
// ^ this updates my ViewModel `messages`
}
}
var body: some View {
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageCell(message)
}
}.scrollTargetLayout()
}
.defaultScrollAnchor(.bottom)
.scrollPosition($scrollPosition)
.onChange(of: scrollPosition) { oldValue, newValue in
guard let visibleMessageId = scrollPosition.viewID(type: Message.ID.self) else { return }
onMessageDidScrollIntoView(visibleMessageId)
}
}

..so if the user scrolls up to the oldest 10 messages, I start loading more and insert them at the top.

The problem with this is that the ScrollView now jumps when new messages are inserted. This is because the ScrollView maintains it's Y position, but the content size changes since we are adding new items "above".

I tried to play around with a few suggestions I found on StackOverflow, namely;

  • Inverting the ScrollView (.scaleEffect(y: -1) on the ScrollView and then again on the MessageCell to counter it): This somehow jumped the x position of the ScrollView and completely breaks .contextMenu.
  • Playing around with .onScrollGeometryChange to update scrollPosition.scrollTo(y:) when it's contentSize changes: This just didn't work and stopped the user scroll gesture/interaction.
  • Setting scrollPosition to the Message.ID I want to keep stable before doing an update: This didn't do anything.

But nothing actually worked for the reasons described above.

How do you actually build these UIs in SwiftUI? I think an inverted ScrollView is quite a common UI, and obviously data has to be loaded lazily.

How do I maintain a stable scroll position when inserting items above in a ScrollView?
 
 
Q