2
votes

I am populating my CollectionView with loadData() which is called in the ViewDidLoad() method. In here, I parse all data from my Firebase realtime database to an array (posts). Then, in the cellForItemAt method, I set all images, labels and textviews accordingly based on the information in the posts array using indexPath.item. Pretty basic stuff.

However, I have two tables in my database: posts and users. In posts, I only collect the information regarding the post and the userID of the author. I then want to fetch the data from users, since the profile picture and username can change over time, so I don't want to make it sticky inside the posts table in the database.

The problem I had before: I loaded the data from the posts inside loadData() and then would get the user information in the cellForItemAt method based on the userID saved in the posts array. This caused my app to be choppy: scrolling to new cells initiated the cellForItemAt method, causing it to request the data, then updating it. So there would be a delay as the information had to be downloaded. Absolutely sure this was the cause, as I now set it to a default image (no profile picture image) and default username ("Username"), making it very smooth again.

I then moved on to fetch the userData and parse it to another array (userInfo):

struct userData {
    var userFirstName: String
    var userLastName: String
    var userProfilePicURL: String
}

var userInfo = [String : userData]()

I can use this as userInfo[posts.userID], which is precisely what I was looking for. The issue I have now is that the userInfo is not populated in time, returning nil when I dump the array in cellForItemAt:

dump(userInfo[post.userID])

So this returns nil on loading the app, but when I scroll, and thus initialize cellForItemAt again, it does return values. So my educated guess would be that the data is not fetched in time. I am now looking for a way to only call cellForItemAt when the posts array ánd the user array is loaded.

How I add values to the user array, inside the loadData function, where dict["userID"] is obtained through observing the posts in the database:

Ref.child("users").child(dict["userID"] as! String).observeSingleEvent(of: .value) { (snapshot) in

    let userValues = snapshot.value as! [String: Any]
    userInfo[dict["userID"] as! String] = userData(userFirstName: userValues["userFirstName"] as! String, userLastName: userValues["userLastName"] as! String, userProfilePicURL: userValues["userProfilePicURL"] as! String)

}

I want to make sure that the information is added to the array before showing the cells, so they can change the profile picture and the username accordingly. I want to do this in the cellForItemAt method. I thought about using timers, hiding my CollectionView for a couple of seconds, but this would all depend on the connection speed etc. so I think there should be a more suitable solution.

Any useful ideas are welcome!

4
So you want to load the collection view only when both the arrays are filled with data, right ? - Awais Fayyaz
Yes, that’s right. - PennyWise

4 Answers

2
votes

You can achieve this from the storyboard you donot need to join the delegate and datasource of the collectionview . And in the controller class when you get the data then after just set collectionview.delegate = self and datasource and reload that collection view.

1
votes

You can use Prefetching mechanism which Apple introduced in IOS 10. They've explained it in with the example in the following link.

https://developer.apple.com/documentation/uikit/uicollectionviewdatasourceprefetching/prefetching_collection_view_data

I hope it will solve your problem.

1
votes

What i would do in this type of scenario,

  1. Don't set delegate of the collectionview to your controller.
  2. Perform Firebase request 1 to load data into your array1. Inside completion of first request, call another function that performs request2 to fetch and load data into array 2.
  3. Inside the completion handler of 2nd request, set the delegate and reload data (In Main Thread)

If you don't want nested calls. you can fire both requests parrallel and wait for the calls to complete Then set delegate and reload data

See this Helpful answer for details on how to do it.

Hope it helps

0
votes

You have asked many things. Yes it is true that the data may not be loaded by the time the view is displayed. Fortunately the UICollectionView was designed with this in mind. I recommend watching the "Let's Build That App" YouTube Channel for good tips on using the UICollectionView.

We do a lot of dynamic searching of a remote server so the content is constantly being updated and may even update as we are loading it. Here is how we handle it.

cellForItemAt is not the time to do a server call. Use this after the data is loaded. In general do not mix your server loading with the data display. Write the UICollectionView with the intent that it is operating on an internal list of items. This will also make your code more readable.

Add your slow server functions in such a way that they update the data quickly and call reloadData() after you have done so. I will provide code below.

In our UICollectionViewController we keep an array of the current content, something like this. We start with these variables up top:

var tasks = [URLSessionDataTask]()
var _entries = [DirectoryEntry]()
var entries: [DirectoryEntry] {
    get {
        return self._entries
    }
    set(newEntries) {
        DispatchQueue.main.async {
            self._entries = newEntries
        }
    }
}

Everytime we fetch data from the server we do something like:

func loadDataFromServer() {

    // Cancel all existing server requests
    for task in tasks {
        task.cancel()
    }
    tasks = [URLSessionDataTask]()

    // Setup your server session and connection
    // { Your Code Here }

    let task = URLSession.shared.dataTask(with: subscriptionsURL!.url!, completionHandler: { data, response, error in
        if let error = error {
            // Process errors
            return
        }

        guard let httpresponse = response as? HTTPURLResponse,
            (200...299).contains(httpresponse.statusCode) else {
                //print("Problem in response code");
                // We need to deal with this.
                return
        }
        // Collect your data and response from the server here populate apiResonse.items
        // {Your Code Here}
        var entries = [DirectoryEntry]()

        for dataitem in apiResponse.items {
            let entry = Entry(dataitem)
            entries.append(dataitem)
        }
        self.entries = entries
        self.reloadData()
    })
    task.resume()
    tasks.append(task)
}

First, we keep tack of each server request (task), so that when we initiate a new one, we can cancel any outstanding requests, so they don't waste resources and so they don't overwrite the current request. Sometimes an older request can actually finish after a newer request giving you weird results.

Second, we load the data into a local array within the server loading function (loadDataFromServer), so that we don't change the live version of the data as we are still loading it. Otherwise you will get errors because the number of items and content might change during the actual display of the data, and iOS doesn't like this and will crash. Once the data is completely loaded, we then load it into the UIViewController. We made this more elegant by using a setter.

self.entries = entries

Third, you have to instruct UICollectionView that you have changed your data, so you have to call reloadData().

self.reloadData()

Fourth, when you update the data use the Dispatch code because you cannot update a UI from background thread.

How and when you decide to load data from the server is up to you and your UI design. You could make your first call in viewDidLoad().