0

I am trying to populate a collection view. When I go to use a value in a dictionary, it is showing up as nil, but when I print it the line before it provides me with a string.

I fixed this issue before, but when I remade my collection view to solve another bug I was having, this bug came back. To fix it, I just removed the line self.users.removeAll(). Adding that line back in then removing it doesn't work this time.

I am having problems specifically in this function:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let cell = collectionview.dequeueReusableCell(withReuseIdentifier: "userCell", for: indexPath) as! UserCell

    print(user[indexPath.row].imagePath!)

    cell.userImage.sd_setImage(with: URL(string: user[indexPath.row].imagePath!))

    cell.nameLabel.text = user[indexPath.row].username
    cell.userID = user[indexPath.row].userID

    return cell
}

Here is the full code:

import UIKit
import Firebase
import SwiftKeychainWrapper
import SwiftUI
import FirebaseUI

class UserViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {

    @IBOutlet weak var collectionview: UICollectionView!

    var user = [User]()

    override func viewDidLoad() {
        super.viewDidLoad()
        collectionview.delegate = self
        collectionview.dataSource = self

        retrieveUsers()
    }

    func retrieveUsers() {
       let ref = Database.database().reference()
       ref.child("users/").observeSingleEvent(of:.value, with: { (snapshot) in
          let users = snapshot.value as! [String : NSDictionary]
          //self.user.removeAll()
          for (_, value) in users {
             if let uid = value["uid"] as? String {
                if uid != Auth.auth().currentUser!.uid {
                   let userToShow = User()
                   if let username = value["username"] as? String, let imagePath = value["urlToImage"] as? String {
                      userToShow.username = username
                      userToShow.imagePath = imagePath
                      userToShow.userID = uid
                      self.user.append(userToShow)
                      print(userToShow)
                   }
                }
             }
          }

          self.collectionview.reloadData()
       })

       ref.removeAllObservers()
    }

    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionview.dequeueReusableCell(withReuseIdentifier: "userCell", for: indexPath) as! UserCell

        print(user[indexPath.row].imagePath!)

        cell.userImage.sd_setImage(with: URL(string: user[indexPath.row].imagePath!))
        cell.nameLabel.text = user[indexPath.row].username
        cell.userID = user[indexPath.row].userID

        return cell
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return user.count //?? 0
    }

    func checkFollowing(indexPath: IndexPath) {
       let uid = Auth.auth().currentUser!.uid
       let ref = Database.database().reference()

       ref.child("users").child(uid).child("following").queryOrderedByKey().observeSingleEvent(of: .value, with: { snapshot in
           if let following = snapshot.value as? [String : AnyObject] {
              for (_, value) in following {
                 if value as! String == self.user[indexPath.row].userID {
//                  self.tableview.cellForRow(at: indexPath)?.accessoryType = .checkmark
                 }
              }
           }
       })

       ref.removeAllObservers()

    }

    @IBAction func logOutPressed(_ sender: Any) {
        KeychainWrapper.standard.removeObject(forKey:"uid")

        do {
            try Auth.auth().signOut()
        } catch let signOutError as NSError{
            print("Error signing out: %@", signOutError)
        }
        dismiss(animated: true, completion: nil)
    }
}


extension UIImageView {
    func downloadImage(from imgURL: String!) {
        let url = URLRequest(url: URL(string: imgURL)!)

        let task = URLSession.shared.dataTask(with: url) {  (data, response, error) in
            if error != nil {
                print(error!)
                return
            }

            DispatchQueue.main.async {
                self.image = UIImage(data: data!)
            }
        }

        task.resume()
    }
}

Am I not populating the dictionary correctly?

Also, here is the User class:

import UIKit

class User: NSObject {
    var userID: String?
    var username: String?
    var imagePath: String?
}

Also, this is what the user dictionary shows in the debugger:

enter image description here

koen
  • 5,383
  • 7
  • 50
  • 89
Nick Steen
  • 71
  • 7
  • What is `sd_setImage`? If it is asynchronous that’s the problem. Your code does not run in the order you think. – matt Dec 17 '19 at 18:14
  • sd_setImage is the function to set an ImageView. Why would the print statement then print something, but the line directly after it not work? Is async really that wonky? This same code was working before I remade the collectionView so I think it's something else but I am too new to Xcode to understand – Nick Steen Dec 17 '19 at 18:16
  • Because the line after is not really after. That is what asynchronous means. – matt Dec 17 '19 at 18:17
  • is there a way to make sure that that line is executed after? From my understanding the collection view shouldn't be formatted until I call reloadData() on it. I don't call reloadData() until an entry in the user dictionary is completed. How can I ensure the cell isn't formed until all the data is there? – Nick Steen Dec 17 '19 at 18:25
  • Async is not wonky at all - but it has a steep learning curve. Maybe this will help: https://stackoverflow.com/questions/31589467/loading-data-asynchronously-into-uitableview – koen Dec 17 '19 at 18:29
  • 1
    Nick, please learn to understand how asynchronous data processing works. And please take advices like reloading the collection view on the main thread and declaring `User` with non-optional properties and an initializer. And consider to vote and accept answers if they helped you. – vadian Dec 17 '19 at 18:29
  • I'm trying to understand how async works but I'm having trouble understanding how, if the variable is shown to have a value in the debugger, the value doesn't exist at runtime. I will look into declaring User with an initializer – Nick Steen Dec 17 '19 at 18:40
  • What am I missing? If the print statement in the 2nd line of `cellForItemAt` prints a string it should *not* be nil when `sd_setImage` is called in the next line. `sd_setImage` is an async method, but the argument to the method isn't async. Based on what I see this should work, though it might take a few seconds for your image to load after the collectionViewCell is displayed. There *is* async code in these examples, but it seems like it is (more or less) being handled properly. – creeperspeak Dec 17 '19 at 19:35
  • Is `user[indexPath.row].imagePath` `nil` or do you get `nil` when you try and initialise a `URL` with that value? I suspect the latter which would indicate that your string isn't a valid URL – Paulw11 Dec 17 '19 at 19:57
  • There are a few things that could be improved & clarified. This `ref.child("users/")` should be `ref.child("users")` no forward slash. Not a big deal but it can lead to confusion. Not clear why your have this `ref.removeAllObservers()` as observeSingleEvent doesn't leave an observe since it's a one-shot. There are better options than this `[String : NSDictionary]` - let's get Swifty and do `[String: Any]` or better yet, treat everything as a snapshot so you can access child values like this `let userToShow.username = snap.childSnapshot("userName").value as? String ?? "No User Name" – Jay Dec 17 '19 at 20:56
  • @vadian I am sure you know this but `self.collectionview.reloadData()` *is* happening on the main thread. [UI updates in Firebase closures are always run on the main thread](https://stackoverflow.com/questions/56314265/how-to-display-data-from-firebase-faster/56314312#56314312) – Jay Dec 17 '19 at 21:00
  • OP, I *discourage* multiple optional assignments on one line like this `if let username = value["username"] as? String, let imagePath =` because if one things fails, it all fails (which is probably this issue here). Better to split them out and provide default values in case, for example, a node is not found. `let result = some_optional ?? "No Value"` for each var. Makes troubleshooting a lot easier too as if you get No Value, you know you're missing a child node in Firebase. – Jay Dec 17 '19 at 21:04
  • @creeperspeak that is what my understanding is, but I guess other people don't think so. This same exact code works perfectly fine in my FeedViewController code so I'm wondering if I'm missing something. I asked this question to get more guidance on using Firebase because the Firebase docs are lacking in actually implementation of Firebase usage. They just give you basic one liners. – Nick Steen Dec 17 '19 at 21:18
  • @Paulw11 The string that gets printed out is a valid URL to an image. I use this same code in the feed section and the URLs are of the same format. The only issue is that this code seems to think there are nil values when the debugger clearly shows me the "user" dictionary is populated – Nick Steen Dec 17 '19 at 21:19
  • @Jay I will try splitting them out, but when I view the "user" dictionary in the debugger when I get a crash everything is populated and valid. I'm only trying to query one user at the moment so it is very easy to verify. – Nick Steen Dec 17 '19 at 21:21
  • If you print the dictionary value and don't get `nil` and then get `nil` when you try and initialise a URL then there is a problem with the format of the URL string. Do this - `let urlStr = user[indexPath.row].imagePath! print(urlStr) let url = URL(string:urlStr)!` - you will probably crash on the third line because the url can't be initialised. – Paulw11 Dec 17 '19 at 22:43
  • You mention you view your user 'dictionary'... there is no User dictionary. Are you meaning the object that's a User Class? On what line is your breakpoint and what error are you getting. Please update the question with that info as it's a bit vague as to where you are seeing the data, where the breakpoints are and what line is crashing. – Jay Dec 17 '19 at 22:47
  • Well what does "it is showing up as nil" mean exactly? – matt Dec 17 '19 at 22:49
  • What is your debug message? – Asmin Ghale Dec 18 '19 at 03:48

1 Answers1

-2

If you experience this error, redo the connections to your collection view Cell

Nick Steen
  • 71
  • 7