3

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 :)

7
  • 0. You did some work before asking a question and this is great. 1. Never post the API keys to open source as you did github.com/aspnet82/MovieSearch/blob/master/Discover_Movie/… 2. The code is very long to study out. Is it possible from you to show some key points, where the things go wrong? 3. I've found some aspects, when you have dataTask - inside of its closure you should have [weak self]. 4. Have you tried to put your let movies = try decoder... inside the Dispatch async block? Commented Mar 6, 2020 at 12:08
  • Please, check this example project, as it is very similar to what you're asking. Especially this view and its model. P.S. This is not self-advertisement, just the issue regarding paging and appending List with data is the popular thing I decided to save somewhere to remember. Commented Mar 6, 2020 at 12:16
  • I tried to put the try decoder inside the Dispatch async block but returns an error. I am also updating the question to give more context Commented Mar 6, 2020 at 13:23
  • 2
    see swiftui-lab.com/a-powerful-combo the second example there is exactly what you want. Commented Mar 6, 2020 at 17:03
  • 1
    see my answer, it use different approach and works with ScrollView or List, whatever you prefer Commented Mar 6, 2020 at 20:01

1 Answer 1

4

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

Sign up to request clarification or add additional context in comments.

2 Comments

Tested on iOS 13.6 & 14.5. The code works. Could you please add some comments? I have been able to understand most of the code.
@Darkwonder I'm happy to answer a specific question about each line of code in which you have doubts.

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.