0

I am trying to load data from firestore(google database) and want to show on tableview.

so in the first VC, by prepare function, get data from database, and transfer to second VC(tableview). but There is one problem. I learned that prepare function goes before viewdidload, in my application, prepare function goes after second VC load.

here's my code.

first VC

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let docRef = db.collection("Posting").getDocuments(){(querySnapshot, err) in
        if let err = err{
            print("errror getting documents")
        }else{
            for document in querySnapshot!.documents{
                print("\(document.documentID) => \(document.data())")
                self.savePostings.append(document.data())
            }
            print("\n\n\(self.savePostings)")
        }
        let vc = segue.destination as! PostingListTableViewController
        vc.updatedPostings = self.savePostings
        vc.testPrint = "잉 기모찌"
        print("배열 전달 확인\n\(vc.updatedPostings)\n\n\(self.savePostings)")
    }
    
}

Second VC (Tableview)

class PostingListTableViewController: UITableViewController {

//private var postings: Array<[String:Any]> = []
private var documents: [DocumentSnapshot] = []
var updatedPostings: Array<[String:Any]?> = []
var testPrint:String = ""

override func viewDidLoad() {
    super.viewDidLoad()
    print("view did load")
    // Uncomment the following line to preserve selection between presentations
    // self.clearsSelectionOnViewWillAppear = false

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem
}

// MARK: - Table view data source



override func numberOfSections(in tableView: UITableView) -> Int {
    // #warning Incomplete implementation, return the number of sections
    return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // #warning Incomplete implementation, return the number of rows
    return updatedPostings.count
}


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "myTableCell", for: indexPath)
    cell.textLabel?.text = updatedPostings[indexPath.row]!["text"] as! String

    // Configure the cell...

    return cell
}

}

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
이원석
  • 13
  • 4
  • Generally you should not perform asynchronous tasks in `prepare(for` which is the reason of the confusion I guess. – vadian Aug 26 '21 at 08:12
  • Your segue loads after you async db call. Put your view controller code inside the async callback. – Desdenova Aug 26 '21 at 08:14

2 Answers2

1

as @vadian correctly said, your problem is that you're making an async call.

prepare(for segue is called before viewDidLoad, but you're updating your properties some time after that, when your request finishes, and that's after viewDidLoad.

Instead of that I suggest you the following:

  1. Remove your segue, add identifier to the destination view controller

  2. Inside tableView:didSelectRowAtIndexPath: run your getDocuments(or inside IBAction if this is a button segue)

    2.1. you can show some progress indicator so user wold know the reason of delay

  3. In completion create your view controller from storyboard using instantiateViewControllerWithIdentifier and present it manually. You don't need to wait for prepare(for segue to set your properties in this case.

If your segue is calling from the cell, you can add your view controller as a delegate, like this:

then you need to conform your view controller to UITableViewDelegate, and didSelectRowAt method will be called when user press a cell. You can get cell number from indexPath.row

extension PostingListTableViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let docRef = db.collection("Posting").getDocuments(){(querySnapshot, err) in
            if let err = err{
                print("errror getting documents")
            }else{
                for document in querySnapshot!.documents{
                    print("\(document.documentID) => \(document.data())")
                    self.savePostings.append(document.data())
                }
                print("\n\n\(self.savePostings)")
            }
            // not sure if completion of getDocuments is called on main thread, if it does - you don't need this line
            DispatchQueue.main.async {
                let vc = storyboard!.instantiateViewController(identifier: "storyboard_identifier") as! PostingListTableViewController
                vc.updatedPostings = self.savePostings
                vc.testPrint = "잉 기모찌"
                present(vc, animated: true)
                print("배열 전달 확인\n\(vc.updatedPostings)\n\n\(self.savePostings)")
            }
        }
    }
}

If you're performing this segue from a plain button, not from a cell, you can do the same with @IBAction:

@IBAction @objc func push() {
    let docRef = db.collection("Posting").getDocuments(){(querySnapshot, err) in
                if let err = err{
                    print("errror getting documents")
                }else{
                    for document in querySnapshot!.documents{
                        print("\(document.documentID) => \(document.data())")
                        self.savePostings.append(document.data())
                    }
                    print("\n\n\(self.savePostings)")
                }
                // not sure if completion of getDocuments is called on main thread, if it does - you don't need this line
                DispatchQueue.main.async {
                    let vc = storyboard!.instantiateViewController(identifier: "storyboard_identifier") as! PostingListTableViewController
                    vc.updatedPostings = self.savePostings
                    vc.testPrint = "잉 기모찌"
                    present(vc, animated: true)
                    print("배열 전달 확인\n\(vc.updatedPostings)\n\n\(self.savePostings)")
                }
            }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • I give an upvote as it is quite similar to what I've proposed. – Wilfried Josset Aug 26 '21 at 09:33
  • Thanks for your answer. But I solved by Wilfried's way. Comparing two solutions, I cannot understand some codes on your answer ( extenstion, UITableViewDelegate) Maybe I have to study harder – 이원석 Aug 27 '21 at 06:42
  • @이원석 I just guess as you have a table view, and that this segue may come from cell tap. I've updated my answer to show how to add a delegate, so my extension will work. Also I've added an alternative version with `@IBAction` – Phil Dukhov Aug 27 '21 at 07:02
0

What I would do first is to DELETE ❌ the segue you've probably created by drag and drop from the button on VC1 to VC2. Replace it by a regular segue which is not attached to any UI component so that you can trigger it manually in code (from its identifier, don't forget to provide one). It will allow you to first perform some asynchronous code, and only when getting the callback trigger the navigation (if data fetch is successful).

To create such a segue:

  1. click on your VC1 in the storyboard => A small rectangle with icons should be now displayed right above the VC1
  2. Ctrl + drag from the yellow icon (probably at the most left of that rectangle) to the VC2 (on main view, does not rally matter where you drop it as long as it is on VC2)
  3. Provide identifier after having clicked on the newly created segue so that you can trigger it from code

That was for the segue, but now, when to trigger it?

  1. Create an an @IBAction on the button which is suppose to trigger the fetch + navigation (or equivalent, like didSelectRowAtIndexPath)
  2. This IBAction should call another method like the following:
private func fetchPostingAndNavigateIfSuccessful() {
   // Should probably first set the activity indicator to `.startAnimating()`
   let docRef = db.collection("Posting").getDocuments() { [weak self] querySnapshot, error in
       // Should probably set the activity indicator to `.stopAnimating()`
       guard error != nil,
             let documents = querySnapshot?.documents else {
             print("error getting documents") //probably a good place to display an alert
             return
       }
       let postingsData = documents.map { $0.data() }
       self?.performSegue(withIdentifier: "NavigateToPostingsSegue", sender: postingsData)
   }
}

Your prepare for segue would then look like that:


override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    super.prepare(for: segue, sender: sender)
    if let postingListTableViewController = segue.destination as? PostingListTableViewController,
       let postingsData = sender as? [[String:Any]] {
        postingListTableViewController.updatedPostings = postingsData
    }
}

Wilfried Josset
  • 1,096
  • 8
  • 11
  • Thanks to your feedback, I solved the problem. but still I have some questions. Is segue triggered by UI component not appropriate for asynchronous works? Since I'm not good at programming yet, I cannot distinguish which code is good for sync/async works. – 이원석 Aug 27 '21 at 06:39
  • @이원석 If it solved your problem please mark as answer / upvote. However I'm happy to help you further. To keep it short, it depends what you want to do. If you trigger a segue with UIButton, you have to keep in mind that the navigation will perform immediately. If you decide to go that way, you can perform asynchronous code after the navigation has been completed like in one of the lifecycle method of the 2nd VC. However, it doesn't really make sense to present a screen if you can't present anything inside it, that is why I would adise to do it before, and to perform the segue programmatically. – Wilfried Josset Aug 27 '21 at 09:16
  • I would highly advised to not do anything complex or trigger any process in the method prepare(for segue), that's not its purpose. so to 2 options 1. either you perform the segue via the ctrl + drag from ui component and you trigger the async code into the lifecycle method of 2nd vc 2. or you first fetched the data in a method triggered by the IBAction and perform segue (or any other mean of navigation) programmatically AFTER you got the data (in the callback / closure / completion) – Wilfried Josset Aug 27 '21 at 09:19