Search code examples
iosswiftfirebasefirebase-realtime-databasecollectionview

How to correctly load information from Firebase?


I will try my best to explain what I'm doing. I have an infinite scrolling collection view that loads a value from my firebase database for every cell. Every time the collection view creates a cell it calls .observe on the database location and gets a snapchat. If it loads any value it sets the cells background to black. Black background = cell loaded from database. If you look at the image below you can tell not all cells are loading from the database. Can the database not handle this many calls? Is it a threading issue? Does what I'm doing work with firebase? As of right now I call firebase in my override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { method

After some testing it seems that it is loading everything from firebase fine, but it's not updating the UI. This leads me to believe it's creating the cell before it's loading the information for some reason? I will try to figure out how to make it 'block' somehow.

The image


Solution

  • You should delegate the loading to the cell itself, not your collectionView: cellForItemAtIndexPath method. The reason for this is the delegate method will hang asynchronously and for the callback of the FireBase network task. While the latter is usually quick (by experience), you might have some issues here with UI loading.. Judging by the number of squares on your view..

    So ideally, you'd want something like this:

    import FirebaseDatabase
    
    
    class FirebaseNode {
    
        //This allows you to set a single point of reference for Firebase Database accross your app
        static let ref = Database.database().reference(fromURL: "Your Firebase URL")
    
    }
    
    class BasicCell : UICollectionViewCell {
    
        var firPathObserver : String { //This will make sure that as soon as you set the value, it will fetch from firebase
            didSet {
                let path = firPathObserver
                FirebaseNode.ref.thePathToYouDoc(path) ..... {
                    snapshot _
                    self.handleSnapshot(snapshot)
                }
            }
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupSubViews()
        }
    
        func setupSubViews() {
            //you add your views here..
        }
    
        func handleSnapshot(_ snapshot: FIRSnapshot) {
            //use the content of the observed value
            DispatchQueue.main.async {
                //handle UI updates/animations here
            }
        }
    
    }
    

    And you'd use it:

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let path = someWhereThatStoresThePath(indexPath.item)//you get your observer ID according to indexPath.item.. in whichever way you do this
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Your Cell ID", for: indexPath) as! BasicCell
            cell.firPathObserver = path
            return cell
        }
    

    If this doesn't work, it's probable that you might be encountering some Firebase limitation.. which is rather unlikely imo.

    Update .. with some corrections and with local cache.

    class FirebaseNode {
    
        //This allows you to set a single point of reference for Firebase Database accross your app
        static let node = FirebaseNode()
    
        let ref = Database.database().reference(fromURL: "Your Firebase URL")
    
        //This is the cache, set to private, since type casting between String and NSString would add messiness to your code
        private var cache2 = NSCache<NSString, DataSnapshot>()
    
        func getSnapshotWith(_ id: String) -> DataSnapshot? {
            let identifier = id as NSString
            return cache2.object(forKey: identifier)
        }
    
        func addSnapToCache(_ id: String,_ snapshot: DataSnapshot) {
            cache2.setObject(snapshot, forKey: id as NSString)
        }
    
    }
    
    class BasicCell : UICollectionViewCell {
    
        var firPathObserver : String? { //This will make sure that as soon as you set the value, it will fetch from firebase
            didSet {
                handleFirebaseContent(self.firPathObserver)
            }
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            setupSubViews()
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        func setupSubViews() {
            //you add your views here..
        }
    
        func handleFirebaseContent(_ atPath: String?) {
            guard let path = atPath else {
                //there is no content
                handleNoPath()
                return
            }
            if let localSnap = FirebaseNode.node.getSnapshotWith(path) {
                handleSnapshot(localSnap)
                return
            }
            makeFirebaseNetworkTaskFor(path)
        }
    
    
        func handleSnapshot(_ snapshot: DataSnapshot) {
            //use the content of the observed value, create and apply vars
            DispatchQueue.main.async {
                //handle UI updates/animations here
            }
        }
    
        private func handleNoPath() {
            //make the change.
        }
    
        private func makeFirebaseNetworkTaskFor(_ id: String) {
            FirebaseNode.node.ref.child("go all the way to your object tree...").child(id).observeSingleEvent(of: .value, with: {
                (snapshot) in
    
                //Add the conditional logic here..
    
                //If snapshot != "<null>"
                FirebaseNode.node.addSnapToCache(id, snapshot)
                self.handleSnapshot(snapshot)
                //If snapshot == "<null>"
                return
    
            }, withCancel: nil)
        }
    
    }
    

    One point however, using NSCache: this works really well for small to medium sized lists or ranges of content; but it has a memory management feature which can de-alocated content if memory becomes scarce.. So when dealing with larger sets like yours, you might as well go for using a classing Dictionnary, as it's memory will not be de-alocated automatically. Using this just becomes as simple as swaping things out:

    class FirebaseNode {
    
        //This allows you to set a single point of reference for Firebase Database accross your app
        static let node = FirebaseNode()
    
        let ref = Database.database().reference(fromURL: "Your Firebase URL")
    
        //This is the cache, set to private, since type casting between String and NSString would add messiness to your code
        private var cache2 : [String:DataSnapshot] = [:]
    
        func getSnapshotWith(_ id: String) -> DataSnapshot? {
            return cache2[id]
        }
    
        func addSnapToCache(_ id: String,_ snapshot: DataSnapshot) {
            cache2[id] = snapshot
        }
    
    }
    

    Also, always make sure you got through the node strong reference to Firebasenode, this ensures that you're always using the ONE instance of Firebasenode. Ie, this is ok: Firebasenode.node.ref or Firebasenode.node.getSnapshot.., this is not: Firebasenode.ref or Firebasenode().ref