Search code examples
iosobjective-cuiscrollviewuiscrollviewdelegate

Cannot form weak reference to UIScrollView sub class (EXC_BAD_INSTRUCTION) when dismissing modal view


Hi everyone I've been debugging this issue for quite some time but no luck so far. I am quite lost here and have no clue on the reason causing this crash and how to fix it. I will be very grateful if anyone can offer me some help on this, thanks a lot!

I've prepared a sample project to demonstrate the issue at GitHub here.

The scenario is as the following:

  1. There are two view controllers, namely the root view and modal view, each has a custom scroll view (class namely SubScorllView) as sub view, and the modal view has a button for dismissing the modal view.

  2. The scroll views are sub-classes of UIScrollView, each with their corresponding delegate protocol, and their class hierarchy is as below:

UIScrollView
∟ SuperScrollView
.....∟ SubScrollView

The app launches and runs in a very simple manner, in AppDelegate's didFinishLaunchingWithOptions:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor blackColor];

    RootViewController * rootVC = [[RootViewController alloc] init];
    self.navVC = [[UINavigationController alloc] initWithRootViewController:rootVC];
    self.navVC.navigationBarHidden = TRUE;

    self.window.rootViewController = self.navVC;
    [self.window makeKeyAndVisible];

    ModalViewController *modalVC = [[ModalViewController alloc] init];
    [self.navVC presentViewController:modalVC animated:YES completion:nil];

    return YES;
}

And the views are loaded from xib files, which the scroll views' delegate are also set inside, and there are some overrides regarding the methods for initiating and setting delegate for the scroll view sub-classes.

The problem occurs when I dismiss the modal view through clicking the "Close" button in the modal view, when the button is clicked, the following happens:

- (IBAction)didPressedCloseButton:(id)sender {

    self.subScrollView.delegate = nil;
    [self dismissViewControllerAnimated:YES completion:nil];

}

And the app crashes at the following segment in SuperScrollView:

- (void)setDelegate:(id<SuperScrollViewDelegate>)delegate {

    _superScrollViewDelegate = delegate;

    // trigger UIScrollView to re-examine delegate for selectors it responds
    super.delegate = nil;
    super.delegate = self;  // app crashes at this line
}

With the following error message in the console:

objc[6745]: Cannot form weak reference to instance (0x7fa803839000) of class SubScrollView. It is possible that this object was over-released, or is in the process of deallocation.

I don't understand why the app would crash and giving the above error message, or how should I fix it. I tried to search with the error message but seems that message is mostly related to other classes like text views, while some others solved it with setting the delegate of the scroll view to nil before deallocating but it doesn't work in my case.

==========

Update: Just tested if this happens on iOS 8 with simulator, it doesn't crash like on iOS 9 at all.


Solution

  • When the SuperScrollView is deallocated, setDelegate is called implicitly. In iOS 9, you cannot set the delegate to self, because self is in the process of being deallocated (no idea why this worked in iOS 8). To work around this issue, you can check first to see if the passed in delegate parameter is not nil, and only then set super.delegate to self:

    - (void)setDelegate:(id<SuperScrollViewDelegate>)delegate {
    
        _superScrollViewDelegate = delegate;
    
        // trigger UIScrollView to re-examine delegate for selectors it responds
        super.delegate = nil;
    
        if(delegate)
        {
            super.delegate = self;
        }
    }
    

    If for some reason you need to support self responding to the UIScrollView delegate methods even when _superScrollViewDelegate is nil, you could create a parameter

    @interface SuperScrollView ()
    
    @property (nonatomic, weak) SuperScrollView * weakSelf;
    
    @end
    

    at the top of the file, and set it in setup

    - (void)setup {
    
        super.delegate = self;
        self.weakSelf = self;
    }
    

    Then, in setDelegate, just check that weakSelf is not nil. If weakSelf is nil, then self is in the process of deallocating and you should not set it to the super.delegate:

    - (void)setDelegate:(id<SuperScrollViewDelegate>)delegate {
    
        _superScrollViewDelegate = delegate;
    
        // trigger UIScrollView to re-examine delegate for selectors it responds
        super.delegate = nil;
    
        if(self.weakSelf)
        {
            super.delegate = self;
        }
    }