0

I'm fairly new to SwiftUI and I'm trying to figure out a problem that I can't seem to solve. I am building an app based on the GitHub API. In one of the screens, I am showing all the followers that a particular user has.

What I am trying to accomplish is that I'm trying to know when the user has scrolled to the bottom of the list of followers as I currently only grab 100 records from the server.

How can I know when the user has scrolled to the bottom of those 100 followers so that I can initiate another request for 100 more records in the 2nd and subsequent pages?

I have googled this and saw things about the ScrollViewReader, GeometryReader but I am not sure how to fix this. Here is the code in this particular screen:

import SwiftUI

struct FollowersScreen: View {
    
    @EnvironmentObject var savedFavorites: SavedFavorites
    
    @State var userID: String
    @State private var isPresented = false
    @State private var followers: [Follower] = []
    @State private var page = 1
    @State private var user: User? = nil
    
    let columns = [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
    
    var body: some View {
        VStack {
            ScrollView {
                LazyVGrid(columns: columns, content: {
                    ForEach(followers, id: \.self) { follower in
                        FollowerCell(follower: follower)
                            .onAppear(perform: {
                                // What do I do here to get 100 more records properly?
                            })
                            .onTapGesture {
                                self.getFollowerDetails(userID: follower.login)
                            }// ON TAP GESTURE
                    }// FOR EACH
                })// LAZYVGRID
                    .padding(.horizontal)
            }// SCROLL VIEW
        }// VSTACK
        .onAppear(perform: {
            self.getFollowers(userID: userID, page: 1)
        })
        .navigationTitle("\(userID) Followers")
        .navigationBarTitleDisplayMode(.inline)
        .navigationBarItems(trailing:
                                Button(action: {
            addFavourite(gitHubUser: userID)
        }) {
            Image(systemName: Images.heartCircle)
                .resizable()
                .scaledToFit()
                .frame(width: 33, height: 33, alignment: .center)
        }
        )// NAV BAR ITEMS
        .sheet(isPresented: $isPresented, onDismiss: nil) {
            FollowersDetailScreen(gitHubUser: $userID, isPresented: $isPresented, followers: $followers, user: user)
        }
        .environmentObject(savedFavorites)
    }// BODY
    
    private func getFollowers(userID: String, page: Int) {
        NetworkManager.shared.getFollowers(for: userID, page: page) { result in
            switch result {
            case .success(let followers):
                self.followers += followers
                print("**** Count of Followers: \(self.followers.count)")
            case .failure(let error):
                print("getFollowers(gitHubUser:, page:) -> Error: \(error)")
            }
        }
    }
    
    private func getFollowerDetails(userID: String) {
        NetworkManager.shared.getUserInfo(for: userID) { result in
            switch result {
            case .success(let user):
                print("Success, found the user info!")
                self.user = user
                guard let tempUser = self.user else { return }
                print(tempUser)
                self.isPresented = true
            case .failure(let error):
                print("getUser(gitHubUser:, page:) -> Error: \(error)")
            }
        }
    }
    
    private func addFavourite(gitHubUser: String) {
        NetworkManager.shared.getUserInfo(for: gitHubUser) { result in
            switch result {
            case .success(let user):
                let favorite = Follower(login: user.login, avatarUrl: user.avatarUrl)
                savedFavorites.favorites.append(favorite)
                PersistenceManager.shared.encodeJSONToDisk(favorites: savedFavorites.favorites)
            case .failure(let error):
                print("getFavourite(gitHubUser:, page:) -> Error: \(error)")
            }
        }
    }
}

struct FollowersScreen_Previews: PreviewProvider {
    static var previews: some View {
        FollowersScreen(userID: "sallen0400")
    }
}
JFortYork
  • 137
  • 10
  • For answering to your question you need to work on `GeometryReader` it needs some line code to notified when we are at the bottom of all followers, but that would be unnecessary since `LazyVGrid` is smart enough to call the data needed! You should not take care about it! You would be do such thing if there was no LazyVGrid exist! – ios coder Oct 07 '21 at 01:46
  • Sorry, but your comment is totally unclear. – JFortYork Oct 07 '21 at 02:04
  • You mean totally unclear for you? – ios coder Oct 07 '21 at 02:18
  • Yes, if you could describe the steps that would much easier to understand. – JFortYork Oct 07 '21 at 12:02

1 Answers1

2

You don't need to know when you are at the bottom of the LazyVGrid, you need to know when you are running out of data. You need the index of where you are in the list, and compare it to the count of your array. Also, I had to use some pseudocode as I didn't have all of your types:

            LazyVGrid(columns: columns, content: {
                // Zip gives you both the index and the item. You could just get the index, but the
                // code reads better this way, IMHO
                ForEach(Array(zip(followers.indices, followers), id: \.1) { index, follower in
                    FollowerCell(follower: follower)
                        .onAppear(perform: {
                            // Now we test our index against the total array count.
                            // You should also build a check that once you get an end of
                            // data response that you don't keep calling for more data.
                            if followers.count()/* - buffer*/ == index { // Place buffer here for earlier update.
                                getFollowers(userID: ...
                            }
                        })
                        .onTapGesture {
                            self.getFollowerDetails(userID: follower.login)
                        }// ON TAP GESTURE
                }// FOR EACH
            })// LAZYVGRID

When I originally answered this I built in a buffer of 20 items before the update was called. However, with further experimentation, I found that this is not necessary as the .onAppear is triggered well before the view actually appears on screen. If you want to add a buffer to give you more time to download the data, all you have to do is subtract a given amount of items from the total count so the update is triggered earlier. I put a comment about it as to where to place it.

Zoe
  • 27,060
  • 21
  • 118
  • 148
Yrb
  • 8,103
  • 2
  • 14
  • 44
  • Edited to correct the `id:` in the `ForEach`. – Yrb Oct 07 '21 at 13:40
  • Thank you for your comment! I will try it out and report back! – JFortYork Oct 07 '21 at 14:49
  • It seems to work.. thank you! – JFortYork Oct 07 '21 at 15:00
  • So, the zip method just allows you to attach an index to the specific item within the followers array? That's a clever idea! – JFortYork Oct 07 '21 at 15:18
  • It essentially creates an array of tuples. One being the element of the array, and one being its index. The edit to the `id:` was to tell the `ForEach` to base the id off of the element, and not the index. See [this answer](https://stackoverflow.com/a/63145650/7129318) for a fuller explanation. I stumbled across it while having an issue with `.enumerated`. – Yrb Oct 07 '21 at 15:59