Backend Completion Handler crashes app if completed after PushViewController - ios

What I have
I don't understand the nature of the problem.
I have 2 View Controllers :
1) FeedViewController ,which shows events in tableview
2) EventViewController, pushed when you press on event.
When Feed Loads, it start asynchronous loadings of all images of all events.
It's done for each event by following function:
EventsManager().loadProfilePictureById(event.profilePictureID as String, currentProgress: event.profilePictureProgress, completionHandler: {
(progress:Double, image:UIImage!, error:NSError!) -> Void in
event.profilePictureProgress = progress
if image != nil {
event.profilePicture = image
}
if (error == nil){
if (self.tableView.headerViewForSection(index) != nil){
var header:eventHeaderView = self.tableView.headerViewForSection(index) as! eventHeaderView
header.updateProfilePicture(
self.eventsManager.events[index].profilePictureID as String,
progress: self.eventsManager.events[index].profilePictureProgress,
image: self.eventsManager.events[index].profilePicture)
}
}else{
println("Error:" + error.description)
}
})
Here is how I push EventViewController:
func PushEventViewController(sender:UITapGestureRecognizer)->Void{
let ViewSender = sender.view!
let selectedRow = ViewSender.tag
//let Cell:HomeEventTableViewCell = TimelineEventTable.cellForRowAtIndexPath(SelectedIndexPath) as HomeEventTableViewCell
dispatch_async(dispatch_get_main_queue(), {
() -> Void in
let VC:EventViewController = self.storyboard?.instantiateViewControllerWithIdentifier("EventViewController") as! EventViewController
VC.event = self.eventsManager.events[selectedRow]
self.navigationController?.pushViewController(VC, animated: true)
})
}
Problem
The problem is that if I press on event and push EventViewController before image is downloaded (completion handlers are still getting called) it crashes app.
Assumptions
I struggled with this for days and couldn't find a solution,
but my assumptions are that completion handler called after
Crash happens when it tries to execute following line after EventViewController was pushed:
event.profilePictureProgress = progress
if image != nil {
event.profilePicture = image
}
I assume when new view controller is pushed , event object which is used in completion handler is being deallocated

Found out where the problem was, the issue was that variable event.profilePictureProgress was declared as dynamic var (I was going to take advantage of that to add observer to it after).

Related

Where to Put DispatchQueue Swift

I am a new developer. I'm using Swift 4.2 and Xcode 10.2.
I'm trying to show a spinner while a photo is being saved. I am simulating a slow connection on my iPhone in Developer Mode to test it. Using the code below the spinner does not show up. The view goes right to the end where the button shows and says the upload is complete (when it isn't). I tried putting it all into a DispatchQueue.global(qos: .userinitiated).async and then showing the button back on the main queue. I also tried putting the showSpinner on a DispatchQueue.main and then savePhoto on a .global(qos: .utility). But I cleary don't understand the GCD processes.
Here's my code:
func savePhoto(image:UIImage) {
// Add a spinner (from the Extensions)
self.showSpinner(onView: self.view)
PhotoService.savePhoto(image: image) { (pct) in
// Can show the loading bar here.
}
// Stop the spinner
self.removeSpinner()
// Show the button.
self.goToPhotosButtonLabel.alpha = 1
self.doneLabel.alpha = 1
}
What types of DispatchQueues should I use and where should I put them?
Here is the savePhoto code:
static func savePhoto(image:UIImage, progressUpdate: #escaping (Double) -> Void) {
// Get data representation of the image
let photoData = image.jpegData(compressionQuality:0.1)
guard photoData != nil else {
print("Couldn't turn the image into data")
return
}
// Get a storage reference
let userid = LocalStorageService.loadCurrentUser()?.userId
let filename = UUID().uuidString
let ref = Storage.storage().reference().child("images/\(String(describing: userid))/\(filename).jpg")
// Upload the photo
let uploadTask = ref.putData(photoData!, metadata: nil) { (metadata, error) in
if error != nil {
// An error during upload occurred
print("There was an error during upload")
}
else {
// Upload was successful, now create a database entry
self.createPhotoDatabaseEntry(ref: ref, filename: filename)
}
}
uploadTask.observe(.progress) { (snapshot) in
let percentage:Double = Double(snapshot.progress!.completedUnitCount /
snapshot.progress!.totalUnitCount) * 100.00
progressUpdate(percentage)
}
}
Since the code that saves the photo is asynchronous , so your current code removes the spinner directly after it's added before the upload is complete
func savePhoto(image:UIImage) {
// Add a spinner (from the Extensions)
self.showSpinner(onView: self.view)
PhotoService.savePhoto(image: image) { (pct) in
// remove spinner when progress is 100.0 = upload complete .
if pct == 100.0 {
// Stop the spinner
self.removeSpinner()
// Show the button.
self.goToPhotosButtonLabel.alpha = 1
self.doneLabel.alpha = 1
}
}
}
Here you don't have to use GCD as firebase upload runs in another background thread so it won't block the main thread

Best practice for navigating to new view controller while still waiting for data

Right now I have a tableviewcontroller. When I click on one of the cells, I segue to a new viewcontroller that displays details about the item I clicked on.
Sometimes not all of the info I need has been loaded yet (especially on slow connections), so I'm trying to load it in the background even after the segue. I know I could just freeze the main thread and wait for my data to load, but I'd rather have a fluent interface.
For example, if a user's profile picture isn't loaded yet, then a placeholder image is shown and replaced when the real one loads in. I can't seem to get this behavior to work though. Here's what I'm doing right now:
func onItemTap(send:AnyObject){
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc: OtherViewController = storyboard.instantiateViewControllerWithIdentifier("OtherViewController") as! OtherViewController
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), {
while(self.parseUser == nil){//wait for it to load}
let profImage = (self.parseUser?.valueForKey("profilePictureLarge") as! PFFile)
profImage.getDataInBackgroundWithBlock({
(imageData: NSData?, error: NSError?) -> Void in
if (error == nil) && vc.profileImageView != nil{
vc.profileImageView.image = UIImage(data: imageData!)
vc.userNameLabel.text = self.parseUser?.valueForKey("displayName") as? String
vc.parseUser = self.parseUser
vc.pfObject = self.parseObject
}
})
})
//these are sent no matter what, but are replaced by background thread if needed
vc.parseUser = self.parseUser
vc.pfObject = self.parseObject
holder?.navigationController?.pushViewController(vc, animated: true)
}
I'm not sure what I should really be doing. This seems to work in some cases, but just not at all most of the time.
How can I asynchronously send data that is being loaded somewhere on a background thread and have it reliably show up when I need it?
Move your dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0) block to viewWillAppear() of view controller being pushed.
Actually you don't need to use dispatch_async since you're already calling asynchronous PFQuery. So your viewcontroller's viewWillAppear() method will look like this:
override func viewWillAppear(animated: Bool){
self.userNameLabel.text = self.parseUser?.valueForKey("displayName") as? String
let profImage = (self.parseUser?.valueForKey("profilePictureLarge") as! PFFile)
profImage.getDataInBackgroundWithBlock({ (imageData: NSData?, error: NSError?) in
if (error == nil) && self.profileImageView != nil{
self.profileImageView.image = UIImage(data: imageData!)
}
})
}
Additionally, if you could, take a look into PFImageView documentation (it is inside ParseUI.framework).
It makes setting images from PFFile much easier.

Swift dispatch_async not updating UIView until iOS notification

I have a UIView that is updated from a server once a button is pushed.
The UIView update happens within a dispatch_async(). Through debugging I can see that the server response has been received and the new subview is created and applied to the UIView.
However the view remains unchanged UNTIL another iOS notification occurs (e.g. the device receives an email). As soon as the notification banner is displayed the UIView is refreshed and displays the image from the server.
What am I missing? How do I get the UIView to update as soon as the new subview is added?
Note: I have tried the following within and outside the dispatch_async() and tried calling them on the same queue after the initial change takes place without luck.
self.view.setNeedsDisplay()
self.view.bringSubviewToFront(view)
self.view.layoutIfNeeded() (thanks for the suggestion #buxik)
EDIT added code (small version of larger class, only relevant section added):
class networkedImage: UIViewController {
#IBOutlet var viewArea: UIView = UIView()
let originalWidth: CGFloat = 200
let originalHeight: CGFloat = 100
let imageURL: NSURL = NSURL(string: "www.example.com/image.jpg")!
func updateViewArea() {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
dispatch_async(dispatch_get_main_queue()) {
self.viewArea.subviews.forEach({ $0.removeFromSuperview() })
let newImageView = UIImageView(frame: CGRectMake(0, 0, self.originalWidth, self.originalHeight))
let data = NSData(contentsOfURL: self.imageURL)
if data != nil {
let image = UIImage(data: data!)
if image != nil {
newImageView.image = image
self.viewArea.addSubview(newImageView)
self.viewArea.bringSubviewToFront(newImageView)
self.viewArea.setNeedsDisplay()
}
NSNotificationCenter.defaultCenter().addObserverForName("NotificationIdentifier", object: nil, queue: nil, usingBlock: {
[unowned self] note in
print("I thought this might work, it didn't.")
})
}
}
}
}
}
To resolve the issue I added an Observer to viewDidLoad()
NSNotificationCenter.defaultCenter().addObserverForName("imageReadyNotification", object: nil, queue: NSOperationQueue.mainQueue(), usingBlock: {
[unowned self] note in
self.updateViewArea()
})
Then at the bottom of updateViewArea() I post the notification
if self.newImage == true {
NSNotificationCenter.defaultCenter().postNotificationName("imageReadyNotification", object: nil, userInfo: nil)
}
The if condition is to ensure the notification isn't fired continually. I have no idea why calling this function twice works, but it does.

Execute swift code in order

I need the "getUserInfo" to complete before I execute the next section of code (the push to storyboard). Currently the "getUserInfo" is still in process while the storyboard push executes. How can I make these execute in order? I'm need to keep these 2 functions separate, so putting the code in the completion handler of loginUser isn't a good solution. Many thanks to those who are smarter than me :)
func loginUser() {
PFUser.logInWithUsernameInBackground(txtEmailAddress.text, password:txtPassword.text) {
(user: PFUser?, error: NSError?) -> Void in
if user != nil {
// Successful login.
self.txtPassword.resignFirstResponder()
self.txtEmailAddress.resignFirstResponder()
getUserInfo()
// Push to Main.storyboard.
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let viewController: AnyObject = storyboard.instantiateInitialViewController()
self.presentViewController(viewController as! UIViewController, animated: true, completion: nil)
} else {
// The login failed. Display alert.
self.displayAlert("Error", message: "Login incorrect")
}
}
}
func getUserInfo() {
let currentUser = PFUser.currentUser()
let userQuery = PFQuery(className: "_User")
userQuery.whereKey("username", equalTo: PFUser.currentUser()!.username!)
userQuery.findObjectsInBackgroundWithBlock({ (results:[AnyObject]?, error:NSError?) -> Void in
if error == nil {
for result in results! {
userType = result["userType"] as! String
if userType == "admin" {
user = "AdminSetting"
} else {
user = "StandardSetting"
}
}
}
})
}
What you want to do is make the asynchronous function (the one with the completion handler) synchronous, so that it returns immediately. However that's usually a bad idea, because if the execution stops in the main thread, the user can't do anything and your app is stuck until the code continues again. This might take a couple seconds depending on the connection, which isn't very good, you should really update your UI asynchronous on asynchronous tasks. There usually aren't good reasons to do something like this, if you have them though, you can tell me.
You could also execute your storyboard code within getUserInfo() as a block/closure passed in as a parameter. That way you can ensure it is executed when the async call in getUserInfo completes
What Shadowman suggested is the correct/most elegant solution.
Here is an example:
func getUserInfo(completion: (results:[AnyObject]?, error:NSError?) -> Void) {
let currentUser = PFUser.currentUser()
let userQuery = PFQuery(className: "_User")
userQuery.whereKey("username", equalTo: PFUser.currentUser()!.username!)
userQuery.findObjectsInBackgroundWithBlock(completion)
}
This way, you get back the actual results from your call after it finished, handy, eh ?
And here is how you call it:
self.getUserInfo { (results, error) -> Void in
// Here the results are already fetched, so proceed with your
// logic (show next controller or whatever...)
if error == nil {
for result in results! {
userType = result["userType"] as! String
if userType == "admin" {
user = "AdminSetting"
} else {
user = "StandardSetting"
}
}
}
// depending on whether this will still run in a background thread, you might have to dispatch this code to the main thread.
// you can check whether this code block is called on the main thread
// by checking if NSThread.isMainThread() returns true
// if not, you will need to use this dispatch block!
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// only call UI code in main thread!
// MOVE TO NEXT controller in here!
})
}

very weird Xcode project bug?

I am attempting to integrate some JSSAlertViews within one of my ViewControllers, but for some odd reason, when I run my project, the alert views do not show. So to make sure it wasn't any error with coding, I created an exact pseudo project to replicate the ViewController of my original project, down to it's UI elements on the storyboard. I copied the exact code from my original project onto the new ViewController, ran it, and everything worked. Im stuck onto figuring out, why won't it work on my original project??
here is the logic i used:
#IBAction func resetPass(sender: AnyObject) {
actview.hidden = false
actview.startAnimating()
PFUser.requestPasswordResetForEmailInBackground(emailReset.text) {
(success:Bool, error:NSError?) ->Void in
if(success){
let yesMessage = "Email was sent to you at \(self.emailReset.text)"
dispatch_async(dispatch_get_main_queue()) {
self.actview.stopAnimating()
JSSAlertView().success(self, title:"Great Success", text:yesMessage)
}
}
if(error != nil){
let errorMessage:String = error!.userInfo!["error"] as! String
dispatch_async(dispatch_get_main_queue()) {
self.actview.stopAnimating()
JSSAlertView().warning(self, title:"Woah There", text:errorMessage)
}
}
}
}
I set a breakpoint on a call of one of the JSSAlertView's , expanded the element in my console and got this :
Is this a memory management error and reason why they aren't visible? how do i fix this?
here is the Git if you want to check it out, its awesome: https://github.com/stakes/JSSAlertView
Anything with the UI needs to be done on the main thread, and you're calling the Parse function on the background thread (via requestPasswordResetForEmailInBackground).
So to get your alerts to appear on the main thread, you need to add a little GCD magic:
#IBAction func resetPass(sender: AnyObject) {
actview.hidden = false
actview.startAnimating()
PFUser.requestPasswordResetForEmailInBackground(emailReset.text) {
(success:Bool, error:NSError?) ->Void in
if(success){
self.actview.stopAnimating()
let yesMessage = "Email was sent to you at \(self.emailReset.text)"
dispatch_async(dispatch_get_main_queue()) {
self.successMessage(yesMessage)
}
};
if(error != nil){
self.actview.stopAnimating()
let errorMessage:String = error!.userInfo!["error"] as! String
dispatch_async(dispatch_get_main_queue()) {
self.failureMessage(errorMessage)
}
}
}
}
See my addition of the "dispatch_async(dispatch_get_main_queue())" lines?

Resources