@Frameworks Engineer I've refined your code a bit with suggestions from other answers here to try and explain the question better.
struct ContentView: View {
@State var data: [String] = (0 ..< 25).map { String($0) }
@State var dataID: String?
var body: some View {
ScrollView {
VStack {
Text("Header")
LazyVStack {
ForEach(data, id: \.self) { item in
Color.red
.frame(width: 100, height: 100)
.overlay {
Text("\(item)")
.padding()
.background()
}
}
}
.scrollTargetLayout()
}
}
.scrollPosition(id: $dataID)
.safeAreaInset(edge: .bottom) {
VStack {
Text("\(Text("Scrolled").bold()) \(dataIDText)")
HStack {
Button {
scrollTo(data.first)
} label: {
Label("Top", systemImage: "arrow.up")
.frame(maxWidth: .infinity)
}
Button {
scrollTo(data.last)
} label: {
Label("Bottom", systemImage: "arrow.down")
.frame(maxWidth: .infinity)
}
Menu {
Button("Prepend") {
let items = createMoreItems()
data.insert(contentsOf: items, at: 0)
}
Button("Append") {
let items = createMoreItems()
data.append(contentsOf: items)
}
Button("Remove First") {
data.removeFirst()
}
Button("Remove Last") {
data.removeLast()
}
} label: {
Label("More", systemImage: "ellipsis.circle")
.frame(maxWidth: .infinity)
}
}
}
.background(Material.ultraThin)
}
}
var dataIDText: String {
dataID.map(String.init(describing:)) ?? "None"
}
private func scrollTo(_ dataID: String?,
animation: Animation? = .easeInOut,
position: UnitPoint = .bottom) {
withAnimation(animation) {
withTransaction(\.scrollTargetAnchor, position) {
self.dataID = dataID
}
}
}
private func createMoreItems(count: Int = 10) -> [String] {
return (0..<count).map { String(data.count + $0) }
}
}
#Preview {
ContentView()
}
While most of the times when adding a single item (append
, insert
) the dataID
scrolled position is kept correctly, when adding multiple items (like a page) on top (prepend) it fails to maintain the right content offset, pushing the whole content down.
Notice that on this scenario dataID
won't change its value.
There are a lot of hacky ways to try fix this like transforming the ScrollView
and its subviews, accessing UIScrollView
directly using introspect,, or re-setting dataID
few moments after appending a page, but none of them is robust and most of the times they create weird scroll jumps.
I've also tried to create a custom ScrollTargetBehavior updating the scroll target to the old page content offset, but it works only when new content is added while drugging, and also creates weird scroll jumps.
Unfortunately achieving reverse scroll behavior in SwiftUI 5 feels impossible ATM.