4

I have found a way to save a ScrollViews offset with a GeometryReader and a PreferenceKey.

SwiftUI | Get current scroll position from ScrollView

And the ScrollViewReader has a method scrollTo to scroll to a set position.

scrollTo

The problem is, that the first one saves an offset while the second method expects a position (or an id, which is similar to the position in my case). How can I convert the offset to a position/id or is there any other way to save and load a ScrollViews position?

Here is the code I have now but it does not scroll the way I want:

ScrollView {
    ScrollViewReader { scrollView in
        LazyVGrid(columns: columns, spacing: 0) {
            ForEach(childObjects, id: \.id) { obj in
                CustomView(obj: obj).id(obj.id)
            }
        }
        .onChange(of: scrollTarget) { target in
            if let target = target {
                scrollTarget = nil
                scrollView.scrollTo(target, anchor: .center)
            }
        }
        .background(GeometryReader {
            Color.clear.preference(key: ViewOffsetKey.self,
                value: -$0.frame(in: .named("scroll")).origin.y)
        })
        .onPreferenceChange(ViewOffsetKey.self) { // save $0 }
    }
}.coordinateSpace(name: "scroll")

And in the onAppear of the View I want to set scrollTarget to the saved position. But it scrolls anywhere but not to the position I want.

I thought about dividing the offset by the size of one item but is that really the way to go? It does not sound very good.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
L3n95
  • 1,505
  • 3
  • 25
  • 49

1 Answers1

7

You don't need actually offset in this scenario, just store id of currently visible view (you can use any appropriate algorithm for your data of how to detect it) and then scroll to view with that id.

Here is a simplified demo of possible approach. Tested with Xcode 12.1/iOS 14.1

demo

struct TestScrollBackView: View {
    @State private var stored: Int = 0
    @State private var current: [Int] = []
    
    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                HStack {
                    Button("Store") {
                        // hard code is just for demo !!!
                        stored = current.sorted()[1] // 1st is out of screen by LazyVStack
                        print("!! stored \(stored)")
                    }
                    Button("Restore") {
                        proxy.scrollTo(stored, anchor: .top)
                        print("[x] restored \(stored)")
                    }
                }
                Divider()
                ScrollView {
                    LazyVStack {
                        ForEach(0..<1000, id: \.self) { obj in
                            Text("Item: \(obj)")
                                .onAppear {
                                    print(">> added \(obj)")
                                    current.append(obj)
                                }
                                .onDisappear {
                                    current.removeAll { $0 == obj }
                                    print("<< removed \(obj)")
                                }.id(obj)
                        }
                    }
                }
            }
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Okay so you store the visible items with onAppear and onDisappear. Thank you. That works better but still not 100% it is always some rows below the saved value. Maybe that is because I have multiple columns in the LazyVGrid? I have to check where that might come from. – L3n95 Jan 24 '21 at 14:54
  • 1
    I see the problem. I am loading the items for the ScrollView in the onAppear and I am loading the ScrollView position in the same onAppear and the problem is, that the Views in the ScrollView have not yet appeared when the scrollTo is called. – L3n95 Jan 24 '21 at 16:20
  • I've got it now. Used a button like you and had to change the iterator and id to 0.. – L3n95 Jan 27 '21 at 10:52
  • Nice way @Asperi ! However how would you handle navigating to another view and coming back? In this case, when going to the other view, the TestScrollBackView disappears and its children all call onDisappear, so current becomes empty. I guess I could store the value somehow before the screen disappears, but it requires to check the actions in the UI. – KJ Newtown Mar 23 '22 at 16:33
  • Never mind, I can store the value. But finally the problem is when coming back to TestScrollBackView it seems lazyVStack is not ready to scroll in .onAppear – KJ Newtown Mar 23 '22 at 16:44