Post

Replies

Boosts

Views

Activity

How can I render a ScrollView so that its ScrollPosition is at the correct ID during layout?
Below you will find an example view of my problem. It has one button that, when pressed, will toggle between two scroll views using withAnimation, setting the scroll position onto the 2nd and 3rd items for either scroll view in onAppear. The intent is to have the background of items within each list transition smoothly from their position in one list, to their position in the other. However, this does not appear to be easily possible when setting the list position using an ID/ScrollPosition: Initializing a ScrollPosition with the correct ID and rendering the ScrollView with that appears to have no effect - the ScrollView will be drawn at the top of the scroll contents The only way I've found to render the ScrollView at an ID position is to scroll to that position in onAppear. However, it appears that when doing so, the matchedGeometryEffect interpolates the position of the elements as if the contentOffset.y of the ScrollView is briefly 0, resulting in a strange effect The desired animation can be seen if the two lists are toggled rapidly, allowing for the matchedGeometryEffect to smooth out the brief y 0 and interpolate between the correct positions It seems I either need to a) ensure the list is laid out at the correct y location beforehand (very difficult with dynamic list items, but seems to solve this problem if setting the y position explicitly) b) ensure that the list is laid out at the correct ID beforehand (have not been able to figure out how) c) ensure the matched geometry effect animation ignores the brief "0" y offset of the ScrollView before setting the ID position in onAppear (have not been able to figure out how) Note that I have to use VStack here for the matched geometry effect to work consistently. Any ideas on solving this? Code: import SwiftUI struct Item: Identifiable { let id = UUID().uuidString var height: CGFloat var label: String } enum TestScrollListStyle { case primary case alternate } struct TestScrollList: View { let items: [Item] let style: TestScrollListStyle let namespace: Namespace.ID @Binding var scrollPosition: ScrollPosition var initialIndex: Int = 2 var body: some View { ScrollView { VStack(spacing: style == .primary ? 8 : 16) { ForEach(items, id: \.id) { item in switch style { case .primary: Text(item.label) .frame(maxWidth: .infinity) .frame(height: item.height) .padding(.horizontal) .background( Rectangle() .fill(.blue.opacity(0.15)) .matchedGeometryEffect(id: item.id, in: namespace) ) case .alternate: HStack { Circle() .fill(.green.opacity(0.25)) .frame(width: 24, height: 24) Text(item.label) .frame(maxWidth: .infinity, alignment: .leading) } .frame(height: item.height) .padding(.horizontal) .background( Rectangle() .fill(.green.opacity(0.08)) .matchedGeometryEffect(id: item.id, in: namespace) ) } } } .scrollTargetLayout() .padding(.vertical) } .scrollPosition($scrollPosition, anchor: .top) .onAppear { var tx = Transaction() tx.disablesAnimations = true withTransaction(tx) { if items.indices.contains(initialIndex) { scrollPosition.scrollTo(id: items[initialIndex].id) } } } } } struct ContentView: View { @Namespace private var matchedNamespace @State private var items: [Item] = (0..<10).map { i in Item(height: .random(in: 80...220), label: "Row \(i)") } @State private var showAlternateView: Bool = false // Scroll positions for each scroll view @State private var primaryScrollPosition = ScrollPosition(idType: String.self) @State private var alternateScrollPosition = ScrollPosition(idType: String.self) var body: some View { NavigationStack { ZStack { if !showAlternateView { TestScrollList( items: items, style: .primary, namespace: matchedNamespace, scrollPosition: $primaryScrollPosition, initialIndex: 2 ) } if showAlternateView { TestScrollList( items: items, style: .alternate, namespace: matchedNamespace, scrollPosition: $alternateScrollPosition, initialIndex: 3 ) } } .navigationTitle("Two ScrollViews + Matched Geometry") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button(showAlternateView ? "Primary" : "Alternate") { withAnimation() { showAlternateView.toggle() } } } } } } } #Preview { ContentView() }
1
0
51
Oct ’25
ScrollView + LazyVStack buggy animation. Fixable?
The below code will render a list of items and a button to shuffle them and change their height withAnimation. If you scroll down and shuffle it you will eventually see glitches in the animation, mostly consisting of ghost items animating on top of other items and disappearing. Does anyone know why this is happening, and how to stop it happening? The issue goes away entirely if I use a VStack, but using a VStack brings its own problems. import SwiftUI struct Item: Identifiable { let id = UUID() var height: CGFloat var label: String } struct ContentView: View { @State private var items: [Item] = (0..<30).map { i in Item(height: .random(in: 80...220), label: "Row \(i)") } var body: some View { NavigationStack { ScrollView { LazyVStack(spacing: 8) { ForEach(items) { item in Text(item.label) .frame(maxWidth: .infinity) .frame(height: item.height) .background(.gray.opacity(0.15)) .clipShape(RoundedRectangle(cornerRadius: 12)) .padding(.horizontal) } } .padding(.vertical) } .navigationTitle("LazyVStack Demo") .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Shuffle") { withAnimation() { items.shuffle() for i in items.indices { items[i].height = .random(in: 80...220) } } } } } } } } #Preview { ContentView() }
0
0
60
Sep ’25
Cannot render ScrollView + VStack + ScrollPosition correctly scrolled initially
The following code works properly, ensuring the list is scrolled at the correct ID when first rendered: import SwiftUI struct DataItem: Identifiable { let id: Int let height: CGFloat init(id: Int) { self.id = id self.height = CGFloat.random(in: 100...300) } } struct ContentView: View { @State private var items = (0..<1000).map { DataItem(id: $0) } @State private var scrollPosition: ScrollPosition init() { let mid = 500 _scrollPosition = State(initialValue: .init(id: mid, anchor: .center)) } var body: some View { ScrollView { LazyVStack(spacing: 8) { ForEach(items) { item in Text("Item \(item.id)") .frame(height: item.height) .frame(maxWidth: .infinity) .background(.gray) } } .scrollTargetLayout() } .scrollPosition($scrollPosition, anchor: .center) } } However, if I change this to use VStack this ceases to work - the list begins rendered at the top of the list. The only way I can render it starting at the correct position is using side effects like onAppear or task, where I would update the scroll position. I have observed the following behavior on my iPhone 15 Pro/iOS 26. Is this a bug?
Topic: UI Frameworks SubTopic: SwiftUI
0
0
51
Sep ’25
Building a bidirectional, infinitely scrolling list using ScrollView - challenges and potential solutions
I have been banging my head against this problem for a bit now. I am trying to build a bidirectional, infinitely scrolling list that implements these core requirements: Loads data up/down on the fly as the user scrolls Preserves scroll velocity as the list is updated Restores the scroll to the exact visual location after data has changed Ensures no flicker when restoring scroll position - the user cannot know the list has updated and should continue scrolling as normal Because LazyVStack does not play well with animations, I am opting to go with VStack and am implementing my own sliding window for data. This means that data can be removed as well as added, and a simple application of a height delta is not enough when restoring position. So far I have tried many things: Relying on ScrollPosition - simply does not work by itself as described (swift UI trying to keep the position stable with ID's) Relying on ScrollPosition.scrollTo - only kind of works with ID, no way to restore position with pixel perfect accuracy Intercepting the UIKit scrollView instance, using it to record and access the top row's position, mutating data and then queuing a scroll restoration using CATransaction.setCompletionBlock - this is the closest I've come, and it satisfies the top 3 requirements but sometimes I get a flicker on slightly heavier lists What I would really like, is a way of using ScrollView and granularly hooking into the lifecycle of the view after layout, and just before draw. At this point I would update the relevant scroll positions, and allow draw to continue. Is this possible? My knowledge is very limited at this point, but I believe I may be able to achieve something of the sort by swizzling layerWillDraw? Does this make sense, and is it prudent? In general, I'm very interesting in hearing what people have to say about the above, as well as this problem in general.
2
0
227
Sep ’25
Is it possible to read and write layout before render with SwiftUI?
I’m trying to keep a specific row visually stable while the data backing a ScrollView changes. Goal 1. Before updating model.items, capture the top row’s offset relative to the scroll view. 2. Mutate the observable state so SwiftUI recomputes layout — but don’t draw yet. 3. Read the new layout, compute the delta, and adjust the scroll position so the previously visible row stays put. 4. Only then draw the new frame. Reduced example @Observable final class SomeModel { var items: [SomeItem] = [/* ... */] } struct MyBox: View { @Environment(SomeModel.self) private var model var body: some View { ScrollView { VStack { ForEach(model.items, id: \.id) { item in Color.red.frame(height: randomStableHeight(for: item.id)) } } } } } // Elsewhere: let oldRow = recordOldRow() // capture the row to stabilize model.items = generateNewItems() // mutate model (invalidates layout) let newPos = capturePreviousRowNewPosition(oldRow) // read new layout? restoreScrollPosition() // adjust so oldRow stays visually fixed // draw now Is that pipeline achievable in SwiftUI? If not, what’s the supported way to keep a row visually stable while the list updates?
1
0
66
Sep ’25
Is realtime multidevice persistence possible using SwiftData?
I really enjoyed using SwiftData for persistence until I found out that the CloudKit integration ensures changes are only eventually consistent, and that changes can propagate to other devices after as long as minutes, making it useless for any feature that involves handoff between devices. Devastating news but I guess it’s on me for nrtfm. I may try my hand at a custom model context DataStore integrating Powersync, but that’s a whole trip and before I embark on it I was wondering if anyone had suggestions for resolving this problem in a simple and elegant manager that allows me to keep as much of the machinery within Apple’s ecosystem as possible, while ensure reliable “live” updates to SwiftData stores on all eligible devices.
2
0
196
Aug ’25