3

I've got a UIViewController that presents/Dismisses another view controller with a custom animation using the transitioningDelegate pattern introduced in iOS 7.

I've noticed in my Crashlytics log files that this section of the code is occasionally crashing in production, and I haven't been able to sort out why. Its never crashed in my testing and looks to only affect about 10% of our users. iOS 7 and iOS 8 are affected as well as iPad's, iPhones, and iPods.

Heres the stack trace from Crashlytics:

Crashed: com.apple.main-thread
EXC_BAD_ACCESS KERN_INVALID_ADDRESS at 0x00014000

Thread : Crashed: com.apple.main-thread
0  libobjc.A.dylib                0x303c3f76 objc_msgSend + 21
1  UIKit                          0x25ed0ac5 -[UIViewController _customAnimatorForDismissedController:] + 64
2  UIKit                          0x25ed0927 -[UIViewController _dismissViewControllerWithTransition:from:completion:] + 538
3  UIKit                          0x25e7e4b3 -[UIViewController dismissViewControllerWithTransition:completion:] + 822
4  UIKit                          0x25e7e4b3 -[UIViewController dismissViewControllerWithTransition:completion:] + 822
5  UIKit                          0x25e7e127 -[UIViewController dismissViewControllerAnimated:completion:] + 222
6  Skin Creator                   0x000f38e7 -[SEUSBodySideChooserViewController imageEditViewController:didFinishWithImage:] (SEUSBodySideChooserViewController.m:251)
7  Skin Creator                   0x000ae2e9 -[SEUSImageEditViewController sendFinishedDelegateMethod] (SEUSImageEditViewController.m:409)
8  Skin Creator                   0x000d585f -[SEUSCloseMenuController circleMenu:didSelectItemAtIndex:] (SEUSCloseMenuController.m:69)
9  Skin Creator                   0x000ceb75 -[SEUSCircleMenu menuButtonPressed:] (SEUSCircleMenu.m:203)
10 UIKit                          0x25dec427 -[UIApplication sendAction:to:from:forEvent:] + 70
11 UIKit                          0x25dec3c9 -[UIControl sendAction:to:forEvent:] + 44
12 UIKit                          0x25dd6fcd -[UIControl _sendActionsForEvents:withEvent:] + 584
13 UIKit                          0x25debdf9 -[UIControl touchesEnded:withEvent:] + 584
14 UIKit                          0x25db0b47 _UIGestureRecognizerUpdate + 10158
15 UIKit                          0x25de5afd -[UIWindow _sendGesturesForEvent:] + 784
16 UIKit                          0x25de53cd -[UIWindow sendEvent:] + 520
17 UIKit                          0x25dbbb5d -[UIApplication sendEvent:] + 196
18 UIKit                          0x2602f4e3 _UIApplicationHandleEventFromQueueEvent + 13874
19 UIKit                          0x25dba59f _UIApplicationHandleEventQueue + 1294
20 CoreFoundation                 0x228dd5e7 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 14
21 CoreFoundation                 0x228dc9fb __CFRunLoopDoSources0 + 222
22 CoreFoundation                 0x228db079 __CFRunLoopRun + 768
23 CoreFoundation                 0x22828981 CFRunLoopRunSpecific + 476
24 CoreFoundation                 0x22828793 CFRunLoopRunInMode + 106
25 GraphicsServices               0x29ba9051 GSEventRunModal + 136
26 UIKit                          0x25e1a981 UIApplicationMain + 1440
27 Skin Creator                   0x000ead6f main (main.m:16)

Just so you can understand this a bit more [SEUSBodySideChooserViewController imageEditViewController:didFinishWithImage:] is a delegate callback where I dismiss the view controller.

Code I use to Present the View Controller

_toPixelEditTransitioningDelegate = [[SEUSFadeInTransitioningDelegate alloc] init];
_imageEditViewController.transitioningDelegate = _toPixelEditTransitioningDelegate;
[self presentViewController:_imageEditViewController animated:YES completion:nil];

Code I use to Dismiss the View Controller

[viewController dismissViewControllerAnimated:YES completion:nil];

My Transitioning Delegate

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    SEUSFadeInTransitioning *transitioning = [[SEUSFadeInTransitioning alloc] init];
    return transitioning;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    SEUSFadeInTransitioning *transitioning = [[SEUSFadeInTransitioning alloc] init];
    transitioning.reverse = YES;
    return transitioning;
}

Transition Object which implements the UIViewControllerAnimatedTransitioning protocol

- (instancetype)init
{
    if (self = [super init])
    {
        _reverse = NO;
        _duration = 0.25f;
    }
    return self;
}

- (void)animateToPixelEdit:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController* overviewVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController* pixelVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    [[transitionContext containerView] addSubview:overviewVC.view];
    [[transitionContext containerView] addSubview:pixelVC.view];

    pixelVC.view.alpha = 0;

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^
    {
        pixelVC.view.alpha = 1;
    }
    completion:^(BOOL finished)
    {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

- (void)animateToOverview:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController* pixelVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController* overviewVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    [[transitionContext containerView] addSubview:pixelVC.view];
    [[transitionContext containerView] addSubview:overviewVC.view];

    overviewVC.view.alpha = 0;

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^
    {
        overviewVC.view.alpha = 1;
    }
    completion:^(BOOL finished)
    {
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return _duration;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    if (self.reverse == NO)
    {
        [self animateToPixelEdit:transitionContext];
    }
    else
    {
        [self animateToOverview:transitionContext];
    }
}

Anyone notice anything that could be going wrong? Its really difficult to troubleshoot as I haven't been able to replicate the crash.

Joel Bell
  • 2,718
  • 3
  • 26
  • 32
  • Could it possible that the imageEditVC is calling its didFinish delegate method twice if a user taps multiple times? – Acey Nov 13 '14 at 18:08
  • Oh man. If its that simple i'm not sure if I should feel stupid or excited. Gunna test it out now! – Joel Bell Nov 13 '14 at 18:10
  • No luck replicating that so far, i'll code in a check for it just incase and push it out. In the mean-time anyone see anything else that could potentially be causing it? – Joel Bell Nov 13 '14 at 18:17
  • Okay another guess... is toPixelEditTransitioningDelegate strongly retained somewhere? – Acey Nov 13 '14 at 18:26
  • I think I figured it out, thanks to your comments. Looks like tapping quickly could present the imageEditVC twice, then when the user tried to close it after it had been presented on itself it would cause this crash. Thanks for your help! – Joel Bell Nov 13 '14 at 18:31
  • Feel free to make an answer if you want, i'll accept. – Joel Bell Nov 13 '14 at 18:33

2 Answers2

1

This code looks just dandy. I suspect users are able to double present or double dismiss your modal - that's a common failure point with these type of things.

Acey
  • 8,048
  • 4
  • 30
  • 46
  • That's true. In my case switching between segment controller tabs and animating the view controller upon taps were causing the crash. An easy fix would be to disable interaction on the UI control or just UIApplication.shared.beginIgnoringInteractionEvents() and re-enable when animations are done UIApplication.shared.endIgnoringInteractionEvents() – Raj D Jan 11 '21 at 06:39
0

I resolved a bug similar to this one, it ended up being the transitioningDelegate (_toPixelEditTransitioningDelegate in the question example) reference which had been deallocated.

If you see an EXEC_BAD_ACCESS you can enable NSZombies which will help you trace the bad reference'd object.

Hope this proves useful to someone else!

Oliver Atkinson
  • 7,970
  • 32
  • 43