2

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!

PennyWise
  • 595
  • 2
  • 12
  • 37

4 Answers4

2

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.

Anil Kumar
  • 1,830
  • 15
  • 24
  • Not using any Storyboards, writing everything in code. However, would it be true that I can set my delegate and datasource after all information is collected, reload and it would work? Is this best practice? – PennyWise Sep 30 '18 at 15:00
  • Yeah i think so but delegate and datasource and reload should be kept in another method. – Anil Kumar Sep 30 '18 at 15:05
  • 1
    I can agree with you. in the code, I used to add the delegate and datasource in side viewDidLoad() method. Whenever i want to wait until my array fills up with data from a remote server, i add delegate and datasource once I'm done with filling data so all are good....and the collectionView can see the data in the array. – Thush-Fdo Aug 25 '20 at 07:43
1

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.

Bilal
  • 227
  • 2
  • 9
  • Definitely going to look into it. Will probably not be an issue as most iOS users are on iOS10+, right? I’ll see what I can get from the URL you provided, thank you! – PennyWise Sep 30 '18 at 12:39
  • Should I replace all my code that currently populates my data in cellForItemAt to the new cellForItemAt protocol that is created by the CustomDataSource? And other protocols as well (e.g. sizeForItemAt)? – PennyWise Sep 30 '18 at 13:05
  • @PennyWise No, but you have to change the logic/code in your existing functions like `cellForItemAt` and `sizeFormItemAt`. But before that, you've to add `prefetchItemsAt` function (`UICollectionViewDataSourcePrefetching` protocol) – Bilal Sep 30 '18 at 13:20
1

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

Awais Fayyaz
  • 2,275
  • 1
  • 22
  • 45
0

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().

David J
  • 1,018
  • 1
  • 9
  • 14
  • Sir, this is some very useful and understandable information. I will go over it again until I completely understand. Although I use Firebase and therefor don't handle HTTPRequests, I assume I can still edit my code based on your answer. Thank you very much! – PennyWise Sep 30 '18 at 14:59
  • I doubt it would matter what you use. You can write your "slow" firebase requests into some code that updates the controller content. – David J Sep 30 '18 at 15:56
  • Can't really get the hang of it just yet. Trying a couple of things now and doesn't do what I expect it to do - but I'm sure that's because I'm not familiar enough with Swift just yet. I'll just do some more research and try my best. – PennyWise Sep 30 '18 at 16:13