4

I do async load of items from network .onAppear, after that my ViewModel force to redraw swiftUI view. After that i need scroll list to particular item. I do it .onRecieve, problem is that view redrawing and scrolling perform almost same time, but indeed scrolling goes first as i understand, so i need add ugly hack - delay, than it works fine. Question is there are some another approach how to postpone scrolling after view refresh.

struct ItemsListView: View {
@Binding var selectedItem: Item?
@ObservedObject var viewModel: ViewModel
var body: some View {
    ScrollViewReader { proxy in
        List(viewModel.state.items) { item in
            let selected = item.id == selectedItem?.id
            self.createCell(item: item, selected: selected)
                .onTapGesture {
                    selectedItem = item
                }
        }
        .listStyle(SidebarListStyle())
        .navigationBarTitleDisplayMode(.inline)
        .onAppear {
            self.viewModel.trigger(.getItems) // triggers async load of items
        }
        .onReceive(viewModel.$state) { state in
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { // hack to give a time redraw ItemsListView
                withAnimation {
                    proxy.scrollTo(selectedItem?.id, anchor: .center)
                }
            }
        }
}

}

View Model code, it's real but a bit simplified for this topic

class ViewModel: ObservableObject {
    
    struct State {
        var items = [Item]()
    }
    
    enum Event {
        case getItems
    }
    
    func trigger(_ event: Event) {
        switch event {
        case .getItems: self.getItems()
        }
    }
    
    @Published var state = State()
    
    init(dataFetcher: RemoteDataProvider) {
        self.dataFetcher = dataFetcher
        super.init()
    }
    
    private func getItems() {
        dataFetcher.getItems()
            .sink { result in
                if case .failure(let err) = result {
                    print("Retrieving car items list failed:\(err)")
                }
            } receiveValue: {[weak self] items in
                self?.state.items = items
            }
            .store(in: &subscriptions)
    }
}
Mering
  • 51
  • 4
  • 1
    I have had to do the same. You may find that simply using DispatchQueue.main.asyncAfter(deadline: .now()) slows it down enough to work. – Yrb Jan 02 '22 at 13:47
  • 1
    i know, but it's hack, and i believe, it should be more elegant approach that ensures order of calls after view redrawing. Or Apple just missed this case and we really don't have API for that? – Mering Jan 02 '22 at 19:09
  • 1
    I really think `ScrollTo` is pretty unfinished. Whether they finish it is another matter. – Yrb Jan 02 '22 at 19:15
  • So far the only solution I've been able to come up with (non-ideal) is to use a GeometryReader to check if a particular item has or has not actually appeared on screen - and use that to trigger the scrollTo. – Gsp Apr 13 '23 at 11:44

0 Answers0