iPhone: modalview not closing - iphone

I have a function, in UpdateViewController, that is being called by a delegate, MyDownloadController, that will close a modal view (which is the UpdateViewController).
-(void)errorDownloading {
self.downloadController.delegate = nil;
[downloadController release];
[self dismissModalViewControllerAnimated:YES];
}
I've tried doing this with and without messing with the delegate pointer and it still doesn't close the view.
The delegate calls the method like this within MyDownloadController:
-(void)connectionError {
if([delegate respondsToSelector:#selector(errorDownloading)]){
[delegate errorDownloading];
}
}
And this function is called by a different delegate (MyConnectionController).
Is there anything wrong with having this many delegates? And would a pointer error or something with them effect the modalview being able to close? If so, how / why?
I have this structure for the delegations:
UpdateViewController (the actual modal view I am trying to close)
|- MyDownloadController (the controller that abstracts the process being done)
|- MyConnectionController (a helper class I wrote to interact with NSURLConnection)
|- NSURLConnection
What is the best way to diagnose this problem?

If downloadController is the view you want dismissed, I believe you're releasing it too soon.
-(void)errorDownloading {
[self dismissModalViewControllerAnimated:YES];
self.downloadController.delegate = nil;
[downloadController release];
}

Apple documentation says:
dismissModalViewControllerAnimated:
Dismisses the modal view controller that was presented by the receiver.
Mean's you call the the dismissModalViewControllerAnimated: method on the viewController that presented the ModalViewController you want to dismiss. in your case, this is the correct code to use.
-(void)errorDownloading {
self.downloadController.delegate = nil;
[downloadController release];
[self.parentViewController dismissModalViewControllerAnimated:YES];
}
Also answer to your other questions on number of delegates and pointers. Better design usually means you don't have huge strings of delegate objects but there's little reason to say thats wrong, it just gets messy IMHO. Pointers and the such as you described would most likely cause leaks or crashes, the reason it won't close is what I specified above, you weren't calling the method to the proper receiver.

Related

Check if delegate still exists before calling respondsToSelector

I have made a UIView sub class to deal with iAds and adMob. The view controller is the sub class delegate and everything works ok. Sometimes however, the view controller disappears before the ad has been fetched. To resolve this I set the delegate to nil in my view controller's dealloc implementation.
The problem I have is that sometimes the respondsToSelector: method is ran at the same time as the view controller is getting deallocated. This causes a crash. Does anyone know how I may rearrange this?
The ad view is on about 10 different view controllers so I would like one place to create the ad code.
Many Thanks
One easy way is retaining the view until you had a chance to set the delegate to nil, and then release it. Then you can ensure that the object is still alive and prevent crashes.
If you can't retain the view, then use a static method to get the instance which is cleared in dealloc. ie: instead of:
if (delegate && [delegate respondsToSelector:#selector(...)])
Do this:
if (s_myViewDelegate && [delegate respondsToSelector:#selector(...)])
In the class:
- (id) init {
s_myViewDelegate = self;
...etc...
}
- (void) dealloc {
s_myViewDelegate = nil;
}
if (delegate && [delegate respondsToSelector:#selector(...)])
Although this is a question long ago, I really messed with it a bit and finally found something that might help.
Set a completion block rather than a delegate for finished or failed event, and this would help.
Yes it's a problem with iAd and admob. I had also this kind of problem. I have solved the problem by adding add view on main window and make delegate to app delegate so app delegate will never deallocated until you close the application.
There is actually a fast and not really good solution - to use #try/#catch block. Just if you get to the #catch block your delegate fails for sure... like:
#try{
if (delegate && [delegate respondsToSelector:#selector(...)])
[delegate callBackMethod];
}
#catch (NSException *e){
// if you get here then delegate is no longer valid regardless its reference is still valid
}
You should not have 10 individual ad views, that's wrong on so many levels. Instead you should have just one that you either move between individual views or - smarter - just keep on top.
You can for example add a view to a tabBarController.view and this will stay present even if you switch tabs. For views that you don't want the ad on you can simply hide it.

List and detail controller

If I have 2 controllers; List and detail controller, what is the correct way to handle memory management for these 2 controllers?
I mean at what point should release be called on them?
Also in case my list controller is dynamic (i.e. data gets called from ext web service) and some data is passed to detail controller, where exactly should I write the code to retrieve/display the data in detail controller. I mean should it be viewDidLoad or viewWillAppear ?
Any examples would be great.
There is no single answer. But my answer is.... viewWillAppear
Take a detail view which is almost never used. So maybe you decide to create one each time it's used and destroy it after every use.
Take another detail view which may be used frequently. You decide create it once and just re-use it. Maybe you even destroy it on low-memory warnings and re-create it the next time it's used. In this case, you can't depend upon viewDidLoad being called each time the
Using viewWillAppear makes my code more consistent and makes it easier to make a change when I realize that detail view is being called a lot more than I expect. I should re-use it instead of creating it every time.
As to when you should release them... that really depends on what perspective. How often is it used? How much memory does it take to simply exist? How much work does it take to re-create?
From my experience, the best way to istantiate the detail view controller is:
MyDetailController * dc = [[MyDetailController alloc] initWithMyObject: anObject];
[self.navigationController pushViewController: dc animated: YES];
[dc release];
where anObject is the piece of your downloaded data you want to present in the detail view controller.
I assume your list controller is the main one of your app, which should be instantiate as follows:
- (BOOL) application: (UIApplication *) application didFinishLaunchingWithOptions: (NSDictionary *) launchOptions {
_baseWindow = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
_listVC = [MyListController new];
[_baseWindow addSubview: _listVC.view];
[_baseWindow makeKeyAndVisible];
return YES;
}
And released:
- (void) dealloc {
[_listVC release]; _listVC = nil;
[_baseWindow release]; _baseWindow = nil;
[super dealloc];
}
Remember that -viewDidLoad is called after -loadView, which in turn is called when someone tries to access the -view property of the view controller.
Thus you may want to prepare your view in -viewDidLoad and do some additional tasks in -viewWillAppear or -viewDidAppear. Please note that -viewWillAppear (and similar methods) is called EVERY TIME that view controller's view is shown on screen. That is, for example, if you push another VC from detail vc and then pop, -viewDidAppear will be called again.

Knowing when -viewWillAppear triggered but -viewWillDisappear hasn't

Is there a way to know that a view controller is somewhere in the view controller lifecycle between -viewWillAppear and -viewWillDisappear?
I ask because I want to be damned sure that a method isn't fired when my view is either not on screen, or is about to disappear from the screen. When the view is about to disappear from the screen, certain objects which I cannot explicitly check at runtime may or may not be deallocated, so, obviously, interacting with them can lead to message sent to deallocated instance errors.
At present, I'm keeping track with a BOOL, like so:
- (void)viewWillAppear:(BOOL)animated {
isOnScreen = YES;
[super viewWillAppear:animated];
}
- (void)willWillDisappear:(BOOL)animated {
isOnScreen = NO;
[super viewWillAppear:animated];
}
And my method looks like this:
if (isOnScreen) [self doSomething];
Is there a simpler way to do this?
your way seems to be the simplest approach, if not the most robust. (simply checking if that instance of the view exists seems like the correct approach (if it hasn't been dealloced yet)).
I also don't REALLY understand the purpose of this, unless you have another view controller running methods that pertain to the view controller being showed that you are using the boolean for. In that case, its more a design problem than an upkeep problem.
Use viewDidDisappear
- (void)viewDidDisappear:(BOOL)animated {
// Do stuff that has to be done when this view is off screen.
}

presentModalViewController crashes my app

It's one of the simplest things to do, I know. But I've been banging my head against this for days. I've done it plenty of times in the past, but for some reason trying to present a modal view controller just crashes the app to a black screen. Nothing reported in the console or anything. I'm hoping someone might have had this problem and has some advice.
This code is called from a UIViewController class:
MFMailComposeViewController *controller = [[MFMailComposeViewController alloc] init];
controller.mailComposeDelegate = self;
[controller setSubject:#"test subject"];
[controller setMessageBody:#"this is the message body" isHTML:NO];
[self presentModalViewController:controller animated:YES];
As Andrew has pointed out in his comment, do you check
+[MFMailComposeViewController canSendMail]
before trying to push the view controller? The behavior of the MFMailComposeViewController is not well defined if this method returns NO (which may also well be the case when running on the simulator, though I'm not sure). From the documentation:
Before using this class, you must
always check to see if the current
device is configured to send email at
all using the canSendMail method. If
the user’s device is not set up for
the delivery of email, you can notify
the user or simply disable the email
dispatch features in your application.
You should not attempt to use this
interface if the canSendMail method
returns NO.
Have you tried to push another view controller instead? Does this crash your app, too?
Are you showing another modal view controller before trying to show MFMailComposeViewController? I had the same problem and found a workaround:
- (void)peopleMultiPickerNavigationController:(PeopleMultiPickerNavigationController *)peoplePicker
didSelectContacts:(NSArray *)contacts {
[self dismissModalViewControllerAnimated:YES];
// some more code here
[self performSelector:#selector(sendEmail) withObject:nil afterDelay:0.45]; // this works only if delay > ~0.4!
// [self sendEmail]; // this won't work
// some more code here
}
- (void) sendEmail {
Class mailClass = (NSClassFromString(#"MFMailComposeViewController"));
if (mailClass != nil) {
// We must always check whether the current device is configured for sending emails
if ([mailClass canSendMail]) {
[self displayComposerSheet:emails];
} else {
[self launchMailAppOnDevice:emails];
}
} else {
[self launchMailAppOnDevice:emails];
}
}
I know it's an ugly workaround, but I didn't found anything better :(
I don't know how relevant this is, but I have been having horrible problems trying to present the MFMailComposeViewController after returning to my main view controller from ANOTHER modal view controller. It just would not work. I was trying to dismiss this other modal view controller before launching the mail controller, and found my solution to be to not call:
[self dismissModalViewControllerAnimated:YES];
But to instead call:
[self dismissModalViewControllerAnimated:NO];
Then go ahead and present the mail view controller.
That one change made all the difference in my case. I suspect this is connected to the problem sgosha was having. Just switch off the animation, rather than putting a delay in (which is probably just waiting until the animation has completed). Looks like a bug in the framework to me.
I should probably explain further. I have a share button on my main view controller, which modally pops up a table view to allow to user to choose what they want to share. From here, if they tap on Email, they get a UIActionSheet letting them further decide which files they wish to attach to their email.
It is possible the UIActionSheet in the mix is contributing to the problem. The actual dismissal of the modal view controllers is taking place in the delegate methods back in my main view controller, and it is this main view controller which tries to launch the mail view controller after dismissing the modal table view controller.
Yes! I did it! I can not believe, but I solved the problem! That's related to:
Opening an MFMailComposeViewController as modal from (and over) some other opened modal controller
Unfortunately, I have to admit that, again, in times of expensive-like-a-Devil-iPhones5, Apple still forses developers to use old, buggy and not convenient code and components! The great example of that is MFMailComposeViewController.
But now let's follow to more pleasant things.
What do we have:
This problem appeared as on iPhone with iOS 5.1 as on Simulator with iOS 6.
The core problem was when you are trying to open the email controller as a modal one from another modal controller.
[MFMailComposeViewController canSendMail] had absolutely no effect for me - it did not work in both ways (it was crashing with or without email functionality available).
[self dismissModalViewControllerAnimated:NO/YES] did not change general sense - it was crashing in both ways, but with a little different behavior.
I was trying to use a standard approach with mailComposeDelegate (like '.mailComposeDelegate = self').
I was calling the email sending controller from both: common (first-level) controller as well as from modal (second-level) one at the same app.
App was not always crashing - sometimes the controller just could not dismiss itself (buttons 'cancel' and 'send' were active, but no actions processed). Dependenly on conditions (who is parent opened the controller, was animation or not on appearing etc).
There were also no difference if any email recepients were added or not.
So, what my killed 5 working hours discovered is that seems the delegate is "releases" somehow somewhen. I can suppose that if you are opening the email controller as modal over some other modal controller, that (previously modal) controller is being cleaned by garbage collector or some other way and that way the delegate is being cleaned as well (I have no wish to kill several more hours for detailed digging, so I'd leave that to Apple's conscience).
Anyway, in two words, my solution was
to hold the delegate object somewhere with "strong" reference.
in my case, the owner of that delegate was main view controller class (which is always available in my case, as most of the app logic is working with it). That can be AppDelegate instance also.
It would look like:
#interface SharingTools : NSObject <MFMailComposeViewControllerDelegate>
#property UIViewController* currentParentController;
...
-(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error
{
if (error)
{
...
}
else
{
// I pass it somewhere before calling the email controller dialog opener method,
// because it's different in case we open the email sender
// from first-level controller or second- (the modal one)
[currentParentController dismissViewControllerAnimated:YES completion:^{
if (_onResult) {
((void(^)(bool))_onResult)(result == MFMailComposeResultSent);
}
}];
}
// and of course clearing all the references etc here
currentParentController.mailComposeDelegate = nil;
currentParentController = nil;
}
-(void)sendEmail //... params here
// (possibly, including to store also currentParentController)
{
currentParentController = ... ;
[currentParentController presentViewController:
newlyCreatedEmailController animated:YES completion:nil];
}
#end
#interface MyMainViewController : UIViewController
#property SharingTools* sharing; // initialize somewhere (on viewDidLoad, for instance)
...
-(void)showSettings
{
...
[self presentModalViewController:settingsController animated:YES];
}
#end
#interface SettingsViewController : UIViewController
...
-(void)sendEmailSupport
{
// again, this is up to you where and how to have either reference
// to main controller (or any other owner of the delegate object)
// or sharing directly
[myMainViewControllerInstance.sharing sendEmail: ... parentController:self];
}
#end
Frankly saying, the code is kind of messy, but this is just general thoughts.
I hope, you'll manage your own much nicer.
Good luck and God bless Microsoft! ^^

How to safely shut down a loading UIWebView in viewWillDisappear?

I have a view containing a UIWebView which is loading a google map (so lots of javascript etc). The problem I have is that if the user hits the 'back' button on the nav bar before the web view has finished loading, it is not clear to me how to tidily tell the web view to stop loading and then release it, without getting messages sent to the deallocated instance. I'm also not sure that a web view likes its container view disappearing before it's done (but I've no choice if the user hits the back button before it's loaded).
In my viewWillDisappear handler I have this
map.delegate=nil;
[self.map stopLoading];
this seems to handle most cases OK, as nil'ing the delegate stops it sending the didFailLoadWithError to my view controller. However if I release the web view in my view's dealloc method, sometimes (intermittently) I will still get a message sent to the deallocated instance, which seems to be related to the javascript running in the actual page, e.g.:
-[UIWebView webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:]: message sent to deallocated instance 0x4469ee0
If I simply don't release the webview, then I don't get these messages though I guess I'm then leaking the webview.
If I don't send the 'stopLoading' message, and simply release the webview within viewWillDisappear, then I see messages like this:
/SourceCache/WebCore/WebCore-351.9.42/wak/WKWindow.c:250 WKWindowIsSuspendedWindow: NULL window.
Possibly related, I sometimes (again totally intermittent) get an ugly heisenbug where clicking the back button on some other view's navbar will pop the title, but not the view. In other words I get left with the title of view n on the stack, but the view showing is still view n+1 (the result is you're trapped on this screen and cannot get back to the root view - you can go the other direction, i.e. push more views and pop back to the view that didn't pop corrrectly, just not to the root view. The only way out is to quit the app). At other times the same sequence of pushes and pops on the same views works fine.
This particular one is driving me nuts. I think it may be related to the view disappearing before the web view is loaded, i.e. in this case I suspect it may scribble on memory and confuse the view stack. Or, this could be completely unrelated and a bug somewhere else (i've never been able to reproduce it in debug build mode, it only happens with release build settings when I can't watch it with gdb :-). From my debug runs, I don't think I'm over-releasing anything. And I only seem to be able to trigger it if at some point I have hit the view that has the web view, and it doesn't happen immediately after that.
A variation on this should fix both the leaking and zombie issues:
- (void)loadRequest:(NSURLRequest *)request
{
[self retain];
if ([webView isLoading])
[webView stopLoading];
[webView loadRequest:request];
[self release];
}
- (void)webViewDidStartLoad:(UIWebView *)webView
{
[self retain];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
[self release];
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
[self release];
}
- (void)viewWillDisappear
{
if ([webView isLoading])
[webView stopLoading];
}
- (void)dealloc
{
[webView setDelegate:nil];
[webView release];
[super dealloc];
}
There's a few ways to handle it, but this should work. You want the didFailLoadWithError message, it's what tells you it's stopped.
Set a flag isLeaving=YES;
Send the Webview a stopLoading.
In didFailLoadWithError:, check for the error you get when
the webview stops:
if ((thiserror.code == NSURLErrorCancelled) && (isLeaving==YES)) {
[otherClass performSelector:#selector(shootWebview) withObject:nil withDelay:0]
}
release the webView in shootWebview:
variations:
if you want to be cavalier about it, you can do the performSelector:withObject:withDelay: with a delay of [fillintheblank], call it 10-30 seconds without the check and you'll almost certainly get away with it, though I don't recommend it.
You can have the didFailLoadWithError set a flag and clean it up somewhere else.
or my favorite, maybe you don't need to dealloc it all when you leave. Won't you ever display that view container again? why not keep it around reuse it?
Your debug being different from release issue, you might want to check your configuration to make sure that it's exactly the same. Bounty was on the reproducible part of the question, right? ;-).
--
Oh wait a second, you might be taking a whole View container down with the WebView. You can do a variation on the above and wait to release the whole container in shootWebView.
The UINavigationController bug you're describing in the second part of your post might be related to your handling of memory warnings. I've experienced this phenomenon and I"ve been able to reproduce it on view n in the stack by simulating a memory warning while viewing view (n+1) in the stack.
UIWebView is a memory eater, so getting memory warnings wouldn't be surprising when it's used as part of a view hierarchy.
A simple release message in dealloc ought to be enough.
Your second problem sounds like a prematurely deallocated view, but I can't say much without seeing some code.
I had a similar problem to this using a UIWebView in OS3 - this description was a good starting point, however I found than simply nil'ing out the web view delegate before releasing the webView solved my problem.
Reading the sample code (the accepted answer - above) - it seems like a lot of overkill. E.g. [webView release] and webView = nil lines do exactly the same thing given the way the author describes the variable is declared (so you don't need both). I'm also not fully convinced by all the retain and release lines either - but I guess your mileage will vary.
Possibly related, I sometimes (again
totally intermittent) get an ugly
heisenbug where clicking the back
button on some other view's navbar
will pop the title, but not the view.
In other words I get left with the
title of view n on the stack, but the
view showing is still view n+1 (the
result is you're trapped on this
screen and cannot get back to the root
view - you can go the other direction,
i.e. push more views and pop back to
the view that didn't pop corrrectly,
just not to the root view. The only
way out is to quit the app). At other
times the same sequence of pushes and
pops on the same views works fine.
I have the same problem, when I'm use navigation controller with view controllers in stack > 2 and current view controller index > 2, if an memoryWarning occurs in this momens, it raises the same problems.
There is inly 1 solution, which I found after many experiments with overriding pop and push methods in NavigationController, with the stack of view controllers, with views and superviews for stacked ViewControllers, etc.
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#interface FixedNavigationController :
UINavigationController <UINavigationControllerDelegate>{
}
#end
#import "FixedNavigationController.h"
static BOOL bugDetected = NO;
#implementation FixedNavigationController
- (void)viewDidLoad{
[self setDelegate:self];
}
- (void)didReceiveMemoryWarning{
// FIX navigationController & memory warning bug
if([self.viewControllers count] > 2)
bugDetected = YES;
}
- (void)navigationController:(UINavigationController *)navigationController
didShowViewController:(UIViewController *)viewController
animated:(BOOL)animated
{
// FIX navigationController & memory warning bug
if(bugDetected){
bugDetected = NO;
if(viewController == [self.viewControllers objectAtIndex:1]){
[self popToRootViewControllerAnimated:NO];
self.viewControllers = [self.viewControllers arrayByAddingObject:viewController];
}
}
}
#end
It works fine for 3 view controllers in stack.