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

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.

Related

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.

Passing VC data not working properly

When a user taps on an image in my application, I would like for that to send them to another view controller to give them more post details but I can't quite figure out how to transfer that image from one view controller to the next!
The code below, should they tap the image, should send them to the detailed view controller - this is done so in the else if statement.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if username != PFUser.currentUser()?.username {
let profileVC: UserProfileViewController = segue.destinationViewController as! UserProfileViewController
profileVC.usernameString = username
} else if (segue.identifier == "toPostDetail") {
var svc = segue.destinationViewController as! PostDetailsViewController
svc.toPass = // Call/pass the image
}
}
To retrieve all the posts in my database I use the following code, this runs perfectly fine but I need to get the postImage trasnfered to the next view controller.
postsArray[indexPath.row]["image"].getDataInBackgroundWithBlock { (data, error) -> Void in
if let downloadedImage = UIImage(data: data!) {
postCellObj.postImage.image = downloadedImage
postCellObj.postImage.layer.masksToBounds = true
postCellObj.postImage.layer.cornerRadius = 5.0
}
}
So what would I put in the svc.toPass in order for the image to be sent to the other view controller? Since this is an array of posts, would I have to do something with didSelectRowAtIndexPath or am I doing the right thing? I am also using Parse.com to get information to and from my databases.
You should pass the post rather than the image, so it would be something like
postsArray[indexPath.row]
How you get the indexPath is up to you. If the segue is triggered by a cell selection then you could store the indexPath in didSelectRowAtIndexPath, or you could pass the indexPath as the sender when you trigger the segue, or you could use indexPathForSelectedRow if it's retained during the segue process.

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

Data loading asynchronously and not displaying in UITabBarController TableView?

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.

How to synchronize a parse-read image with screen display in iOS

In TableViewController I display a table of records read from a parse database. Each cell displays a thumbnail, a title and a note. When a row is selected, the row index is saved and a segue is initiated to a detailView Controller which displays the image saved in a global UIImage variable (noteImage). In prepareForSegue I invoke loadImageFromParse in background which reads the image associated with the thumbnail and saves it into noteImage.
My problem is that there is a slight delay for the image to be read from parse so the detailViewController displays a blank. When I back up from the detailViewController to the TableViewController and reselect the row the picture is displayed.
I need a way to hold the segue invocation till the image has been read from Parse.
Any help would be appreciated.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedRow = indexPath.row
self.performSegueWithIdentifier("NoteSegue", sender: self)
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
// MARK: - Navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "NoteSegue"{
let vc = segue.destinationViewController as NoteViewController
vc.selectedRow = selectedRow
self.loadImageFromParse(objectIds[selectedRow])
}
}
// Load one image from parse
func loadImageFromParse (objectId: String) {
var query = PFQuery(className:"Record")
query.whereKey("objectId", equalTo:objectId)
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
println("Successfully retrieved \(objects.count) records.")
for object in objects {
let userImageFile = object["image"] as PFFile!
userImageFile.getDataInBackgroundWithBlock {
(imageData: NSData!, error: NSError!) -> Void in
if error == nil {
noteImage = UIImage(data:imageData)!
println("Image successfully retrieved")
}
}
}
} else {
NSLog("Error: %# %#", error, error.userInfo!)
}
}
}
You said:
I need a way to hold the segue invocation till the image has been read from Parse.
That is, as I think you now guessed, not really the best way to do it. It's much better to initiate the transition to the next scene, and if the image is not available, present a UIActivityIndicatorView (a spinner), retrieve the image, and when it's done, remove the spinner and show the image. But delaying for a fixed amount of time is not going to work well: Sometimes you'll make the user wait longer than needed; sometimes you won't have waited long enough. (And, of course, doing the request synchronously is invariably not the right approach.)
The implementation of this is probably simplified if you put this image retrieval code inside the destination view controller. To have one view controller initiate an asynchronous process and have another be notified of the completion is awkward (though can be done through judicious use of notifications). But in this example (where you don't know which image to retrieve until you select a row), I see little benefit.
Having said all of that, the code is initiating two asynchronous requests, one for Record and one for the image itself. I wonder if you could capture this Record information when you populate this initiate table view. That way you only have to retrieve the image. Now perhaps Parse does some caching and the retrieval of the Record is incredibly fast anyway (so benchmark it before you refactor the code), but if not, you might want to capture the PFFile objects when you populate the original table view, saving you from having to do two queries.
I resolved the problem by introducing a 1 second delay after I start the background method loadImageFromParse and before I initiate a segue in performSegueWithDelay. This gives a slight pause after I select the row and before the image is visible in the target segue but it ensures that the image is available. Obviously there is a risk that this will not always work depending on size of the file I am getting from Parse.com and also the network load. I am considering putting an alert to the user if the image has not loaded.
Here is the code:
func performSegueWithDelay(timer: NSTimer) {
// action function for timer in didSelectRowAtIndexPath
self.performSegueWithIdentifier("NoteSegue", sender: self)
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedRow = indexPath.row
self.loadImageFromParse(objectIds[selectedRow])
let myTimer : NSTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self,
selector: Selector("performSegueWithDelay:"), userInfo: nil, repeats: false)
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
// MARK: - Navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "NoteSegue"{
let vc = segue.destinationViewController as NoteViewController
}
}
// Load one image from parse
func loadImageFromParse (objectId: String) {
var query = PFQuery(className:"Record")
query.whereKey("objectId", equalTo:objectId)
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
println("Successfully retrieved \(objects.count) records.")
for object in objects {
let userImageFile = object["image"] as PFFile!
userImageFile.getDataInBackgroundWithBlock {
(imageData: NSData!, error: NSError!) -> Void in
if error == nil {
noteImage = UIImage(data:imageData)!
println("Image successfully retrieved")
}
}
}
} else {
NSLog("Error: %# %#", error, error.userInfo!)
}
}
}

Resources