1

So I have a tableView that has a header element and some cells in it. I am pulling the cells from the server and then reloading the tableview after I am done. Well that is the goal atleast right now I am reloading the tavleView after every single append to an array. I ultimately want to reload it only after all the data is pulled basically once.The function

self.fetchEventsFromServer()

handles the work of pulling the data. I read up on dispatchGroups and figured that would be the right way to go but I don't know how to go about doing it.

import UIKit
import Firebase

class FriendsEventsView: UITableViewController{
    var cellID = "cellID"
    var friends = [Friend]()
    var followingUsers = [String]()
    //label that will be displayed if there are no events
    var currentUserName: String?
    var currentUserPic: String?
    var currentEventKey: String?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "Friends Events"
        view.backgroundColor = .white
        // Auto resizing the height of the cell
        tableView.estimatedRowHeight = 44.0
        tableView.rowHeight = UITableViewAutomaticDimension
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: #imageLiteral(resourceName: "close_black").withRenderingMode(.alwaysOriginal), style: .done, target: self, action: #selector(self.goBack))
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
        self.tableView.tableFooterView = UIView(frame: CGRect.zero)
        fetchEventsFromServer { (error) in
            if error != nil {
                print(error)
                return
            } else {
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            }
        }

    }

    @objc func goBack(){
        dismiss(animated: true)
    }
    override func numberOfSections(in tableView: UITableView) -> Int {
        print(friends.count)
        return friends.count
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//        return friends[section].collapsed ? 0 : friends[section].items.count
        return 1
    }
    func tableView(_ tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return UITableViewAutomaticDimension
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
        cell.textLabel?.text = "Something to fill Section: \(indexPath.section) Row: \(indexPath.row)"
        return cell
    }

    override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "header") as? CollapsibleTableViewHeader ?? CollapsibleTableViewHeader(reuseIdentifier: "header")
        print(section)
        header.arrowLabel.text = ">"
        header.setCollapsed(friends[section].collapsed!)
        print(friends[section].collapsed!)
        header.section = section
       // header.delegate = self
        header.friendDetails = friends[section]
        return header
    }
    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 50
    }

   func fetchEventsFromServer(_ completion: @escaping (_ error: Error?) -> Void ){
        //will grab the uid of the current user

        guard let myUserId = Auth.auth().currentUser?.uid else {
            return
        }
        let ref = Database.database().reference()
        //checking database for users that the current user is following
        ref.child("following").child(myUserId).observeSingleEvent(of: .value, with: { (followingSnapshot) in
           //handling potentail nil or error cases
            guard let following = followingSnapshot.children.allObjects as? [DataSnapshot]
                else {return}

            //validating if proper data was pulled
            for followingId in following {
                print(followingId.key)
                ref.child("users").child(followingId.key).observeSingleEvent(of: .value, with: { (userInfoSnapShot) in
                    guard let followingUserInfo = userInfoSnapShot.children.allObjects as? [DataSnapshot] else {
                        return
                    }
                    //validating if proper data was pulled for each follower
                    for currentUserInfo in followingUserInfo {
                        if currentUserInfo.key == "username"{
                            self.currentUserName = currentUserInfo.value as! String
                            print(self.currentUserName)
                            var friend = Friend(friendName: self.currentUserName!, imageUrl: self.currentUserPic!)
                            self.friends.append(friend)
                        }
                        if currentUserInfo.key == "profilePic"{
                            self.currentUserPic = currentUserInfo.value as! String
                            print(self.currentUserPic)
                        }



                    }

                }, withCancel: { (err) in
                    completion(err)
                    print("Couldn't grab info for the current list of users: \(err)")
                })
                completion(nil)
            }

        }) { (err) in
            completion(err)
            print("Couldn't grab people that you are currently following: \(err)")
        }
    completion(nil)

    }
}

Any idea on how I would go about accomplishing this in swift it's really bugging me

Snapshot of following strucutre 
 "following" : {
    "Iwr3EWqFBmS6kYRjuLW0Pw0CRJw2" : {
      "CW1AIDxM43Ot3C1GtsyhQ0Zzwof2" : true
    },
    "OYWgNjYHEtX6EatkolPO5YXt6Rw2" : {
      "nlSbmr1CXPbtuaUALNnftdHrbSt1" : true,
      "qYSDao0zhFbLrzd0IJBafi7qdis2" : true
    },
    "nRrGzLFt3TeN4OOrwTe0RjHQoF13" : {
      "CW1AIDxM43Ot3C1GtsyhQ0Zzwof2" : true,
      "XV62sIs7anaGaoo0Wr9kooC8FDP2" : true,
      "r51UQXn4Q2WcPWIhXIG3dhZTHkX2" : true
    },
    "nbmheFEPmBerm5avZwnriGJkaK12" : {
      "nlSbmr1CXPbtuaUALNnftdHrbSt1" : true
    },
    "nlSbmr1CXPbtuaUALNnftdHrbSt1" : {
      "OYWgNjYHEtX6EatkolPO5YXt6Rw2" : true,
      "qYSDao0zhFbLrzd0IJBafi7qdis2" : true
    },
    "qYSDao0zhFbLrzd0IJBafi7qdis2" : {
      "nlSbmr1CXPbtuaUALNnftdHrbSt1" : true
    },
    "wdciX8B2LeUy2NyDwU5cjLog5xx2" : {
      "72297UgQllfrEaAQnUCPKuQMv933" : true,
      "nbmheFEPmBerm5avZwnriGJkaK12" : true
    }
  }
Shivam Tripathi
  • 1,405
  • 3
  • 19
  • 37
Ron Baker
  • 381
  • 6
  • 16
  • It's an interesting question, but can you trim the code down just to the essentials? Probably don't need `viewDidLoad` for example. – jscs Feb 13 '18 at 23:41
  • You're calling it in the for loop. Call it after `withCancel:{}) ` after the next } – Jake Feb 14 '18 at 00:06
  • It would be helpful to see how you've modeled the "following" section of your database. It seems there is a much easier way to parse that info in a more efficient way, so seeing the structure will help come to an answer. – creeperspeak Feb 14 '18 at 00:09
  • @creeperspeak I am uploading that now – Ron Baker Feb 14 '18 at 00:42
  • @creeperspeak please see edits – Ron Baker Feb 14 '18 at 00:45

3 Answers3

2

You want to add a completion handler to the func fetchEventsFromServer() function and call reloadData() when it's done.

func fetchEventsFromServer(_ completion: @escaping (_ error: Error?) -> Void ) {
......... 
    withCancel: { (err) in
                completion(err)
                print("Couldn't grab info for the current list of users: \(err)")
             })
         } completion(nil) // Right here I would think
     }) { (err) in
}

Then when you call

fetchEventsFromServer { (error) in 
    if error != nil {
        print(error)
        return
    } else {
        self.tableView.reloadData()
    }
}

Also, you don't need to add self. to a lot of what you have it added to in your code. Only when it's nested in closures and things like that.

creeperspeak
  • 5,403
  • 1
  • 17
  • 38
Jake
  • 2,126
  • 1
  • 10
  • 23
  • No problem! Happy to help. – Jake Feb 14 '18 at 00:43
  • I don't know if that matters but creeperspeak said it would help with coming up with a more efficient solution what do you think I uploaded my snapshot of following – Ron Baker Feb 14 '18 at 00:45
  • It could be more concise, but if it works you can tidy it up later. We could talk about BigO and the most efficient way to handle it but that type of skill comes in time. I would try to abstract a bit. Move bits of code into other functions to make it easier to follow. But again, if it works... you can mess with that later. I like to complete a task until everything works and clean up what I can when I'm done. – Jake Feb 14 '18 at 00:50
  • Try putting it in a dispatch.main.async{} – Jake Feb 14 '18 at 01:01
  • umm still nothing – Ron Baker Feb 14 '18 at 01:03
  • Interesting. And no, I didn't see the edits... But there is a lot up there to look through so I'm not sure I'd notice to be honest. Add a breakpoint on the `fetchEventsFromServer()` function and step through it and let me know what you see. – Jake Feb 14 '18 at 01:05
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/165100/discussion-between-ron-baker-and-jake). – Ron Baker Feb 14 '18 at 01:05
1

Here's an extremely simplified example as a guide (my loop of 20 is arbitrary, only there for an example):

let group = DispatchGroup()
for _ in 0...20
{
    group.enter()

    // Do your server call and in it's completion block call..
    group.leave()
}
let result = group.wait(timeout: .now() + 60)

The basic idea is to create the group, at the beginning of your for loop you "enter" the group, each time you get a response from your server you "leave" the group, and then outside the loop the "wait" command basically blocks the method from continuing until you've "left" as many times as you "entered" the group (or you take too long).

Note - Since this will BLOCK the thread it's on, you most definitely don't want to do this in viewDidLoad because it will lock your entire UI until all data is received. Kick this off in a non-main thread.

ghostatron
  • 2,620
  • 23
  • 27
1

If you don't know in advance how many times your data should be updated (how many records you'll have) you can use a debouncer instead of calling reloadData every time. For a debouncer implementation look How can I debounce a method call?

For example using this answer https://stackoverflow.com/a/40634366/7132300 you can do

let debouncedFunction = debounce(interval: 500, queue: DispatchQueue.main, action: { (_: String) in
    self.tableView.reloadData()
})

and then call debouncedFunction("dummy") instead of reloadData. Feel free to adjust the timeout parameter for your needs. Also it should be easy to get rid of that dummy string parameter since you don't need it.

algrid
  • 5,600
  • 3
  • 34
  • 37