UINavigationController State Restoration (without Storyboards)
Asked Answered
C

3

18

I've been toying around with state restoration. In the code below, the scroll position of the UITableViewController gets restored, however, if I were to tap through into the detail view (pushing an instance of MyViewController onto the navigation stack), when the app restarts, it always returns to the first view controller in the navigation stack (i.e. MyTableViewController). Would somebody be able to help me restore to the correct view controller (i.e. MyOtherViewController)?

AppDelegate.m

- (BOOL)launchWithOptions:(NSDictionary *)launchOptions
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
        // Override point for customization after application launch.


        MyTableViewController *table = [[MyTableViewController alloc] initWithStyle:UITableViewStylePlain];
        table.depth = 0;
        UINavigationController *navCon = [[UINavigationController alloc] initWithRootViewController:table];
        navCon.restorationIdentifier = @"navigationController";

        self.window.rootViewController = navCon;

        self.window.backgroundColor = [UIColor whiteColor];
        [self.window makeKeyAndVisible];

    });

    return YES;
}

- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    return [self launchWithOptions:launchOptions];
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    return [self launchWithOptions:launchOptions];
}

MyTableViewController.m

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if(self)
    {
        self.restorationIdentifier = @"master";
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.title = @"Master";
    self.tableView.restorationIdentifier = @"masterView";
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 5;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 10;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    return [NSString stringWithFormat:@"Section %d", section];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if(!cell)
    {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }

    cell.textLabel.text = [NSString stringWithFormat:@"%d", indexPath.row];

    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyOtherViewController *vc = [[MyOtherViewController alloc] initWithNibName:nil bundle:nil];
    [self.navigationController pushViewController:vc animated:YES];
}

MyOtherViewController.m

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        self.restorationIdentifier = @"detail";
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.title = @"Detail";
    self.view.backgroundColor = [UIColor redColor];
    self.view.restorationIdentifier = @"detailView";
}
Caddell answered 17/1, 2013 at 10:18 Comment(2)
I've got something that I think will help you but I'm about to go away for the long (Australia Day) weekend here. If you've still not got an acceptable answer when I'm back I'll post it.Athey
Hi Andy did you sort out your problem?Haul
T
18

Because you are creating your detail view controller in code, and not instantiating it from a Storyboard, you need to implement a restoration class, so the system restoration process knows how to create the detail view controller.

A restoration class is really just a factory which knows how to create a specific view controller from a restoration path. You don't actually have to create a separate class for this, we can just handle it in MyOtherViewController:

in MyOtherViewController.h, implement the protocol: UIViewControllerRestoration

Then when you set the restorationID in MyOtherViewController.m, also set the restoration class:

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        self.restorationIdentifier = @"detail";
        self.restorationClass = [self class]; //SET THE RESTORATION CLASS
    }
    return self;
}

You then need to implement this method in the restoration class (MyOtherViewController.m)

    +(UIViewController *) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
   //At a minimum, just create an instance of the correct class. 
    return [[MyOtherViewController alloc] initWithNibName:nil bundle:nil];
}

That gets you as far as the restoration subsystem being able to create and push the Detail View controller onto the Navigation Stack. (What you initially asked.)

Extending the Example to handle state:

In a real app, you'd need to both save the state and handle restoring it... I added the following property definition to MyOtherViewController to simulate this:

@property (nonatomic, strong) NSString *selectedRecordId;

And I also modified MyOtherViewContoller's viewDidLoad method to set the Detail title to whatever record Id the user selected:

- (void)viewDidLoad
{
    [super viewDidLoad];
  //  self.title = @"Detail";
    self.title = self.selectedRecordId;
    self.view.backgroundColor = [UIColor redColor];
    self.view.restorationIdentifier = @"detailView";
}

To make the example work, I set selectedRecordId when initially pushing the Detail View from MyTableViewController:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyOtherViewController *vc = [[MyOtherViewController alloc] initWithNibName:nil bundle:nil];
    vc.selectedRecordId = [NSString stringWithFormat:@"Section:%d, Row:%d", indexPath.section,  indexPath.row];
    [self.navigationController pushViewController:vc animated:YES];
}

The other missing piece are the methods in MyOtherViewController, your detail view, to save the state of Detail controller.

-(void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
    [coder encodeObject:self.selectedRecordId forKey:@"selectedRecordId"];
    [super encodeRestorableStateWithCoder:coder];
}

-(void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
     self.selectedRecordId = [coder decodeObjectForKey:@"selectedRecordId"];
    [super decodeRestorableStateWithCoder:coder];
}

Now this actually doesn't quite work yet. This is because the "ViewDidLoad" method is called on restoration before the decoreRestorablStateWithCoder method is. Hence the title doesn't get set before the view is displayed. To fix this, we handle either set the title for the Detail view controller in viewWillAppear: , or we can modify the viewControllerWithRestorationIdentifierPath method to restore the state when it creates the class like this:

+(UIViewController *) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
    MyOtherViewController *controller =  [[self alloc] initWithNibName:nil bundle:nil];
    controller.selectedRecordId = [coder decodeObjectForKey:@"selectedRecordId"];
    return controller;
    
}

That's it.

A few other notes...

i presume you did the following in your App Delegate even though i didn't see the code?

-(BOOL) application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder
{
    return YES;
}
- (BOOL) application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder 
{
    return YES;
}

I saw a bit of flakiness when running the demo code in the simulator with it correctly preserving the Scroll offset. It seemed to work occasionally for me, but not all the time. I suspect this may be because really restoration of MyTableViewController should also use a restorationClass approach, since it's instantiated in code. However I haven't tried that out yet.

My testing method, was to put the app in the background by returning to springboard (required for the app state to be saved.) , then relaunching the app from within XCode to simulate a clean start.

Toby answered 23/1, 2013 at 1:35 Comment(4)
Thanks for your reply, but it's not quite what I'm looking for. "you'd need to both save the state and handle restoring it": I don't want to handle this state myself - that is what Apple's state restoration logic should do (see "State Preservation" on developer.apple.com/library/ios/#documentation/uikit/reference/… ). Also, you don't need to provide the restoration class, UIKit should infer it (see developer.apple.com/library/ios/#documentation/iphone/… )Caddell
From the Link you posted: "During preservation, your app is responsible for: Telling UIKit that it supports state preservation. Telling UIKit which view controllers and views should be preserved. Encoding relevant data for any preserved objects. During restoration, your app is responsible for: Telling UIKit that it supports state restoration. Providing (or creating) the objects that are requested by UIKit. Decoding the state of your preserved objects and using it to return the object to its previous state." -Apple can only infer so much.Toby
From the same link "For example, a navigation controller encodes information about the order of the view controllers on its navigation stack. It then uses this information later to return those view controllers to their previous positions on the stack."Caddell
I was able to get this code working properly, which is great. I struggled with this all day. However, if I throw log statements in either ViewController, I see that the init method (and other methods) are actually called twice. Once from appWillFinish, and then again within viewControllerWithRestorationIdentifierPath. Is that expected? If not, how do you code around that?Morphogenesis
P
1

the only problem with your code is navigation controller these two lines

UINavigationController *navCon = [[UINavigationController alloc] initWithRootViewController:table];
    navCon.restorationIdentifier = @"navigationController";

i dont know why navigation controller never get its restoration class. i had the same problem. i found some alternative solution to create a navigation controller stand alone in storyboard and use that.

here is code

UIStoryboard* storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" 
    bundle:nil];
UINavigationController* navigationController = [storyboard
    instantiateViewControllerWithIdentifier:
    @"UINavigationController"];

navigationController.viewControllers = @[myController];

it will work fine .

just follow this http://petr-pavlik.squarespace.com/blog/2013/6/16/stare-restoration-without-storyboard

Premillennialism answered 28/6, 2013 at 11:20 Comment(0)
H
0

Since you are doing this in code and not via Storyboard, you will need to provide not only a restorationIdentifier but also a restorationClass to you detail view controller.

You could leave it unassigned in which case -(UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder gets called on the application delegate to make the object (view controller with a restorationIdentifier but no restorationClass).

Try the following (please note the UIViewControllerRestoration protocol):

@interface MyViewController ()<UIViewControllerRestoration>
@end

@implementation

- (id)init
{
    self = [super init];
    if (self) {
        // Custom initialization
    }

    if( [self respondsToSelector:@selector(restorationIdentifier)] ){
        self.restorationIdentifier = @"DetailViewController";
        self.restorationClass = [self class];
    }
    return self;
}

#pragma mark - State restoration

+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
    UIViewController *viewController = [[self alloc] init];

    return viewController;
}

@end

Also note that this is a very simple example, usually you might have a lot more interesting things going on in -viewControllerWithRestorationIdentifierPath:coder:

Haul answered 5/5, 2013 at 21:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.