Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is ARC Memory Management of presentViewController:animated:completion: broken in iOS 6.x?

My view controller is presenting a view via the presentViewController:animated:completion: method. The view is presented fine.

Then I dismiss this view and re-present it and get the following crash:

*** -[WebBrowser isKindOfClass:]: message sent to deallocated instance 0x1f640ac0

My code is using ARC. Here is the code of my WebBrowser class, a straightforward embedded browser.

WebBrowser.h:

@interface WebBrowser : ITViewController <UIWebViewDelegate, UIAlertViewDelegate>

@property (nonatomic, strong) NSString *URL;
@property (nonatomic, weak) IBOutlet UIWebView *webView;
@property (nonatomic, weak) IBOutlet UIActivityIndicatorView *spinner;

- (id)initWithURL:(NSString *)URL;
- (IBAction)dismissView:(id)sender;

@end

WebBrowser.m:

@implementation WebBrowser

- (id)initWithURL:(NSString *)URL_ {
    self = [super initWithNibName:@"MyNib" bundle:nil];
    if (self) {
        self.URL = URL_;
    }
    return self;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.webView.delegate = self;

    if (self.URL) {
        [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:self.URL]]];
    }
}

- (IBAction)dismissView:(id)sender {
    self.URL = nil;
    [self.webView stopLoading];
    self.webView.delegate = nil;
    [self dismissViewControllerAnimated:YES completion:NULL];
}

// + some non-related web view delegate stuff 

@end

And finally here is how I present the view in my parent view controller:

WebBrowser *browser = [[WebBrowser alloc] initWithURL:URL];
[self presentViewController:browser animated:YES completion:NULL];

I'm running iOS 6 and compiling with ARC.

First I thought this bug was ARC related. Here is my original post:

ORIGINAL POST

I've noticed crashes in my app with iOS 6.x when displaying modal view controllers and releasing it when it was working just fine with previous versions of iOS.

Blame me for not using ARC yet (it's my next big step on this project), but for instance, when displaying Game Center leaderboards with the following code, the following steps :

  1. display the leaderboard
  2. close the leaderboard
  3. display the leaderboard again (PRECISION UPDATE: by running the same showLeaderboard shown below, ie displaying a new instance of GKLeaderboardViewController)

then, the following error happens

*** -[GKLeaderboardViewController isKindOfClass:]: message sent to deallocated instance 0x17467120

This is my code:

- (void)showLeaderboard {
    if ([[GKLocalPlayer localPlayer] isAuthenticated]) {
        GKLeaderboardViewController *lb = [[GKLeaderboardViewController alloc] init];
        lb.category = ...;
        lb.leaderboardDelegate = self;
        self.modalPresentationStyle = UIModalPresentationCurrentContext;
        [self presentModalViewController:lb animated:YES];
        [lb release];
    } else {
        ...
    }
}

- (void)leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController{
    [self dismissModalViewControllerAnimated:YES];
}

It turns out that removing the [lb release] instruction solves my problem and, again, that no such crash happens with iOS 5.x.

The same happens with the Game Center achievements view controller, or any other custom view controllers of mine being displayed with presentModalViewController:.

It also seems that replacing the deprecated-presentModalViewController: instruction by the new presentViewController:animated:completion: DOES NOT solve the problem.

like image 290
Dirty Henry Avatar asked Dec 07 '12 13:12

Dirty Henry


2 Answers

I see at least one possible problem:

[self.webView stopLoading];
self.webView.delegate = nil;

In general, it is safer to set delegate to nil before calling stopLoading.

Also, it is definitely safer to use dismissViewControllerAnimated it the way it is supposed to be used, that is, call it on the presenting controller. Although the documentation states the call is passed to the presenting controller, it's not a good idea to call a method on an object which is being deallocated inside the method.

- (IBAction)dismissView:(id)sender {
    // pass the event to the presenting controller
    // the presenting controller should call [self dismissViewControllerAnimated:YES completion:NULL];
}

- (void)viewWillDissapear {
  [super viewWillDissapear];

   //no need to do this, it's done automatically in ARC
   //self.URL = nil;

   self.webView.delegate = nil;
   [self.webView stopLoading];
}
like image 66
Sulthan Avatar answered Nov 19 '22 23:11

Sulthan


Edit

According to Apple's documentation you SHOULD be dismissing the viewcontroller from the parent (presenting) viewcontroller: http://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/ModalViewControllers/ModalViewControllers.html#//apple_ref/doc/uid/TP40007457-CH111-SW14

"When it comes time to dismiss a presented view controller, the preferred approach is to let the presenting view controller dismiss it. In other words, whenever possible, the same view controller that presented the view controller should also take responsibility for dismissing it."

Also, can you please clear up which viewcontrollers are presenting which, and how many are overlayed on top of each other. from apples docs:

"If you present several view controllers in succession, and thus build a stack of presented view controllers, calling this method on a view controller lower in the stack dismisses its immediate child view controller and all view controllers above that child on the stack. When this happens, only the top-most view is dismissed in an animated fashion; any intermediate view controllers are simply removed from the stack. The top-most view is dismissed using its modal transition style, which may differ from the styles used by other view controllers lower in the stack."

So if you think it might have anything to do with delegate methods coming from your webview, you should probably be unsubscribing the view from the delegate property/stopping the webview from loading in viewWillUnload, not in the dismissal IBAction, since that won't necessarily be called.

I have edited this to make it more complete/clear

You need to set the browser view to a instance variable with a strong property before presenting the view. Then set it to nil after dismissing it.

First create a delegate protocol for Modals:

@protocol ModalViewDelegate
/**
 Delegation callback for when a modal has been dismissed
*/
- (void)modalDidDismiss:(UIViewController*)viewController;
@end

In your presenting view controllers interface, subscribe to the protocol:

@interface PresentingViewController <ModalViewDelegate>
@property (nonatomic,strong) WebBrowser *browserView;
@end

In your implementation when presenting the view:

WebBrowser *browser = [[WebBrowser alloc] initWithURL:URL];
browser.delegate = self
self.browserView = browser;
[self presentViewController:browser animated:YES completion:NULL];

In your WebBrowser interface:

@interface WebBrowser : ITViewController <UIWebViewDelegate, UIAlertViewDelegate>

@property (nonatomic, strong) NSString *URL;
@property (nonatomic, weak) IBOutlet UIWebView *webView;
@property (nonatomic, weak) IBOutlet UIActivityIndicatorView *spinner;

@property (nonatomic, weak) id <ModalViewDelegate> delegate;

- (id)initWithURL:(NSString *)URL;
- (IBAction)dismissView:(id)sender;

@end

In your WebBrowser implementation:

- (IBAction)dismissView:(id)sender {
    self.URL = nil;
    [self.webView stopLoading];
    self.webView.delegate = nil;
    [self.delegate didDismissBrowser:self];
}

And back in your parent view controller:

- (void)didDismissBrowser:(WebBrowser*)browser
{
    if (browser == self.browserView)
    {
        [self dismissViewControllerAnimated:YES completion:NULL];
        self.browserView = nil;
    }
}
like image 1
G. Shearer Avatar answered Nov 19 '22 22:11

G. Shearer