Last Updated: February 25, 2016
·
1.63K
· Andrej Mihajlov

State restoration for controllers presented with custom transition

Unfortunately state restoration does not take into account such things as how preserved controller was presented. However we can fix that with a simple category:

//
//  UIViewController+CustomTransitionRestoration.m
//
//  An extension for UIViewController to restore state of controllers
//  presented with custom transitioning delegate.
//
//  This extension saves transitioning delegate in restoration archive
//  therefore I believe only Storyboard controllers can be referenced.
//  Otherwise same controllers will be instantiated twice on state restoration.
//
//

static NSString* const kStateRestorationModalPresentationStyleKey = @"modalPresentationStyle";
static NSString* const kStateRestorationTransitioningDelegateKey = @"transitioningDelegate";
static NSString* const kStateRestorationModalPresentationCapturesStatusBarAppearanceKey = @"modalPresentationCapturesStatusBarAppearance";

@implementation UIViewController (CustomTransitionRestoration)

- (void)encodeRestorableStateForCustomTransitionWithCoder:(NSCoder *)coder {
    // Encode custom transition properties
    [coder encodeInteger:self.modalPresentationStyle forKey:kStateRestorationModalPresentationStyleKey];
    [coder encodeInteger:self.modalPresentationCapturesStatusBarAppearance forKey:kStateRestorationModalPresentationCapturesStatusBarAppearanceKey];
    [coder encodeObject:self.transitioningDelegate forKey:kStateRestorationTransitioningDelegateKey];

    if([self.transitioningDelegate isKindOfClass:[UIViewController class]]) {
        if( !((UIViewController*)self.transitioningDelegate).storyboard ) {
            NSLog(@"The transitioning delegate used for %@ is not a storyboard-based view controller. Therefore it will not be referenced but instantiated twice during state restoration. This may lead to unexpected behavior.", NSStringFromClass(self.class));
        }
    }
}

- (void)decodeRestorableStateForCustomTransitionWithCoder:(NSCoder *)coder {
    // Decode custom transition properties
    self.modalPresentationStyle = [coder decodeIntegerForKey:kStateRestorationModalPresentationStyleKey];
    self.modalPresentationCapturesStatusBarAppearance = [coder decodeIntegerForKey:kStateRestorationModalPresentationCapturesStatusBarAppearanceKey];
    self.transitioningDelegate = [coder decodeObjectForKey:kStateRestorationTransitioningDelegateKey];
}

@end

You can use this category in combination with standard state restoration methods.

Another bug that you may experience in certain configurations is that after state restoration unwinding may not work.

Apparently some other important information was missed during state preservation so now your controller may ask your navigation controller or root view controller to provide unwinding segue.

I think you can stumble upon such issue if you use navigation controllers. So to fix that you should subclass your navigation controller and return proper controller for unwinding.

I have a more complex setting with drawer controller as root controller and navigation controller as central controller, so in my case I had to subclass drawer controller and return proper unwind segue. The best way was to check against unwind selectors because they are sort of unique across the app:

- (UIViewController*)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
    if([NSStringFromSelector(action) hasPrefix:@"unwindToInitialViewController"]) {
        return [[((UINavigationController*)self.centerViewController) viewControllers] firstObject];
    } else if([NSStringFromSelector(action) hasPrefix:@"unwindToLastViewController"]) {
        return [((UINavigationController*)self.centerViewController) topViewController];
    }

    UIViewController* c = [super viewControllerForUnwindSegueAction:action fromViewController:fromViewController withSender:sender];

    return c;
}

Of course you should carefully monitor what UIKit is doing while state restoration and adapt your code depending on that behavior because it can change depending on your controllers hierarchy.