iPhone app crashes when user exits UITableView with MBProgressHUD running in separate thread - iphone

In my app, I have a UITableViewController which loads calculated data from my Core Data store. This can take quite some time and hangs the main thread, so to give the user visual feedback, I have installed the MBProgressHUD widget which will show a progress indicator (just the spinning wheel) while the calculation method runs in a separate thread.
This is fine, except that the user can still exit the UITableViewController if they think it's taking too long. Of course, this is a good thing, except that when the separate thread concludes its operation, it still tries to call its delegate (the UITableViewController) to hide the MBProgressHUD. This causes a crash because since the user already exited the UITableViewController, it has dealloc'd and release'd itself.
MBProgressHUD has the following code to try to stop this:
if(delegate != nil && [delegate conformsToProtocol:#protocol(MBProgressHUDDelegate)]) {
if([delegate respondsToSelector:#selector(hudWasHidden)]) {
[delegate performSelector:#selector(hudWasHidden)];
}
}
However, my app somehow seems to still be running this inner code ([delegate performSelector:#selector(hudWasHidden)]) even though the UITableViewController is totally gone--causing the app to crash.
Any suggestions? I am not running with NSZombiesEnabled.

From your UITableViewController viewWillDisappear, viewDidDisappear or dealloc method, set the MBProgressHUD.delegate = nil;

Once the user has exited the table controller, the delegate property of the hud points to a deallocated object (= a memory zone that could contain anything). That causes the crash when the computing thread ends and tries to send any message to the delegate.
In your table view controller dealloc, you must set the hud delegate to nil.

Related

Xcode4, iOS: Certain parts of instance method being ignored, no errors, just passed by

new iOS guy here. I have a problem that Googling and searching on here has not shed any light on. I'm assuming this is basic. I have a simple app (app delegate and 1 view controller), and as part of it I'm using local notification. So, in the app delegate I use the 'didReceiveLocalNotification' to watch for the notifications. Depending on which one comes in, I then call one of several instance methods in my main view controller.
ie in the AppDelegate.m
- (void)application:(UIApplication *)application didReceiveLocalNotification: (UILocalNotification *)notification {
MyViewController* controller = [[MyViewController alloc] autorelease];
if ([[notification.userInfo objectForKey:#"id"] isEqualToString:#"myKey"]) {
[controller checkActive];
}
}
Through logging and watching some breakpoints, this is all working. If the app is in the background, the notification comes in, app opens, and the correct instance method is called.
What I cannot figure out at all is why some parts of the instance method are simply being passed by, with no effect. For a simple example, if we have this:
-(void)checkActive {
ViewThing.alpha = 1.0;
NSLog(#"checkActive ran");
}
The log statement will show up fine, but the ViewThing will not change. Elsewhere in the main view controller I'm calling the same checkActive method with no problems and it changes the ViewThing. (via another interface button IBAction method in that case).
There are no errors, no warnings, the console shows nothing, putting a breakpoint on ViewThing shows that it hits the line. I'm stumped, cannot see what is different from trying to calling the method from the delegate vs. on an IBAction.
Thanks for any tips!
If the alpha is not correctly changing there a few possible issues with 1 and 2 being the most likely.
ViewThing is nil. Reasons could be is the view unloaded and you set it to nil or checkActive was called before the outlets were set.
ViewThing.alpha is being set on a thread that is not the main thread. Attempting to change UI elements on a separate thread will caused undefined behavior such as never updating the change or taking an extended amount of time to update the change. You can check if it is the main thread using [NSThread isMainThread].
ViewThing is pointing a different view.
1 & 2 can easily be checked by logging view
NSLog(#"checkActive ran %#", ViewThing);

asihttprequest crashes my app

I have a navigation based app. Press a button on main view, then I push a new view to the navigation controller. All pretty basic stuff.
When the new view is loaded, I do an ASIHTTPRequest to fetch some json data, which is a list of image urls.
Then I do a for loop, create a bunch of ASIHTTPRequests, add them to a queue and then run the queue.
But if I click on the back button before the queue is finished, the app crashes, this app displays houses and lets say you pick the wrong house, click back very quickly, before any photo is displayed, bumm crash.
This thread http://groups.google.com/group/asihttprequest/browse_thread/thread/3d4815198aa889b9 explains my problem real well, except I do cancel all requests on view did unload, set delegate to nil and release the queue.
Still I crash. I crash pretty much every time if I use 3G, but on wifi it is real hard to make it crash, but quite doable.
In almost 80% instances the debugger jumps to this line in ASIHTTPRequest.m
(void)requestReceivedResponseHeaders:(NSMutableDictionary *)newResponseHeaders {
if ([self error] || [self mainRequest]) { return; }
--> if (delegate && [delegate respondsToSelector:didReceiveResponseHeadersSelector]) {
Many many cases it jumps to :
(void)requestReceivedResponseHeaders:(NSMutableDictionary *)newResponseHeaders {
if ([self error] || [self mainRequest]) { return; }
---> if (delegate && [delegate respondsToSelector:didReceiveResponseHeadersSelector]) {
Ad in a handful of instances it goes to my main loop
int main(int argc, char *argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
--> int retVal = UIApplicationMain(argc, argv, nil, nil); with SIGBART error [pool release]; return retVal;
I am using MBP and MacPro, latest OS X, Xcode 4.0.2 and I test on all apple devices except original iPhones.
I really don't want to re-write my whole app, but is there anything else out there that compares with ASIHTTPRequest ?
Try cancelling and unsetting the delegate in -viewWillUnload rather than -viewDidUnload. I suspect the window of time in which it's actually unloading (between calling those two UIViewController methods) is the time period when you're crashable. The delegate has gone away, but you haven't told your ASIHTTPRequest object that yet.
The error is that the delegate is still set.
I have found 2 ways to fix this.
The way I consider ugly is that you make a universal delegate that does all network traffic and is instantiated when the app is first run. I actually used the app delegate and listen to nsnotification center messages. It works like a charm, the app never crashes, but I think it is not optimal.
The best way is to not set the delegate and not use "setDidFinishSelector", but instead use "setCompletionBlock:^". This will only work on devices running iOS 4.0 and up, which is more than 90-95% and growing. This is just an awesome way and will not crash the application.
You won't find anything better that ASIHTTPRequest, the problem will be how you are using it and vanishing delegates on navigation are a common problem to have to deal with.
It sounds like your problem relates to the viewcontroller that is handling the queue being destroyed due to user navigation. I find the best way of solving these issues is to have a central model class that handles all my communications and keep that class throughout the application lifecycle.
That way you don't get unexplained crashes when delegates have vanished unexpectedly.
Option 2
Another approach can be to disable user navigation until the network operation completes. Put a modal view over the entire screen that shows a uiactivityview so the user knows their actions are being blocked. Then you can fade the modal view off when the data has arrived. If you design the screen nicely with a gradient so the background just dims a bit, this can look OK. But it's not really the best approach - you should fix the delegate AWOL instead.
We probably need to see more of the code relating to the queue creation, destruction etc to find the exact issue.
Your application delegate can own an array of request queues. The array lives independent of the state of the navigation controller stack and associated views. Instead of tying requests to a view controller in the nav stack, and having to do UI tricks to block popping back to a parent view, you could add requests to an app delegate queue instance, or stop all requests and empty the queue, etc.

Managing calls to objects that get deallocated when the view is backed out of

I have a view controller managed in a UINavigationController. My view controller puts up a "loading" screen, and uses ASIHTTP to asynchronously load table data. Then it renders its table, and in each cell there's an image that it uses ASIHTTP to asynchronously load. When it lands each image, it uses [self.tableView reloadRowsAtIndexPaths] to reload that row, inside which the image is fed to the UIImageView in each row.
Works great. But if I back out of the view before it's done loading, it crashes.
Where it crashes is in my -tableView:numberOfRowsInSection method, and NSZombies tells me it dies because it's asking for the -count of an NSArray called self.offers that has been deallocated.
That method looks like this:
-(NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section
{
return [self.offers count];
}
Wrapping that return in if (self.offers) made no difference.
My -dealloc releases and sets-to-nil every one of these properties, including both self.offers and self.tableView itself. I even tried setting up a BOOL disappearing property, hitting it with YES in -viewWillDisappear, and hanging conditional behavior off that, but it doesn't work because viewWillDisappear doesn't seem to get called! Far as I can tell we're not getting ANY method called when the navigation bar pops us off.
What do I do about this?
EDIT:
Thanks to #cduhn (who's bucking for a check!), I did a bunch more looking at this. The problem has been, my -dealloc just isn't getting called when I pop this viewcontroller (nor my -viewWillDisappear nor -viewDidUnload or anything else I could use to unhook the delegation structure that's at the root of this problem).
Then I realized: THIS viewController isn't the one on the NavController stack! What's at the top of the stack right here is a shell view, just a segmented controller and a big empty UIView. I toggle the contents of that UIView between two other UIViewController subclasses depending on the state of my segmented controller. So when my view with the table on it's PARENT view gets popped from the nav stack, this CHILD I'm working on doesn't seem to get any notice about it. Which is odd, because I'm definitely releaseing it.
I can call its -dealloc from my shell controller. I could call its -viewWillDisappear too, for that matter. Is that how I should be handling this? Probably I should put something into my shell controller's viewWillDisappear like:
[[self.mainView subviews] makeObjectsPerformSelector:#selector(viewWillDisappear)];
...so that message propagates down to my child views.
Am I on the right track here, you think??
(Oh man... and that also explains why actions from inside this child table view can't get to self.navigationController! I've been puzzled about that for weeks!)
Bugs like this, where a method gets called on an object after it's been deallocated, often happen when a method gets called on a delegate after that delegate has been deallocated. The recommended practice to avoid bugs like these is to set any delegate (or delegate-like) properties of an object to nil before you release that object in the delegate's dealloc method. I know that's a confusing sentence, so I'll explain it in the context of your bug.
You have an asynchronous image download that finishes after you've backed out of your table view controller. When this happens, you're calling reloadRowsAtIndexPaths:withRowAnimation:, which results in a call to tableView:numberOfRowsInSection: on the table view's dataSource. This call is failing because that dataSource no longer exists.
The problem is that the table view object still has your controller set as its dataSource and delegate properties, even after your controller has been deallocated. The solution is to simply set these properties to nil in your controller's dealloc, like this:
- (void)dealloc {
self.tableView.dataSource = nil;
self.tableView.delegate = nil;
self.tableView = nil; // Releases as a side-effect due to #property (retain)
[super dealloc];
}
Now when your table view tries to call tableView:numberOfRowsInSection: on its dataSource, it will send the message to the nil object, which swallows all messages silently in Objective C.
You should also do the same thing with your ASIHTTPRequest's delegate, by the way.
Any time you have an asynchronous operation that calls delegate methods upon completion, it's particularly important that you set that delegate to nil. When using UITableViewController under normal circumstances you can typically get away without setting the dataSource and delegate to nil, but in this case it was necessary because you're calling methods on the tableView after its controller has gone away.
As far as I can tell, the user cannot actually "back out" of a view while the UITableView is loading it's data. The methods are not run on a thread and block the main one, also blocking UI interaction. I cannot replicate your results. Even, scrolling the table view quickly and then pressing the back button.
I suggest that the stack popping is not the problem here.

UIViewController is popped from view stack and NSURLConnection crashes the application

I am pushing a UIViewController onto a UINavigationController. This view controller immediately starts a download of an xml feed and then parses it. However, if you hit the back button before it is done downloading, and crashes with EXC_BAD_ACCESS. The line that is crashing it is in parserDidEndDocument and is this line:
if (self.delegate && [self.delegate conformsToProtocol:#protocol(ModelDelegate)]) [self.delegate modelDidFinishParsing:self];
I assume it is crashing because it is trying to access self.delegate which is not assigned anymore. How do I get around this?
Also, I would release the model object in the modelDidFinishParsing method. How would I release this model if it never reaches this method.
I set up objects to handle my downloads (and other asynchronous or long running tasks) in the AppDelegate, then trigger them as required from various controllers. That way they are owned and have persistence through the life of the application.
The best way to do this is to pass them to the viewControllers that will need them (rather than the viewController "expecting" the appDelegate to have such and such an object ready and waiting) - dependency injection.
These objects update my model in some way when they finish and if I need to, I use NSNotifications to announce they are done. This isolates me from the mess I used to get into trying to cancel or swap delegates in viewWillDisappear etc to avoid the kind of issues you are running into.
The reason your app is crashing is probably because NSURLConnection retains its delegate (so it can call back to it reliably) but objects that this delegate has weak references to have been deallocated.
Ie, in your case what self.delegate points to has probably been deallocated when the view controller is popped but the delegate property has not been cleared (set to nil).
The solution to your problem is to clear (nil) self.delegate at the appropriate time when the UIViewController subclass is being popped off the navigation stack.
Note: retaining delegates is not usual behaviour for Cocoa classes. In situations where it happens contrary to standard practice it is documented (see the NSURLConnection docs).

How to avoid data lose when UIImagePickerController unloads my controller?

I am using UIImagePickerController to take a photo from the camera. However, the I find that randomly my calling controller (the one that is shown before the UIImagePickercontroller is shown) gets unloaded. I logged the viewDidUnload, and indeed it does get called. When the camera is done and dismissed, my controller's viewDidLoad will be called, unfortunately all the state is now gone. Things like text entered or things selected will be gone.
Obviously this is something to do with running out of memory. But is this behavior normal? Should I be handling it? This is NOT typically how modalViewController works. Usually you show it and dismiss it, everything should be intact.
What is a good way to avoid data lost in this case? Should I have to write a bunch of code to save the full state?
what's happening is that your view controller's didReceiveMemoryWarning is being called, and then since your view isn't visible, it's being unloaded.
the solution is that any data that wants to be persistent should be stored in your view controller rather than in your view.
you can prevent this from happening by providing an implementation of didReceiveMemoryWarning in your UIViewController class that doesn't call [super didReceiveMemoryWarning] which is where the unloading of the view happens, but it's still a good thing to try to understand what's going on.
It's really not advisable to override -didReceiveMemoryWarning so that UIViewController can't deallocate the view because then you may run into a "real" problem of low memory and your app will be forced to quit by the system.
What you really should do is store the text and other bits of entered data from the view in your -didReceiveMemoryWarning method (making sure to call super). Then in -viewDidLoad you can put the text and other bits back into the UI.
This will lighten the memory usage and reduce the likelihood of your app being forced to quit because there's no more memory left. All the while, the user won't know it ever disappeared!
My solution to the same issue described above was to declare an instance variable in my view controller which stores the contents of the UITextView should the view get unloaded. Thus in my viewWillDissapear method I save the contents of UITextView.text into my instance variable and in my viewWillAppear method I restore the contents.
- (void)viewWillAppear:(BOOL)animated
{
if (messageTextString != nil)
{
textEdit.text = messageTextString;
}
[super viewWillAppear:animated];
}
(void)viewWillDisappear:(BOOL)animated
{
self.messageTextString = textEdit.text;
[super viewWillDisappear:animated];
}