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!)
}
}
}
Related
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
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.
I've got Pull to Refresh working great, except when the table reloads there is a split second delay before the data in the table reloads.
Do I just have some small thing out of place? Any ideas?
viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.getCloudKit()
}
handleRefresh for Pull to Refresh:
func handleRefresh(refreshControl: UIRefreshControl) {
self.objects.removeAll()
self.getCloudKit()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
refreshControl.endRefreshing()
})
}
Need the data in two places, so created a function for it getCloudKit:
func getCloudKit() {
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil { // There is no error
for play in results! {
let newPlay = Play()
newPlay.color = play["Color"] as! String
self.objects.append(newPlay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
} else {
print(error)
}
}
}
tableView:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let object = objects[indexPath.row]
if let label = cell.textLabel{
label.text = object.matchup
}
return cell
}
This is how you should do this:
In your handleRefresh function, add a bool to track the refresh operation in process - say isLoading.
In your getCloudKit function just before reloading the table view call endRefreshing function if isLoading was true.
Reset isLoading to false.
Importantly - Do not remove your model data before refresh operation is even instantiated. What if there is error in fetching the data? Delete it only after you get response back in getCloudKit function.
Also, as a side note, if I would you, I would implement a timestamp based approach where I would pass my last service data timestamp (time at which last update was taken from server) to server and server side would return me complete data only there were changes post that timestamp else I would expect them to tell me no change. In such a case I would simple call endRefreshing function and would not reload data on table. Trust me - this saves a lot and gives a good end user experience as most of time there is no change in data!
I want to make such thing:
On one ViewControleer I'm making a query to Parse.com, where I'm sending objects fields to Label.Text. By clicking one button objects randomly changes, by clicking another one- next ViewController is opening. Just imagine Tinder - on the first VC I swiping girls, on the new one chat is opening, with the girl's name in the head of the NavigatorItem
So I want to send Object Field "Name" that I'm using in that view to another without other query.
I don't know, whether I can do it via segue, or protocol. Can U somehow help me with implementation?
here is the code of my random function
func retriveJobData() {
var query: PFQuery = PFQuery(className: "Jobs")
query.getObjectInBackgroundWithId("AUeuvj0zk2") {
(newJobObject: PFObject?, error: NSError?) -> Void in
if error == nil && newJobObject != nil {
println(newJobObject)
if let newJobObject = newJobObject {
self.PrcieTextField.text = newJobObject["jobPrice"] as? String
self.DateTextField.text = newJobObject["jobDate"] as? String
self.DescriptionTextField.text = newJobObject["jobDescription"] as? String
}
} else {
println(error)
}
}
}
I want to send newJobObject["jobName"] to NavigatorItemName of another ViewController
you can override prepareForSegue for this purpose:
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if (segue.identifier == "yourSegueIdentifier") {
// pass data to next view
}
}
Assuming you have some method that triggers a push to the new viewController and that you're using the storyboard, call performSegue using the identifier you set up in the storyboard
#IBAction func buttonPressed(sender: UIButton!) {
performSegueWithIdentifier("identifier", sender: nil)
}
Then override prepareForSegue and pass in the string
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "identifier" {
let controller = segue.destinationViewController as! ViewController
controller.jobName = someWayThatYouRetrieveNewJobObjectName
}
Then in ViewController ensure you have a property for jobName
var jobName:String! //declare this as an optional instead if needed
And set the navigation title
navigationItem.title = jobName
I do hope all is well. I am new to Swift. I am building a shell for 2 social app ideas of mine. I have successfully completed login, signup, logout and minimal querying using Parse. Up until now, I have been progressing, however I have reached a mental block.
I created a User Profile ViewController that queries the current user's information from the database and initially displays the results on the in the controller; First Name, Last Name, and etc.
I also created an Edit Profile ViewController that enables the user to update their profile information and logout. So far I have been able to submit the changes to the database, however I am having a hard time having the UILabels and Text Fields update to display the new values.
Some additional insight: Becuase my main view that users are redirected to after successful signin/registration is embedded in a navigation controller which means the navigation bar is inferred.
Please help me!
This is the code for my UserProfileViewController. Please let me knwo if I need to clarify.
override func viewDidLoad() {
super.viewDidLoad()
//---------------------------------------
//User Profile View - this can go between viewDidLoad and viewDidAppear functions.
//---------------------------------------
let userImageFile = PFUser.currentUser()["userProfileImage"] as PFFile
userImageFile.getDataInBackgroundWithBlock {
(imageData: NSData!, error: NSError!) -> Void in
if error == nil {
var image = UIImage(data:imageData)
self.currentUserProfilePicture.image = image
}
}
var query = PFUser.query()
query.whereKey("objectId", equalTo: PFUser.currentUser().objectId)
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
let user = PFUser.currentUser()
var firstname = PFUser.currentUser()["firstName"]! as String
var lastname = PFUser.currentUser()["lastName"]! as String
var aboutMe = PFUser.currentUser()["aboutMe"]! as String
self.firstName.text = firstName
self.lastName.text = lastname
self.aboutMe.text = aboutMe
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewDidAppear(animated: Bool) {
}
Hi and welcome to swift.
Are you using the prepareForSegue, for sending data between ViewControllers?
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "yourIdentifier" //you set the identifier in the storyboard
{
var vc = segue.destinationViewController as! YourDestinationViewController //the class of the destination view controller
vc.someValue = value
vc.otherValue = otherValue
vc.dataArray = array //here you set the values in the destination view controller
}
}