I have a controller that makes HTTP GET requests using a custom class, which acts as the delegate for the NSURLConnection. Once the NSURLConnection fails or finishes, the custom class invokes methods on the controller and passes along an NSData object of received data.
I'm running into a problem where the controller in action is being dynamically created and pushed onto a navigation controller's stack. This controller makes the HTTP GET request in its viewDidLoad method. If the user quickly presses "back" on the navigation bar, this controller gets dealloc'ed. If this happens before the HTTP GET request finishes, the resulting NSURLConnection callback becomes a method call to a dealloc'ed object, which results in an EXC_BAD_ACCESS.
What's the best approach to cleaning up any pending NSURLConnections that have been kicked off by a controller which may actually be deallocated already?
I threw in some NSLog statements, and it seems that my custom class used as a NSURLConnection delegate doesn't actually receive a dealloc message. I made sure to set the controller's instance of this class to nil in viewDidUnload, and also call release on it, but it still seems to live longer than the controller.
If I understand it correctly you just need to do [whateverConnection cancel] in your viewDidUnload or dealloc method. This cancels the connection. It is almost the same if you have a custom downloader object for example for some large image that uses NSURLConnection. Make a cancel method for your class (that cancels the connection and releases it) and invoke it in your controller's dealloc method. You should also use a bool flag something like wasCanceled and do not invoke any method from your custom object's delegate if wasCanceled was set from your cancel method. (You only have a weak pointer to your delegate so it is probably released already when some other object invokes your cancel method). I assume that the delegate for your custom object is the view controller. I had several downloaders like this and it worked ok, without leaks even if I quickly canceled a download.
#interface CompaniesDownloader : NSObject /*<NSXMLParserDelegate>*/
{
id<CompaniesDownloaderDelegate> delegate; //a view controller is the delegate
NSMutableArray *companies;
BOOL isWorking;
BOOL wasCanceled;
#private
//url connection object
NSURLConnection *companiesConnection;
//this is where i put the binary data that gets transformed into xml
NSMutableData *webData;
//temporary string used when parsing xml
NSMutableString *tmpString;
//temporary company used when parsing xml
Company *tmpCompany;
}
In the implementation:
-(void) cancel
{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible: FALSE];
wasCanceled = TRUE;
[companiesConnection cancel];
[webData release];
webData = nil;
self.companiesConnection = nil; //OR [companiesConnection release]; companiesConnection=nil;
isWorking = FALSE;
}
You need to retain your view controller when you make a request and release when the get request finished
YourViewController.m
- (void)callGetRequest {
[self retain];
}
- (void)didFinishAllGetTask {
[self release];
}
If the user quickly presses "back" on the navigation bar, this controller gets dealloc'ed. If this happens before the HTTP GET request finishes, the resulting NSURLConnection callback becomes a method call to a dealloc'ed object, which results in an EXC_BAD_ACCESS.
When the user hits the back button, de-register the view controller class from being the delegate for that object. (Keep a reference to the object in the view controller class, so you can do something like someObject.delegate = nil;). You can do that in the view controller's dealloc method.
Related
I have two view controllers (BuildingsViewController and RoomsViewController) that both use a function within the App Delegate called upload. The upload function basically does an HTTP request, and if its successful or unsuccessful, triggers a uialertview. This is working fine.
The part I'm struggling with is from within the app delegate's connectionDidFinishLoading method. I need to be able to basically refresh the current view controller via perhaps viewWillAppear method of that view controller. Inside the viewWillAppear function of each view controller I have code which determines the buttons on the bottom toolbar.
I want the "upload" button in the toolbar of each view controller to automatically be removed when the uploading is done via the app delegate.
I've tried doing [viewController viewWillAppear:YES] from within the connectionDidFinishLoading method of the app delegate, but it never gets called.
I hope I'm clear enough. Any help is greatly appreciated.
Thanks.
To do the refresh of the view do not call viewWillAppear if the view is already displayed. What you want to do is the following:
When ConnectionDidFinishLoading method is triggered post a notification
[[NSNotificationCenter defaultCenter] postNotificationName:#"refreshView" object:nil];
In your viewController observe for this notification. You do it by adding this code to your init or viewDidLoad method
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(refreshView:) name:#"refreshView" object:nil];
Now implement -(void)refreshView:(NSNotification *) notification method in your viewController to manage your view to your liking.
If you are targeting iOS 4.0 and later, you can use the window's rootViewController property to get the current view controller.
[window.rootViewController viewWillAppear];
If you want your application to run on versions prior to iOS 4.0, then you could add an instance variable to the application delegate to remember which view controller called the upload method, having the controller send itself as a parameter.
- (void)upload:(UIViewController *)viewController {
self.uploadingViewController = viewController; // This is the property you add
...
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.uploadingViewController viewWillAppear];
self.uploadingViewController = nil;
}
You should also consider using a different method to reload the buttons, something like reloadButtons, since it is not related to the view appearing in this case. You would then call that method from within viewWillAppear.
Step 1:
In your App Delegate .h file you need to declare a protocol like so:
#protocol AppConnectionDelegate <NSObject>
#required
-(void)connectionFinished:(NSObject*)outObject;
#end
In the same file, add an ivar like so:
id *delegate;
Declare the ivar as a property:
#property (nonatomic, assign) id<AppConnectionDelegate> delegate;
In the App Delegate .m file, synthesize the ivar:
#synthesize delegate;
In the App Delegate .m file, on connectionDidFinishLoading do:
if([self.delegate respondsToSelector:#selector(connectionFinished:)])
{
[self.delegate connectionFinished:objectYouWantToSend];
}
In your viewcontroller's .h file, implement the AppConnectionDelegate by importing a reference to the app delegate file:
#import "AppDelegate_iPhone.h" //if using iPhone
#import "AppDelegate_iPad.h" //if using iPad
In the same file, at the end of the first line of the interface declaration do:
#interface AppDelegate_iPhone : AppDelegate_Shared <AppConnectionDelegate>
Declare ivars accordingly:
AppDelegate_iPhone *appDelegate; //if using iPhone
AppDelegate_iPad *appDelegate; // if using iPad
In your viewcontroller's .m file in the viewDidLoad(), get a reference to your app delegate using:
If iPhone;
appDelegate = (AppDelegate_iPhone*)[[UIApplication sharedApplication] delegate];
If iPad:
appDelegate = (AppDelegate_iPad*)[[UIApplication sharedApplication] delegate];
Then set the viewcontroller to be the delegate in viewDidLoad() by doing:
appDelegate.delegate = self;
Now you need to simply implement the connectionFinished method in the .m file:
- (void)connectionFinished:(NSObject*)incomingObject
{
//Do whatever you want here when the connection is finished. IncomingObject is the object that the app delegate sent.
}
Now whenever your app delegate's connectionDidFinishLoading is called, the view controller will be notified.
[It's a best practice to set appDelegate.delegate = nil if you're done using the connectionFinished callback]
This is tried and tested. If you have questions, leave a comment......
--EDIT--This is a robust alternative to NSNotification. I use both depending on the requirements. The process I use to decide between using NSNotification or a delegate callback using a protocol is simply:
For notifications:
One sender, multiple listeners.
No reference possible between sender and listener.
Complex/multiple objects need not be sent
For delegate callbacks using protocols:
One sender, limited (usually 1) listeners.
A reference between sender and listener is possible.
Complex/multiple objects are to be sent (for example, response objects that need to be sent)
I know sending objects is possible through notifications but I prefer protocols for that.
--EDIT--
Worse comes to worst, you can have both view controllers adhere to a simple one method protocol that will remove that button and refresh the view. Then in your connectionDidFinishLoading method, since you know your view controller must adhere to that protocol, by your design, you simply do something like
ViewController<MyProtocol> curView = (Get the current view controller somehow);
[curview refreshView];
I am using ASIFormDataRequest class to send and receive data from server to iphone client. I have a problem. For example, I have a View Controller using ASIFormDataRequest, if the current view is View Controller, the method requestFinished of ASIFormDataRequest delegate called successfully. But in some case, I pop back to the previous view of View Controller, that means the View Controller release, the ASIFormDataRequest object of View Controller class is also released. But the requestFinished still called. I debug and I see that View Controller has become a Zombie object, I don't know why the requestFinished method been called even object release. I also set in the dealloc method of ViewController
- (void) dealloc {
asiFormDataRequest.delegate = nil;
[super dealloc];
}
I think, better use your request not in this ViewController class, but create some independent class, and use from there you ASIRequests, using this class as delegate
btw, it helps me with similar situation
I have a problem. My view controller (ViewController) implement a delegate method of a object (DataPuller, data get from the internet). DataPuller will retrieve data on the internet without blocking user interaction with the view. But when I navigate between screen, in some cases, that ViewController release before DataPuller return the list of objects. The DataPuller return, it checks:
if (delegate && [delegate respondsToSelector:#selector(getCommentDidDownloadFinish:)]) {
[self.delegate performSelector:#selector(getCommentDidDownloadFinish:) withObject:self];
}
And the application crash here because ViewController release, it becomes a zombie object. Does anyone have this problem before and how to solve it? I think another way is using NSNotification, but I wonder any other better solutions. Any ideas, solutions are welcomes. Thanks.
Your view controller must remove itself as the DataPuller delegate at some point. Typicially, this is handled in the dealloc method:
- (void)dealloc {
dataPuller.delegate = nil;
[dataPuller release];
[super dealloc];
}
You may also decide to do this in -viewDidUnload or -viewDidDisappear:.
Delegation (usually) implies some sort of ownership - i.e., if you make an object a delegate of another object, usually the delegate object holds a strong reference (i.e., retains) the delegating object.
As an example, a UITableViewController is the delegate of its UITableView. This is okay, because the controller retains the tableview through the "view" property.
If your design does not allow ownership, use notifications, like you already suggested. As a bonus, notifications can signal multiple listeners if you would ever need that.
Don't forget to remove your observer in the dealloc of the view controller!
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.
I am using an UITableView to show some string messages, and I use NSOperationQueue to hold an customized NSOperation which fetchs message in background thread. After one message fetched successfully, customized NSOperation will notify the UITableView controller to show it.
If I click back button on the navigation bar to switch from the UITableView to other view after all message loaded, every thing is OK. But, if i click the back button while some message still loading, an EXC_BAD_ACCESS is throw. I have checked that the exception happened while customized NSOperation notify UITableView controller with performSelectorOnMainThread method. Sound like the target UITableView controller is not invalid after view switched, but I think Navigation Controller will hold the view controller instance. May I know how to resolve this issue? Thanks.
Customized operation is initialized in the UITableView controller with following code:
StatusMessageLoadingOperation *operation = [[StatusMessageLoadingOperation alloc]
initWithData:person
messageArray:cachedStatusMessages
target:self
action:#selector(didFinishStatusMessages:)];
[operationQueue addOperation:operation];
[operation release];
The customized NSOperation class will update UITableView with following code:
- (void)main{
for (int i = 0; i < [[person statusMessages] count]; i++) {
[target performSelectorOnMainThread:action withObject:messageArray waitUntilDone:NO];
}
}
Have you tried calling [operationQueue cancelAllOperations] in your viewWillDisappear method?
Because popping a view controller calls that controller's -dealloc method, you may be releasing your queue too early, and some other part of your application is trying to access the queue or an operation inside it that no longer exists.
My recommendation is to put your NSOperationQueue *myQueue instance into your application delegate.
Use the app delegate's -applicationDidFinishLaunching: and -dealloc methods to initialize and release your queue and its contents.
By separating your queue from the view controller, your queue won't get released when you pop off a view controller from your navigation stack. It and any remaining operations should still be available to the rest of the application.
To make it easier to access your queue, set up the following macro definition:
#define UIAppDelegate ((MyAppDelegate *)[UIApplication sharedApplication].delegate)
You can then use the macro to access your queue as follows, e.g.:
NSLog(#"%#", [[UIAppDelegate myQueue] operations]);
Or, for example:
[[UIAppDelegate myQueue] addOperation:myOperation];