Andrew
Andrew

Reputation: 11427

How can I programmatically scroll in SwiftUI?

There is a way in SwiftUI to scroll to a view. like this example:

ScrollView {

    ScrollViewReader { value in

        Button("Jump to #8") {

            // we scroll on button press
            value.scrollTo(8, anchor: .top)
        }
        .padding()

        ForEach(0..<100) { i in

            Text("Example \(i)")
                .font(.title)
                .frame(width: 200, height: 200)
                .background(colors[i % colors.count])
                
                // We need to set ID for scrolling
                .id(i)

        }
    }
}

But my UI is not so simple. I have a View and ViewModel. Also some views generated with some delay.

I need to scroll to some element after all views will be loaded.

Do it after is not a problem, but how to scroll to needed element programmatically from ViewModel?


@ObservedObject var model: someViewModel

var body: some View {
    ScrollView {
        ScrollViewReader { pageScroller in

            ForEach(0..<100) { i in
                Text("Example \(i)")
                    .frame(width: 200, height: 200)
                    .background(colors[i % colors.count])
                    .id(i)
            }
        }
    }
    .frame(height: 350)
}

class someViewModel: ObservableObject {
    @Published var scrollToPageName = 4
    var needToScroll = false

    init() { }
}

How to execute pageScroller.scrollTo(pageIndex, anchor: .top) from my model WITHOUT page refresh?

Or how to execute some view func from model by some event?

Also I need to not do refresh of the page before the scroll(!) - just like on button click from first example.

Upvotes: 1

Views: 2255

Answers (2)

Andrew
Andrew

Reputation: 11427

IMPORTANT! This answer will be usefull for you only if it is important to do not make refresh of the view. In another case better to use solution of EmilioPelaez

This answer shows how to:

  • send event from model to view

  • and execute some function when event was fired

  • Full view Refresh will be not fired! Only code will be executed just like on button press!

@ObservedObject var model: someViewModel

var body: some View {
    ScrollView {
        ScrollViewReader { pageScroller in

            ForEach(0..<100) { i in
                Text("Example \(i)")
                    .frame(width: 200, height: 200)
                    .background(colors[i % colors.count])
                    .id(i)
            }
            //Magic here
            .onReceive(model.scroller.objectWillChange) {
                pageScroller.scrollTo(model.scroller.scrollToPageName, anchor: .top)
            }
        }
    }
    .frame(height: 350)
}

class someViewModel: ObservableObject {
    //Magic here
    @ObservedObject var scroller: scrollerHelper

    init() {
        someDelayedCall()
    }

    func someDelayedCall(){
         scroller.scrollToPageName = 8
    }
}

//Magic here
class scrollerHelper: ObservableObject {
    @Published var scrollToPageName: Int = -1
}

Upvotes: 1

EmilioPelaez
EmilioPelaez

Reputation: 19912

It's not the view model's responsibility to scroll, but what you can do is add a selectedIndex property to your view model, and observe and react to the change.

var body: some View {
    ScrollView {
        ScrollViewReader { pageScroller in
            ForEach(0..<100) { i in
                Text("Example \(i)")
                    .frame(width: 200, height: 200)
                    .background(colors[i % colors.count])
                    .id(i)
            }
            .onChange(of: viewModel.selectedIndex) { newIndex in
                pageScroller.scrollTo(newIndex, anchor: .top)
            }
        }
    }
    .frame(height: 350)
}

Since you mentioned that your content is loaded asynchronously, you can probably update the selectedIndex property once everything is loaded.

Upvotes: 4

Related Questions