iosobjective-cdelegatesuikit-state-preservation

State Preservation for View Controllers with Custom Delegates or Data Sources


I am attempting to use iOS 6+ (my app is 7.0+) State Preservation to preserve a view that is presented modally from another View Controller. As such it, has the typical modal view controller dismissal pattern:

TNTLoginViewController.h contains

@protocol TNTLoginViewControllerDelegate <NSObject>

- (void)TNTLoginViewControllerDismiss:(TNTLoginViewController *)controller;

@end

@interface TNTLoginViewControllerDelegate : NSObject

@interface TNTLoginViewController : UIViewController

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

- (IBAction)getStarted:(id)sender;

@end

getStarted: implementation

- (IBAction)getStarted:(id)sender
{
    // Perform login
    ...

    // Dismiss me
    [self.delegate TNTLoginViewControllerDismiss:self];
}

TNTLoginViewControllerDismiss: method on delegate, which presented the modal

- (void)TNTLoginViewControllerDismiss:(TNTLoginViewController *)controller
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

And it all works like a charm! Until State Preservation. Simply put, I don't know how TNTLoginViewController would preserve its delegate. I understand why it can't: it's just a pointer! So I tried various ways of deriving the delegate instead:

  1. Restoration class: sadly, as a class method, viewControllerWithRestorationIdentifierPath:coder: doesn't help me point to my specific presenting View Controller.
  2. Set my presenting VC as my modal VC's delegate in the Storyboard: Xcode wouldn't let me draw that connection, even when my presenting VC's class publicly adopted the TNTLogingViewControllerDelegate> protocol in its header. That may be a separate issue, or this may not be allowed.
  3. Use the application-delegate-level application:viewControllerWithRestorationIdentifierPath:coder: to return a modal view controller with its delegate set to my presenting View Controller. I have to be able to derive that presenting VC from the App Delegate, but it might work.

I'm going with #3 for now, but if there is a better solution someone could recommend, I would be thrilled.

Setups that would yield similar problems:

  1. Setting a data source, say for a table view.

Solution

  • You are right this is possible to do from the application-delegate level with application:viewControllerWithRestorationIdentifierPath:coder:, but you need to be careful/cleaver in how you do this!

    The goal here is to return a TNTLoginViewController during the state restoration process with its delegate set to its parent.

    First you must create a TNTLoginViewController object. You mentioned a storyboard so I will load it from there. I will assume that you have a fairly standard setup with a Main.storyboard file, and the identity properly set in the Identity Inspector.

    TNTLoginViewController * loginViewController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"loginViewController"];
    

    Next you need to set its delegate to a parent. I am going to assume that there is a UINavigationController connecting this model. To find this from the application-delegate object you will need to dig into its window property.

    The window property is a UIWindow object which has another property called rootViewController. This is a UIViewController object. Since I am assuming there is a UINavigationController connecting your model you will need to typecast this UIViewController to a UINavigationViewController (I would place the link I cannot at my current reputation level).

    Now you can use the topViewController property the controller at the top of your navigation stack which is what you want to set as your delegate! If not then you can navigate your UINavigationController object for which object you want as your delegate.

    And remember, since you are setting a delegate from the application-delegate level you might need to specify your protocol here to avoid vagueness.

    To implement these last four steps in code will look something like this.

    loginViewController.delegate = (id <TNTLoginViewControllerDelegate>)((UINavigationController *) self.window.rootViewController).topViewController;
    

    And then you can return your TNTLoginViewController with its delegate properly set!

    Make sure not to forget the implications of using application:viewControllerWithRestorationIdentifierPath:coder:. You only want to do this for the case of restoring your TNTLoginViewController. Luckily you can check for this with the identifierComponents argument which is passed in. Compare this to your identity name in the Identity Inspector and return nil if they do not match.

    Your final method in the AppDelegate.m file will look something like this.

    - (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
    {
    if ([[identifierComponents lastObject] isEqualToString:@"loginViewController"]) {
        TNTLoginViewController * loginViewController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"loginViewController"];
    
        loginViewController.delegate = (id <TNTLoginViewControllerDelegate>)((UINavigationController *) self.window.rootViewController).topViewController;
    
        return loginViewController;
    }
    
    return nil;
    }
    

    I hope this helps!