2

I have this closure which I use to populate my array and dictionary. However, when I try to use it outside the function, it's empty. I understand that this closure works on an asynchronous thread, so is it right to assume that I try to access that variable before it's been populated? As a result, I get an empty array. Here is my code.

class HomeCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchBarDelegate, UIGestureRecognizerDelegate {

var entries = [String: DiaryEntry]()
var entryIDS =  [String]()

var searchController: UISearchController!

override func viewDidLoad() {
    super.viewDidLoad()

    // Register cell classes
    self.collectionView!.register(DiaryCell.self, forCellWithReuseIdentifier: "homeCell")
    collectionView?.backgroundColor = UIColor.white
    navigationController?.hidesBarsOnSwipe = true

    if let userID = FIRAuth.auth()?.currentUser?.uid {
        FirebaseService.service.getUserEntriesRef(uid: userID).observe(.value, with: { [weak weakSelf = self] (snapshot) in
            let enumerator = snapshot.children
            while let entry = enumerator.nextObject() as? FIRDataSnapshot {
                weakSelf?.entryIDS.append(entry.key)
                weakSelf?.entries[entry.key] = DiaryEntry(snapshot: entry)
            }
            weakSelf?.entryIDS.reverse()
            weakSelf?.collectionView?.reloadData()
        })
        print("Entries: \(entryIDS.count) ")
    }
    // Do any additional setup after loading the view.
}

What's the best way to deal with such a multithreaded execution?

Vandan Patel
  • 1,012
  • 1
  • 12
  • 23

3 Answers3

1

I follow the coding standards (for Swift) of Raywenderlich and his team. If I'm going to re-write your code to have a strong self, it would be like this:

class HomeCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchBarDelegate, UIGestureRecognizerDelegate {

    var entries = [String: DiaryEntry]()
    var entryIDS =  [String]()

    var searchController: UISearchController!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Register cell classes
        self.collectionView!.register(DiaryCell.self, forCellWithReuseIdentifier: "homeCell")
        collectionView?.backgroundColor = UIColor.white
        navigationController?.hidesBarsOnSwipe = true

        if let userID = FIRAuth.auth()?.currentUser?.uid {
            FirebaseService.service.getUserEntriesRef(uid: userID).observe(.value, with: {
                [weak self] (snapshot) in

                guard let strongSelf = self else {
                    return
                }

                let enumerator = snapshot.children
                while let entry = enumerator.nextObject() as? FIRDataSnapshot {
                    strongSelf.entryIDS.append(entry.key)
                    strongSelf.entries[entry.key] = DiaryEntry(snapshot: entry)
                }
                strongSelf.entryIDS.reverse()
                strongSelf.collectionView?.reloadData()
            })
            print("Entries: \(entryIDS.count) ")
        }
        // Do any additional setup after loading the view.
}

I hope this works. I'm writing my code like this too when connecting to any APIs, such as Firebase and Alamofire.

Glenn Posadas
  • 12,555
  • 6
  • 54
  • 95
  • I will try it, but you think strong self is the issue here? – Vandan Patel Feb 03 '17 at 03:27
  • Probably. I remember when I was new to Swift, I couldn't access a property in my class inside a closure too. – Glenn Posadas Feb 03 '17 at 03:34
  • But isn't it the same as saying weak weakSelf = self. I saw that in one of the Stanford university's iOS course. I could be wrong. – Vandan Patel Feb 03 '17 at 03:38
  • Yea, I tested your way of handling self inside the closure, and it worked. Back to your question, you can use and access your data sources even before they are populated, since you already have initialized them. I bet there's an error in your fetching of data? or handling the snapshot. Either of the two. Can you do more testings and printing objects by using breakpoints? – Glenn Posadas Feb 03 '17 at 03:59
  • I can use them, but they are empty. However, you are right. Let me do some debugging... – Vandan Patel Feb 03 '17 at 04:11
0

Use Dispatch Groups to keep track of when you're done appending all the elements to your array, then make a notify callback that'll automatically be called when they're all added.

class HomeCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, UISearchBarDelegate, UIGestureRecognizerDelegate {

var entries = [String: DiaryEntry]()
var entryIDS =  [String]()

let dispatchGroup = DispatchGroup()

var searchController: UISearchController!

override func viewDidLoad() {
    super.viewDidLoad()

    // Register cell classes
    self.collectionView!.register(DiaryCell.self, forCellWithReuseIdentifier: "homeCell")
    collectionView?.backgroundColor = UIColor.white
    navigationController?.hidesBarsOnSwipe = true
    self.dispatchGroup.enter()
    if let userID = FIRAuth.auth()?.currentUser?.uid {
        FirebaseService.service.getUserEntriesRef(uid: userID).observe(.value, with: { [weak weakSelf = self] (snapshot) in
            let enumerator = snapshot.children
            while let entry = enumerator.nextObject() as? FIRDataSnapshot {
                weakSelf?.entryIDS.append(entry.key)
                weakSelf?.entries[entry.key] = DiaryEntry(snapshot: entry)
            }
            self.dispatchGroup.leave()
        })
        self.dispatchGroup.notify(queue: DispatchQueue.main, execute: {
             print("Entries: \(entryIDS.count) ")
             weakSelf?.entryIDS.reverse()
             weakSelf?.collectionView?.reloadData()
        })
    }
    // Do any additional setup after loading the view.
}

I'd also recommend just using self instead of weakSelf?.

MarksCode
  • 8,074
  • 15
  • 64
  • 133
  • Would this work if I want to access this array and dictionary in a method called menuButtonPressed(). What I am trying to achieve here is, when user clicks on the menuImage on the screen, I would call this menuButtonPressed() method, and I want to access that array and dictionary into this method. – Vandan Patel Feb 03 '17 at 03:03
  • Once your Firebase fetch happens your arrays will be populated, so it should work. The good thing about dispatch groups is they don't block the main thread. – MarksCode Feb 03 '17 at 03:06
  • I am sorry for the disagreement, but I still don't see how DispatchGroup would help my situation. Just like closure, it won't block main thread, and I would end up accessing those variables before they are available or populated. I am talking about accessing them into some method. Above code would work when you try and print("Entries: \(entryIDS.count) "). – Vandan Patel Feb 03 '17 at 03:11
  • Maybe I don't understand your situation. If you want to fetch something from your database once a user has pressed a button, then populate a collectionView once that data has been fetched, this is the way to do it. As soon as the data has been retrieved from your database the `notify` callback will reload the collectionView with the populated arrays. – MarksCode Feb 03 '17 at 03:17
  • I am sorry, I should have done better explaining my problem. Let me try again : fetching firebase data works fine. When my collection view loads, each of the cells have this menu button. When they click that menu button on any of the cells, I want to perform operations using that array and dictionary. Unfortunately, they both come empty. So my guess is my method doesn't get data populated by that closure. I hope this helps you understand my problem. – Vandan Patel Feb 03 '17 at 03:22
  • I'd recommend just waiting to populate the collectionView until the `notify` callback is called, and use an activity indicator to let the user know that data is being loaded. – MarksCode Feb 03 '17 at 03:25
0

I just came across your problem, while searching for a solution of the same problem and i finally managed to figure out. so, i will try to answer it would be helpful for many others coming behind us.

The problem is that the array exists only inside the closure. So, the solution is to make an array outside of viewDidLoad and set it using once you have the complete array, then use didSet to set entryIDS

var entryIDS =  [String]() {
    didSet {
        //something
     }
}
bona912
  • 589
  • 2
  • 6
  • 14