Taylor Simpson
Taylor Simpson

Reputation: 970

How to reload tableView data after data is passed by a Segue

I have two table views. One which the user clicks on and one where data is displayed. When the user clicks on a cell in the first table view a query is made to my firebase database and the query is stored in an Array. I then pass the data through a segue. I used a property observer so I know that the variable is being set. By using break points I was able to determine that my variable obtains its value right before the cellForRowAtIndexPath method. I need help displaying the data in my table view. I do not know where to reload the data to get the table view to update with my data. I am using Swift.

EDIT 2: I have solved my problem. I will post my first and second table views so that you can see my solution.

FirstTableView

import UIKit
import Firebase
import FirebaseDatabase

class GenreTableViewController: UITableViewController {

let dataBase = FIRDatabase.database()

var genreArray = ["Drama","Classic,Comic/Graphic novel","Crime/Detective","Fable,Fairy tale","Fantasy","Fiction narrative", "Fiction in verse","Folklore","Historical fiction","Horror","Humour","Legend","Magical realism","Metafiction","Mystery","Mythology","Mythopoeia","Realistic fiction","Science fiction","Short story","Suspense/Thriller","Tall tale","Western,Biography","Autobiography","Essay","Narrative nonfiction/Personal narrative","Memoir","Speech","Textbook","Reference book","Self-help book","Journalism", "Religon"]

var ResultArray: [NSObject] = []
var infoArray:[AnyObject] = [] 

override func viewDidLoad() {
    super.viewDidLoad()
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
}

// MARK: - Table view data source

override func numberOfSectionsInTableView(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 genreArray.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)

    // Configure the cell...

    cell.textLabel?.text = genreArray[indexPath.row]

    return cell
}

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

}

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

    let DestViewController: ResultTableViewController = segue.destinationViewController as! ResultTableViewController

    if segue.identifier == "letsGo" {
        if let indexPath = self.tableView.indexPathForSelectedRow {
            let tappedItem = self.genreArray[indexPath.row]
            DestViewController.someString = tappedItem 
        }  
    }
}

}

import UIKit
import Firebase
import FirebaseDatabase

class ResultTableViewController: UITableViewController {


let dataBase = FIRDatabase.database()
var SecondResultArray: [FIRDataSnapshot]! = []
var someString: String?{
    didSet {
      print("I AM A LARGE TEXT")
      print(someString)
    }
}

override func viewDidLoad() {

    let bookRef = dataBase.reference().child("books")

    bookRef.queryOrderedByChild("Genre")
        .queryEqualToValue(someString)
        .observeSingleEventOfType(.Value, withBlock:{ snapshot in
            for child in snapshot.children {

                self.SecondResultArray.append(child as! FIRDataSnapshot)
                //print(self.ResultArray)
            }

            dispatch_async(dispatch_get_main_queue()) {
                self.tableView.reloadData()
            }

        })

    super.viewDidLoad()

    // 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()
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

// MARK: - Table view data source

override func numberOfSectionsInTableView(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 SecondResultArray.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("cell2", forIndexPath: indexPath)

    // Configure the cell...

    let bookSnapShot: FIRDataSnapshot! = self.SecondResultArray[indexPath.row]

    let book = bookSnapShot.value as! Dictionary<String, String>

    let Author = book["Author"] as String!
    let Comment = book["Comment"] as String!
    let Genre = book["Genre"] as String!
    let User = book["User"] as String!
    let title = book["title"] as String!

    cell.textLabel?.numberOfLines = 0
    cell.textLabel?.lineBreakMode = NSLineBreakMode.ByWordWrapping

    cell.textLabel?.text = "Author: " + Author + "\n" + "Comment: " + Comment + "\n" + "Genre: " + Genre + "\n" + "User: " + User + "\n" +  "Title: " + title

    let photoUrl = book["bookPhoto"], url = NSURL(string:photoUrl!), data = NSData(contentsOfURL: url!)
        cell.imageView?.image = UIImage(data: data!)

    return cell
}

}

For better context and troubleshooting here is my current code for the tableView which is supposed to display data:

    import UIKit

    class ResultTableViewController: UITableViewController {

        var SecondResultArray: Array<NSObject> = []{
            willSet(newVal){ 
                print("The old value was \(SecondResultArray) and the new value is \(newVal)")
            }
            didSet(oldVal){
               print("The old value was \(oldVal) and the new value is \(SecondResultArray)")
               self.tableView.reloadData()        
            }
        }
        override func viewDidLoad() {
            print ("I have this many elements\(SecondResultArray.count)")
            super.viewDidLoad()
        }
        // MARK: - Table view data source
        override func numberOfSectionsInTableView(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 SecondResultArray.count
        }

        override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCellWithIdentifier("cell2", forIndexPath: indexPath)
            cell.textLabel?.text = SecondResultArray[indexPath.row] as? String
            return cell
        }
    }

Edit:

Here is my first table view controller. I have tried using the completion handler, but I can't call it correctly and I am constricted by the fact that my query happens in the didSelectRowAtIndexPath method. Please help.

import UIKit
import Firebase
import FirebaseDatabase

class GenreTableViewController: UITableViewController {


    let dataBase = FIRDatabase.database()

    var genreArray = ["Drama","Classic,Comic/Graphic novel","Crime/Detective","Fable,Fairy tale","Fantasy","Fiction narrative", "Fiction in verse","Folklore","Historical fiction","Horror","Humour","Legend","Magical realism","Metafiction","Mystery","Mythology","Mythopoeia","Realistic fiction","Science fiction","Short story","Suspense/Thriller","Tall tale","Western,Biography","Autobiography","Essay","Narrative nonfiction/Personal narrative","Memoir","Speech","Textbook","Reference book","Self-help book","Journalism", "Religon"]

    var ResultArray: [NSObject] = []
    var infoArray:[AnyObject] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        // 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()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

   // MARK: - Table view data source

   override func numberOfSectionsInTableView(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 genreArray.count
}


override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)

    cell.textLabel?.text = genreArray[indexPath.row]
    return cell
}


override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

    typealias CompletionHandler = (result:NSObject?, error: NSError?) -> Void

    func getData(completionHandeler: CompletionHandler){
        let bookRef = self.dataBase.reference().child("books")
        let GenreSelector = self.genreArray[indexPath.row]
        bookRef.queryOrderedByChild("Genre")
            .queryEqualToValue(GenreSelector)
            .observeSingleEventOfType(.Value, withBlock:{ snapshot in
                for child in snapshot.children {
                    print("Loading group \((child.key!))")

                    self.ResultArray.append(child as! NSObject)
                }
                print(self.ResultArray)

                self.performSegueWithIdentifier("letsGo", sender: self)
                self.tableView.reloadData()
            })
    }
}


override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    var DestViewController: ResultTableViewController = segue.destinationViewController as! ResultTableViewController
    DestViewController.SecondResultArray = self.ResultArray
}

Upvotes: 3

Views: 5003

Answers (6)

user3861282
user3861282

Reputation: 63

You can inject the data to the destination viewController in prepareForSegue Method of the first UIViewController and reload your UITableView in viewDidAppear. If you are getting your data asynchronously, have a completionHandler and reload it in the completionHandler. Here is an example.

  func fetchDataWithCompletion(response: (NSDictionary?, error:NSError?)-> Void) -> Void {
    //make the API call here 
    }

Upvotes: 2

Jay
Jay

Reputation: 35657

Based on additional code that was added to the post, the issue is a controller variable going out of scope.

So here's the issue

class MyClass {

  func setUpVars {
    let x = 1
  }

  func doStuff {
    print(x)
  }
}

Create a class and attempt to print the value of x

let aClass = MyClass()
aClass.setUpVars
aClass.doStuff

This will print nothing (conceptually) as once setUpVars ended, the 'x' variable went out of scope.

whereas

class MyClass {

  var x: Int

  func setUpVars {
    x = 1
  }

  func doStuff {
    print(x)
  }
}

will print the value of x, 1.

So the real solution is that your viewControllers need to 'stay alive' during the duration of your class (or app).

Here's the pattern. In the MasterViewController

import UIKit

    class MasterViewController: UITableViewController {

        var detailViewController: DetailViewController? = nil

then in your MasterViewController viewDidLoad (or wherever), create the detailViewController

 override func viewDidLoad() {
    super.viewDidLoad()
    let controllers = split.viewControllers //this is from a splitViewController
    self.detailViewController = 
        controllers[controllers.count-1].topViewController as? DetailViewController
 }

and from there you have it... use prepareForSegue to 'send' the data to the detailViewController

Just wanted to have this posted for future reference.

Upvotes: 0

Jay
Jay

Reputation: 35657

How about this:

Assume you have an array (myArray) populated from Firebase and stored in the first tableViewController. There's a second tableViewController and a segue connecting them.

We want to be able to tap on an item in the first tableviewController, have the app retrieve detailed data for the item from Firebase (a 'data' node) and display the detailed data in the second tableViewController.

Firebase structure

some_node
   child_node_0
     data: some detailed data about child_node_0
   child_node_1
     data: some detailed data about child_node_1

Within the second tableViewContoller:

var passedObject: AnyObject? {
    didSet {
      self.configView() // Update the view.
     }
}

Tapping an item in the first tableView calls the following function

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
   if segue.identifier == "showListInSecondTable" {
          if let indexPath = self.tableView.indexPathForSelectedRow {
              let tappedItem = myArray[indexPath.row] as! String
              let keyOfTappedItem = tappedItem.firebaseKey //child_node_0 for example
              doFirebase(keyOfTappedItem)
            }
        }
    }

and the prepareForSegue then calls the following which loads the data from firebase and when the snapshot returns within the block, it populates the passedObject property in the second tableView

func doFirebase(firebaseKey: String) {

  ref = myRootRef.childByAppendingPath("\(firebaseKey)/data")
  //if we want the detailed data for child_node_0 this would resolve
  //  to    rootRef/child_node_0/data
  ref.observeSingleEventOfType(.Value, { snapshot in
     let detailObjectToPass = snapshot.Value["data"] as! NSArray or string etc

     let controller = (segue.destinationViewController as! UINavigationController).myViewController as! SecondViewController
     controller.passedObject = detailObjectToPass
}

and of course in secondController, setting the passedArray calls didSet and sets up the view, and tells the tableView to reload itself, displaying the passed array.

func configView() {
   //set up the view and buttons
   self.reloadData()
}

I did this super quick so ignore the typos's. The pattern is correct and satisfies the question. (and eliminates the need for an observer to boot!)

P.S. this is way over coded but I wanted to demonstrate the flow and leveraging the asynchronous call to firebase to load the second tableView when the data was valid within the block.

Upvotes: 0

Gero
Gero

Reputation: 4424

Edit:

On second read, you are already using a completion handler, but I think you didn't see it. Let me correct your code above a bit:

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {


    let bookRef = self.dataBase.reference().child("books")
    let GenreSelector = self.genreArray[indexPath.row]
    bookRef.queryOrderedByChild("Genre")
        .queryEqualToValue(GenreSelector)
        .observeSingleEventOfType(.Value, withBlock:{ snapshot in
            // This here is your completion handler code!
            // I assume it is called asynchronously once your DB is done
            for child in snapshot.children {
                print("Loading group \((child.key!))")

                self.ResultArray.append(child as! NSObject)
            }
            print(self.ResultArray)

            self.performSegueWithIdentifier("letsGo", sender: self)
            // self.tableView.reloadData() // is this really needed
        })
    }
}

You defined a closure, but simply didn't call it. I don't see a reason for that anyways, assuming the block gets called once the database gives you your results. Am I missing something?


That's a good start already, but I think you didn't entirely get how to use a completion handler in this regard, but of course I may be wrong.

I built on top of user3861282's answer and created a small demo project at my github.

In short: You can do all inter-table-communication in the prepareForSegue: method of your first table view controller. Configure the second table view controller there (via its vars). Not any closures/completion handlers there yet.

Then in the second view controller's viewWillAppear: method, start the loading (including an animation if you want). I suggest something like NSURLSession that already defines a completion handler. In that you work with your data from remote, stop any loading animations and you're good.

If the completion handler must be defined in the first table view controller, you can even set it as a var in the second table view controller. That way you "hand over" the closure, i.e. "piece of code".

Alternatively, you can start animations and remote request in the first table view controller and then performSegueWithIdentifier once that is done. In your question you wrote that you want to load in the second table view controller, however, if I understood you correctly.


Your code above properly defines a closure that expects a completion handler (which is also a closure and so kind of doubles what you want), but you never actually call it somewhere. Nor do you call the completion handler in the closure. See my demo for how it can work.

The project I wrote illustrates just one way to do it (minus animations, not enough time). It also shows how you can define your own function expecting a completion handler, but as I said, the standard remote connections in the framework provide one anyways.

Upvotes: 0

chickenparm
chickenparm

Reputation: 1620

Try updating your closure to include this:

dispatch_async(dispatch_get_main_queue()) {
    self.tableView.reloadData()
}

Upvotes: 0

tequila slammer
tequila slammer

Reputation: 2881

You can reload the TableView with [tableView reloadData];.

Upvotes: -1

Related Questions