Data loading asynchronously and not displaying in UITabBarController TableView? - ios

My application is a UITabBarController application and when it first begins, it needs to make a call my Firebase database so that I can populate the UITableView within one of the tabs in the UITabBarController. However, I noticed that the first time I login and go to the TabBarController, the data does not show. I have to go from that tab to another tab, and then back to the original tab to have the data be displayed. However, I want it so that the data displays the first time around. I understand this is an error with the fact that Firebase asynchronously grabs data and that the view loads before all the data is processed but I just can't seem to get it to work as desired.
I tried to query for all the values we want first before we perform the segue, store them into an array, and then send that array to a predefined array in OffersView but that did not seem to work. Here is my attempt:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if(segue.identifier == "OffersView"){
let barViewControllers = segue.destinationViewController as UITabBarController
let navigationOfferController = barViewControllers.viewControllers![0] as UINavigationController
let offersViewController = navigationOfferController.topViewController as OffersView
offersViewController.offers = offersQuery()
}
func offersQuery() -> [Offer]{
firebaseRef = Firebase(url:"https://OffersDB.firebaseio.com/offers")
//Read the data at our posts reference
firebaseRef.observeEventType(FEventType.ChildAdded, withBlock: { (snapshot) -> Void in
let restaurant = snapshot.value["restaurant"] as? String
let offer = Offer(restaurant: restaurant)
//Maintain array of offers
self.offers.append(offer)
}) { (error) -> Void in
println(error.description)
}
return offers
}
Any help would be much appreciated!
Edit: Im trying to use a completion handler everytime the childAdded call occurs and I am trying to do it like so but I can't seem to get it to work. I get an error saying: 'NSInternalInconsistencyException', reason: 'attempt to insert row 1 into section 0, but there are only 1 rows in section 0 after the update
setupOffers { (result) -> Void in
if(result == true){
var row = self.offers.count
self.tableView.beginUpdates()
var indexPaths = NSIndexPath(forRow: row, inSection: 0)
println(indexPaths)
self.tableView.insertRowsAtIndexPaths([indexPaths], withRowAnimation: UITableViewRowAnimation.Bottom)
}
}

You should segue normally, and inside the UITableViewController, perform the query. Once the query callback is called, you can go ahead and reload the table with -reloadData so it will populate the cells.

Related

Core data object get saved, but doesn't show up in table until I pop to related object table and then come forward

I have three view controllers embedded in a navigation controller.
The first view controller is a list of tests, which use fetchedResultsController to populate the table.
The second view controller is a detail view controller, which allows adding a new test, editing it's detail, saving the new test or adding Questions to the test. It also includes a table of questions, which is also populated with a fetchedResultsController.
The problem I have is that when I have an existing Test which is saved, then when I go to add on questions, they correctly populate the table when I pop back to the TestDetailsVC, however, if I'm adding a new test, and then adding new questions to it, the test saves to core data, then in the QuestionDetailsVC questions also save to core data. However, when I pop back to the TestDetailsVC, the new questions don't populate the table. However, when I navigate back to the TestListVC and then go forward, the questions then populate the table correctly. I'm trying to get the questions to populate the table the first time I pop back to the table. Shouldn't the table re-fetche this data when the view loads, and the table work correctly when I navigate back since these objects are saved in core data? Why does it work correctly with a previously saved test object, but not work with a newly saved test object?
My code for saving the new test in the TestDetailsVC is:
//if we tap on a row, then we select that question to edit
//if a question is selected, we will pass that information over to
//the question editor view so it can be edited
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//make sure there is at least one question
//objs = means object selected
if let objs = controller.fetchedObjects, objs.count > 0 {
//if there is, keep track of the test which is selected
let question = objs[indexPath.row]
//pass along that test to the editor to be edited
//the sender is the selected test at that particular row
performSegue(withIdentifier: "EditQuestionSegue", sender: question)
}
}
//we need to get ready to do the segue before we call it
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let identifier = segue.identifier {
switch identifier {
case "EditQuestionSegue":
//find the question needing to be edited
//pass this along to the question editing view controller
if let destination = segue.destination as? QuestionDetailsVC {
if let question = sender as? Question {
destination.questionToEdit = question
//pass along the test that the question is
//related to also
if let test = testToEdit {
destination.testToEdit = test
}
}
}
case "AddNewQuestionSegue":
//if no test to edit got passed in, it means
//we're now editing a new test
//once we type in the test title, then we have to
//make sure the test has been saved if it's new test
//before we go on to edit questions
if testToEdit == nil {
saveTestBeforeAddingQuestion()
//make sure we set up as if new
//before we start adding questions
//these are called in the view did load
}
if let destination = segue.destination as? QuestionDetailsVC {
if let test = testToEdit {
destination.testToEdit = test
}
}
default:
print("no segue this time")
}
}
}
func saveTestBeforeAddingQuestion() {
var test: Test!
//if there's not a passed in value into the testToEdit core data
//object test entity, then we're going to edit as if new
if testToEdit == nil {
//then instantiate a new test object ready to be written to
test = Test(context: context)
} else {
test = testToEdit
}
if let title = titleTextField.text {
test.title = title
}
if let abrevTitle = abrevTitleTextField.text {
test.abrevTitle = abrevTitle
}
if let author = authorTextField {
test.author = author.text
}
if let publisher = publisherTextField {
test.publisher = publisher.text
}
ad.saveContext()
//since we have now saved a new test
//let's put that test into our testToEdit variable
//so we can pass it along to the next view controller
//during our segue
if let newTestCreated = test {
testToEdit = newTestCreated
}
}
My code for saving the new question is:
#IBAction func savePressed(_ sender: UIButton) {
var question: Question!
//if there's not a passed in value into the questionToEdit core data
//object test entity, then we're going to edit as if new
if questionToEdit == nil {
//then instantiate a new test object ready to be written to
question = Question(context: context)
} else {
question = questionToEdit
}
//make sure to relate the question added to the testToEdit test
question.test = self.testToEdit
//if there is something in the sentence text field
//then assign that value to the question.sentence attribute
//of the sentence entity in our core data context
if let sentence = questionSentenceTextField.text {
question.sentence = sentence
}
if let identifier = questionIdentifierTextField.text {
question.identifier = identifier
}
if let displayOrder = questionDisplayOrderTextField.text {
question.displayOrder = Int(displayOrder) as NSNumber?
}
// save the context
ad.saveContext()
_ = navigationController?.popViewController(animated: true)
}
I spent days trying to get the question table to load, but finally discovered that it wasn't loading back up because when I pop back to the view controller it doesn't automatically reload the table with the fetchedResultsController. So I had to place a call to refetch the data in the viewWillAppear method and then reload the tableView:
(TestDetailsVC)
override func viewWillAppear(_ animated: Bool) {
guard testToEdit != nil else {return}
attempFetch()
tableView.reloadData()
}

iOS Swift: best way to pass data to other VCs after query is completed

Context: iOS App written in Swift 3 powered by Firebase 3.0
Challenge: On my app, the user's currentScore is stored on Firebase. This user can complete/un-complete tasks (that will increase/decrease its currentScore) from several ViewControllers.
Overview of the App's architecture:
ProfileVC - where I fetch the currentUser's data from Firebase & display the currentScore.
⎿ ContainerView
⎿ CollectionViewController - users can update their score from here
⎿ DetailsVC - (when users tap on a collectionView cell) - again users can update their score from here.
Question: I need to pass the currentScore to the VCs where the score can be updated. I thought about using prepare(forSegue) in cascade but this doesn't work since it passes "nil" before the query on ProfileVC is finished.
I want to avoid having a global variable as I've been told it's bad practice.
Any thoughts would be much appreciated.
Why don't you create a function that will pull all data before you do anything else.
So in ViewDidLoad call...
pullFirebaseDataASYNC()
Function will look like below...
typealias CompletionHandler = (_ success: Bool) -> Void
func pullFirebaseDataASYNC() {
self.pullFirebaseDataFunction() { (success) -> Void in
if success {
// Perform all other functions and carry on as normal...
Firebase function may look like...
func pullFirebaseDataFunction(completionHandler: #escaping CompletionHandler) {
let refUserData = DBProvider.instance.userDataRef
refUserData.observeSingleEvent(of: .value, with: { snapshot in
if let dictionary = snapshot.value as? [String: AnyObject] {
self.userCurrentScore = dictionary["UserScore"] as! Int
completionHandler(true)
}
})
}
Then when you segue the information across...
In ProfileVC
Create 2 properties
var containerVC: ContainerVC!
var userCurrentScore = Int()
Include the below function in ProfileVC...
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ProfileToContainerSegue" {
let destination = segue.destination as! ContainerVC
containerVC = destination
containerVC.userCurrentScore = userCurrentScore
}
}
In ContainerVC create a property
var userCurrentScore = Int()
Ways to improve could be an error message to make sure all the information is pulled from Firebase before the user can continue...
Then the information can be segued across the same way as above.
Try instantiation, first embed a navigation controller to your first storyboard, and then give a storyboardID to the VC you are going to show.
let feedVCScene = self.navigationController?.storyboard?.instantiateViewController(withIdentifier: "ViewControllerVC_ID") as! ViewController
feedVCScene.scoreToChange = current_Score // scoreToChange is your local variable in the class
// current_Score is the current score of the user.
self.navigationController?.pushViewController(feedVCScene, animated: true)
PS:- The reason why instantiation is much healthier than a modal segue storyboard transfer , it nearly removes the memory leaks that you have while navigating to and fro, also avoid's stacking of the VC's.

Moving tableView data to another tableView with separate Entity name

I am trying to use a button to move an entire section of my tableView at once. I want it to move all of the selected rows to another tableView and delete them from the first. The first tableView data is saved in SList the second is SCList. Both Entities have attributes item, qty, desc just the first letters change from sl to sc (i.e. slitem to scitem). The problem I am having is getting the data to change from the SList entity to the SCList entity. How do I get it to remove the data from SList entity and change to SCList and add it to the respective tableView?
Button Code:
#IBAction func Move(sender: AnyObject) {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell") as! SLTableViewCell
let passValue = cell.cellLabel!.text
print(passValue)
self.performSegueWithIdentifier("moveToSC", sender: self)
}
Segue Code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if segue.identifier == "editItem" {
let SListController:SLEdit = segue.destinationViewController as! SLEdit
SListController.item = selectedItem
}
if segue.identifier == "moveToSC" {
let SCListController:SCart = segue.destinationViewController as! SCart
SCListController.item = item
}
I commented out the "if segue.identifier == "moveToSC"" code and it segues but doesn't pass any information. All that prints is "Optional("Label")" and nothing passes or deleted.
I'm not sure if any other code is needed. If you need to review any please let me know and i'll edit my code.
I'm trying not to use the didSelectRow method because that changes the row from section 0 to section 1. I would like to use a button to move all at once. Do I even have to mess with the Entities or should it move and change it by itself? I'm pretty lost on this...
The direct answer to your question is that you need to create an instance of SCList, set its attribute values to be the same as the attributes of the SList object, and then delete the SList object. I understand from your previous questions that you are using a fetchedResultsController for the first tableView, with two sections; the objects to move are in the section 1:
#IBAction func Move(sender: AnyObject) {
let sectionInfo = self.fetchedResultsController.sections![1]
let objectsToMove = sectionInfo.objects as! [SList]
for item in objectsToMove {
let newItem = NSEntityDescription.insertNewObjectForEntityForName("SCList", inManagedObjectContext:self.managedObjectContext!) as! SCList
newItem.scitem = item.slitem
newItem.scqty = item.slqty
newItem.scdesc = item.scdesc
self.managedObjectContext.deleteObject(item)
}
self.performSegueWithIdentifier("moveToSC", sender: self)
}
The longer answer to your question is that copying data from one entity to another like this is not ideal - it suggests you might need to think through your data model some more. You could, for example, have an additional attribute which reflects which tableView the item should appear in. You would specify predicates for the fetched results controllers to ensure the table views display only the appropriate items. "Moving" an item would then be a simple matter of changing the value of the new attribute.

EXC_BAD_ACCESS while passing array to new ViewController

I'm loading some XML from a webservice (car data), create some car objects and would like to display them in a TableViewController.
When the user has selected start and destination location, I'm making an async call to the webservice, show an activity indicator and as soon as the data is loaded, I go to a new view. So I have something like this:
class NewReservationViewController : UIViewController {
#IBAction func searchCarsClicked(sender: UIBarButtonItem) {
//show load cars activity indicator
loadingCarsActivityIndicator.startAnimating()
//load available cars from webservice asyncronously
DataManager.getAvailableCars([...parameter list...], carsLoadedCallback: carsLoaded)
}
func carsLoaded(loadedCars: [Car]) {
//dismiss the waiting widget
//trigger the segue and advance to the next screen
loadingCarsActivityIndicator.stopAnimating()
print("stopped cars loading activity indicator")
print("cars loaded callback called")
print("loaded \(loadedCars.count) distinct cars")
self.cars = loadedCars
performSegueWithIdentifier("showAvailableCars", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showAvailableCars" {
let carTableViewController = segue.destinationViewController as! CarTableViewController
carTableViewController.cars = self.cars
presentViewController(carTableViewController, animated: false, completion: nil)
}
}
}
class DataManager {
class func getAvailableCars([...parameter list...], carsLoadedCallback: ([Car]) -> Void){
Webservice.getAvailability([...parameter list...], completionHandler: { (data, response, error) in
//parse xml
//in the end I get an Array of Car objects
var cars: [Car] = ...
carsLoadedCallback(cars)
})}
}
}
When I populate the TableView with some DummyData I create within the CarTableViewController class, it works fine. However when I try to pass the car arrays to my TableViewController I get a EXC_BAD_ACCESS code = 2 exception. As far as I know this is some kind of Memory exception that is usually caused by a corrupt pointer. So I guess that the car array I created in my static DataManager class within the static method I called gets destroyed. However I'm not sure about that because automatic reference counting should avoid that.
The table view even displays the data but then immediately crashes with the EXC_BAD_ACCESS exception. I tried to set a general breakpoint in the XCode's breakpoints tab but however I don't get a reasonable error message on why the app crashes.
Do you have any ideas on why this happens. How can I get a better error message?
Thanks for your help in advance.
First of all check this function:
func carsLoaded(loadedCars: [Car])
It is returned as callback - what thread is it running? main or backround?
You should call on main thread
dispatch_async(dispatch_get_main_queue(), {
performSegueWithIdentifier("showAvailableCars", sender: self)
})
If doesn't help - provide the line, where it breaks in debugger, so I can help and see more.
UPD: Didn't notice, why do you do manual present?
presentViewController(carTableViewController, animated: false, completion: nil)
Your segue automatically shows view controller, you can change it's style (modal, push) in storyboard.
There are two potential issues that I can see. The first is that you are triggering your segue from a method that might not be on the main thread, you need to ensure that this is done on the main thread. The other issue is that in your prepare for segue you are unwrapping the new ViewController without checking it, so try this instead:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showAvailableCars" {
if let carTableViewController = segue.destinationViewController as? CarTableViewController {
carTableViewController.cars = self.cars
presentViewController(carTableViewController, animated: false, completion: nil)
}
}
}
As you said the table view displays the data and then immediately crashes I would assume that the problem is in your CarTableViewController. One thing you could check is when your cells are rendered, if you are trying to access some information that is null from the Car objects

SWIFT / iOS: Data takes a few seconds to load when screen appears

I've created a user profile screen, and when loaded, the data takes a few seconds to appear. I'm looking for a more elegant solution.
Here's the view controller in the storyboard:
As you can see, there is placeholder text. The issue is that this text appears very briefly when this screen is loaded (before the data is retrieved from the database).
Here is what the screen looks like when the data is fully loaded:
I've seen that an Activity Indicator on the previous screen may be a good solution. Can anyone verify this and point me to a good SWIFT tutorial or Stack Overflow solution?
UPDATE:
I'm loading the data on the previous View Controller as suggested. The issue that I'm running into is that the constants which I store my data are not accessible in prepareForSegue -
override func performSegueWithIdentifier(identifier: String, sender: AnyObject?) {
let query = PFQuery(className:"UserProfileData")
query.whereKey("username", equalTo: (PFUser.currentUser()?.username)!)
query.findObjectsInBackgroundWithBlock { (objects: [PFObject]?, error: NSError?) -> Void in
if error == nil {
if let objects = objects! as? [PFObject] {
for object in objects {
let yourselfObject = object["yourself"] as! String?
let brideGroomObject = object["brideGroom"] as! String?
}
}
} else {
print(error)
}
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "RSVPToUserProfile" {
if let destination = segue.destinationViewController as? UserProfileViewController {
destination.yourselfPassed = yourselfObject
destination.brideGroomPassed = brideGroomObject
}
}
}
This one is actually very simple... Just remove the placeholder text from the storyboard. That way it won't "appear very briefly."
If you don't like the idea of a blank screen, then move the loading code to the place where the view is being presented. Don't present the view until after the loading code is complete. You will have to pass the data to the view controller at presentation time if you do this.
-- EDIT --
Do not override performSegueWithIdentifier:sender:! Doing so will not accomplish your goal.
I say again. Move the loading code to the place where you are calling perform segue, and delay the call until after your data is loaded. (I.E. move the perform segue call into the findObjectsInBackgroundWithBlock block after you get the items.

Resources