1

I want to set the scroll position of a ScrollView containing a LazyVStack such that it shows a view nested arbitrarily deeply within that stack. Currently I give the view which should be scrolled to an id using the .id modifier and then attempt to use a ScrollViewReader to scroll to that view using that id. However this does not set the scroll position if the target view is not loaded by the LazyVStack and it appears that this approach will only work for views which are direct children of the lazy stack.

Is there any way, that I can achieve the desired behaviour without making the target views direct children of the lazy stack?

The example below displays a list of lists of strings, where the outer list is displayed using a LazyVStack and the inner lists use a VStack. Pressing the button in ContentView should scroll the ScrollView to the Text view given by the id. However this currently only works if the ScrollView is scrolled far enough, such that the StringsView containing the target view is loaded by the lazy stack.

import SwiftUI

class StringsViewModel: ObservableObject {
    let uuid: UUID = UUID()
    
    @Published var strings: [String] = []
    
    init(_ range: Range<Int>) {
        for i in range {
            strings.append(String(i))
        }
    }
}

struct StringsView: View {
    @ObservedObject private var viewModel: StringsViewModel
    
    init(viewModel: StringsViewModel) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        VStack {
            ForEach(viewModel.strings, id: \.self) { str in
                Text(str)
                    .id(Int(str)) // The id to use in order to scroll to that view.
            }
        }
    }
}

class ViewModel: ObservableObject {
    @Published var stringsModels: [StringsViewModel] = []
    
    init(blockCount: Int, numsPerBlock: Int) {
        for i in 0..<blockCount {
            self.stringsModels.append(StringsViewModel((i*numsPerBlock)..<((i+1)*numsPerBlock)))
        }
    }
}

struct ContentView: View {
    @StateObject private var viewModel: ViewModel = ViewModel(blockCount: 5, numsPerBlock: 250)
    
    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                // Pressing this button should set the scroll position of the scroll view.
                Button {
                    proxy.scrollTo(500)
                } label: {
                    Text("Scroll")
                }
                
                // The scroll view whose scroll position should be modified by the button.
                ScrollView(.vertical) {
                    LazyVStack {
                        ForEach(viewModel.stringsModels, id: \.uuid) { stringModel in
                            StringsView(viewModel: stringModel)
                                .border(Color.black)
                        }
                    }
                }
            }
        }
    }
}

#Preview(body: {
    ContentView()
})
1
  • Do not nest ObservableObject like you do in class ViewModel: ObservableObject with [StringsViewModel] inside. SO has plenty of explanations about this, search for it. Alternatively you could try using the more modern @Observable that you can nest, see Apple docs Managing model data in your app, requires iOS17+ Commented Apr 4 at 23:01

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.