Search code examples
listnetwork-programmingswiftuigrand-central-dispatch

SwiftUI async Data fetch


I am trying to learn SwiftUI and creating a movie search app with The movie Database API

I would like to fetch new data once the scroll goes at the end of the List. I found a possible solution on SO using ForEach inside the List and checking when the list reaches the last item and then performing the onAppear() call.

In SwiftUI, where are the control events, i.e. scrollViewDidScroll to detect the bottom of list data

How can I load new pages from the search when the first page has loaded?

You can find the project here, was too big to post https://github.com/aspnet82/MovieSearch

SearchMovieManager -> Make the fetch call, I used Dispatch.main.async

Here what the app should do

  1. Fetch the data from the API and display the first page -> WORKING
  2. When the List scroll to the last item makes another fetch request on the following page to load more data -> This not works, it works only for the second page of the request. It always displays the same data after the second fetch call.

The issue I think is in the GCD, but I do not know how to queue things to make it works automatically

UPDATE: A workaround I found:

List(searchMovieManager.allMovies) { movie in
     Text(movie.title)
}
Text("load more").onTapGesture {
     fetchNextPage(obs: self.searchMovieManager, page: self.searchMovieManager.pageTofetch)
 }

I think might be ok as solution, it adds new page once I tap on the button, and can be also fine in controlling data download maybe?

Thanks for the help :)


Solution

  • Try the next, it use anchor preferences and simple model which mimics async operation to add some records to ScrollView (or List)

    import SwiftUI
    
    struct PositionData: Identifiable {
        let id: Int
        let center: Anchor<CGPoint>
    }
    
    struct Positions: PreferenceKey {
        static var defaultValue: [PositionData] = []
        static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
            value.append(contentsOf: nextValue())
        }
    }
    
    struct Data: Identifiable {
        let id: Int
    }
    
    class Model: ObservableObject {
        var _flag = false
        var flag: Bool {
            get {
                _flag
            }
            set(newValue) {
                if newValue == true {
                    _flag = newValue
    
                    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                        self._flag = false
                        self.rows += 20
                        print("done")
                    }
                }
            }
        }
        @Published var rows = 20
    }
    struct ContentView: View {
        @ObservedObject var model = Model()
        var body: some View {
            List {
                ForEach(0 ..< model.rows, id:\.self) { i in
                    Text("row \(i)").font(.largeTitle).tag(i)
                }
                Rectangle().tag(model.rows).frame(height: 0).anchorPreference(key: Positions.self, value: .center) { (anchor) in
                    [PositionData(id: self.model.rows, center: anchor)]
                }.id(model.rows)
            }
            .backgroundPreferenceValue(Positions.self) { (preferences) in
                GeometryReader { proxy in
                    Rectangle().frame(width: 0, height: 0).position(self.getPosition(proxy: proxy, tag: self.model.rows, preferences: preferences))
                }
            }
        }
        func getPosition(proxy: GeometryProxy, tag: Int, preferences: [PositionData])->CGPoint {
            let p = preferences.filter({ (p) -> Bool in
                p.id == tag
                })
            if p.isEmpty { return .zero }
            if proxy.size.height - proxy[p[0].center].y > 0 && model.flag == false {
                self.model.flag.toggle()
                print("fetch")
            }
            return .zero
        }
    }
    
    
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    and here is how it looks like ...

    enter image description here