Help needed for silver challenge


#1

I’ve done everything on the HypnoNerd application that we did on Homepwner up to the bottom of p463 in the book. When I build and run, on the simulator, I get a blank screen with a blank UITabBarController. On the console I get this:

I realise that I’m probably doing something bone headed but I have absolutely no idea what this message means. I’m not trying to rotate anything. Any help would be appreciated, or even just post your solution if you have got your’s to work.


#2

The main issue with this challenge is that empty tab bar shows up after the restoration. While debugging, I found there was no viewControllers set in UITabBarController after restoration, so it was necessary to add child controller manually to tab bar controller. I hope someone would explain why it happens.


BNRHypnosisViewController.m

#import "BNRAppDelegate.h"

@property (nonatomic, strong) UITextField *textField;
@property (nonatomic, strong) NSString *labelText;

+(UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents
                                                           coder:(NSCoder *)coder
{
    NSLog(@"[%@][%@]", NSStringFromClass([self class]), NSStringFromSelector(_cmd));

    BNRAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    UITabBarController *tbc = (UITabBarController *)appDelegate.window.rootViewController;
    
    BNRHypnosisViewController *hvc = [[self alloc] init];
    [tbc addChildViewController:hvc];
    
    return hvc;
}

-(void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
    [super encodeRestorableStateWithCoder:coder];

    [coder encodeObject:self.textField.text forKey:@"HypnosisTextFieldText"];
    [coder encodeObject:self.labelText forKey:@"HypnosisLabelText"];
}

-(void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
    [super decodeRestorableStateWithCoder:coder];
    
    self.textField.text = [coder decodeObjectForKey:@"HypnosisTextFieldText"];
    self.labelText = [coder decodeObjectForKey:@"HypnosisLabelText"];
    [self drawHypnoticMessage:self.labelText];
}

-(instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    ...
    if (self) {
        self.restorationIdentifier = NSStringFromClass([self class]);
        self.restorationClass = [self class];
    ...
        
-(void)loadView
{
    ...
    self.textField = [[UITextField alloc] initWithFrame:textFieldFrame];
    ...    

-(void)drawHypnoticMessage:(NSString *)message
{
    ...        
        self.labelText = [message copy];
    }
}


BNRHypnosisView.m

-(void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
    [super encodeRestorableStateWithCoder:coder];

    [coder encodeObject:self.circleColor forKey:@"circleColor"];
}

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

- (id)initWithFrame:(CGRect)frame
{
    ...
    if (self) {
        self.restorationIdentifier = @"BNRHypnosisView";
    ...
        

BNRReminderViewController.m

#import "BNRAppDelegate.h"

+(UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents
                                                           coder:(NSCoder *)coder
{
    BNRAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    UITabBarController *tbc = (UITabBarController *)appDelegate.window.rootViewController;
    
    BNRReminderViewController *rvc = [[self alloc] init];
    [tbc addChildViewController:rvc];
    
    return rvc;
}

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

-(void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
    [super decodeRestorableStateWithCoder:coder];

    NSDate *date = [coder decodeObjectForKey:@"ReminderViewDatePickerDate"];
    if (date) {
        self.datePicker.date = date;
    }
}

-(instancetype)initWithNibName:(NSString *)nibNameOrNil
                        bundle:(NSBundle *)nibBundleOrNil
{
    ...
    if (self) {
        self.restorationIdentifier = NSStringFromClass([self class]);
        self.restorationClass = [self class];
    ...

#3

sunny4s has a good way to overcome the problem, but it feels like the extra work should not be necessary and I’m missing something. Every piece of documentation and guide I’ve found indicates that tabs (and which tab is selected) are automatically restored. I’ve tried setting up simple tests using storyboards and code, and I can’t figure it out.

I expect I’m missing something simple. Can anyone provide code in which a UITabBarController is successfully preserved and restored using only the methods outlined in the book? Or is the best way to go about this the solution sunny4s has presented? I’d appreciate any insight, it has been several days since I attempted the challenge and this has continued to bother me :slight_smile:


#4

The tab bar controller seems to be implemented differently when it comes to state restoration. This info is scattered in two different areas in Apple’s docs.

First clue is in the UITabBarController’s doc, under the State Restoration sections:

If not read carefully, this might give the impression things will be taken care of just like it was for a UINavigationController. However reading the section about State Restoration paints the whole picture:

[quote]Although UIKit helps restore the individual view controllers, it does not automatically restore the relationships between those view controllers. Instead, each view controller is responsible for encoding enough state information to return itself to its previous state. 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. Other view controllers that have embedded child view controllers are similarly responsible for encoding any information they need to restore their children later.
[/quote]

Most importantly, this follows:

It seems sunny4s approach may be the best, even though it may not strictly follow the steps of creating the child view controllers first and then the tab view controller. Or, if you want to be a stickler about it, you can do this…

In AppDelegate.m

[code]- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
// Create a new tab bar controller
UITabBarController *tbc = [[UITabBarController alloc] init];

BNRHypnosisViewController *hvc = [[BNRHypnosisViewController alloc] init];

NSBundle *appBundle = [NSBundle mainBundle];

// Look in the appBundle for the file BNRReminderViewController.xib
BNRReminderViewController *rvc = [[BNRReminderViewController alloc] initWithNibName:@"BNRReminderViewController" bundle:appBundle];


tbc.viewControllers = @[hvc, rvc];
// The last object in the path array is the restoration
// identifier for this view controller
tbc.restorationIdentifier = [identifierComponents lastObject];

// If there is only 1 identifier component, then
// this is the root view controller
if ([identifierComponents count] == 1) {
    self.window.rootViewController = tbc;
}

return tbc;

}
[/code]

…and in each of the child view controllers, do this…

In BNRHypnosisViewController.m

[code]+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
// First object in the array stores the root view controller, which is the tabBarController
UITabBarController *rootViewController = (UITabBarController *)[UIApplication sharedApplication].delegate.window.rootViewController;

return rootViewController.viewControllers[0];

}
[/code]

In BNRReminderViewController.m

[code]+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
// First object in the array stores the root view controller, which is the tabBarController
UITabBarController *rootViewController = (UITabBarController *)[UIApplication sharedApplication].delegate.window.rootViewController;

return rootViewController.viewControllers[1];

}[/code]


#5

Here is my solution

In BNRAppDelegate.m

[code]// called before state restoration begins.

  • (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    NSLog(@"%@", NSStringFromSelector(_cmd));
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    BNRHypnosisViewController *hvc = [[BNRHypnosisViewController alloc] init];
    BNRReminderViewController *rvc = [[BNRReminderViewController alloc] init];

    UITabBarController *rootViewController = [[UITabBarController alloc] init];

    // For state restoration
    rootViewController.restorationIdentifier = NSStringFromClass([rootViewController class]);
    rootViewController.viewControllers = @[hvc, rvc];
    self.window.rootViewController = rootViewController;
    self.window.backgroundColor = [UIColor whiteColor];
    return YES;
    }

// called after state restoration
// Yes, this method is pretty much empty

  • (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
    NSLog(@"%@", NSStringFromSelector(_cmd));

    [self.window makeKeyAndVisible];
    return YES;
    }

// Enable state restoration in my app.

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

  • (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder
    {
    return YES;
    }
    [/code]

As you can see above…that is all that is required to let iOS restore UITabBarController and the selected index.

Now to do state restoration of data within the 2 view controllers.

BNRHypnosisViewController.m

// class extenstion to declare protocol
@interface BNRHypnosisViewController () <UITextFieldDelegate>

@property (nonatomic, strong) UITextField *textField;
@property (nonatomic, strong) NSMutableArray *messages;

@end

@implementation BNRHypnosisViewController

// override the designated initializer
- (instancetype) initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    
    if(self)
    {
        // set the tab bar items title
        self.tabBarItem.title = @"Hypnotize";
        
        // Create UI Image from file
        // This will use Hypno@2x.png on retina display devices
        UIImage *image = [UIImage imageNamed:@"Hypno.png"];
        
        //put image on tab bar item.
        self.tabBarItem.image = image;
        
        //set the restoration identifier
        self.restorationIdentifier = NSStringFromClass([self class]);
// Restoration class not required.
    }
    
    return self;
}

- (BOOL) textFieldShouldReturn: (UITextField*) tf
{
    NSLog(@"Return key pressed - %@", tf.text);
    
// Keep track of all labels added, so we can restore later if required.
    [self.messages addObject:tf.text];

    [self drawHypnoticMessage:tf.text];
    tf.text = @"";
    [tf resignFirstResponder];
    return YES;
}

- (void) decodeRestorableStateWithCoder:(NSCoder *)coder
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    self.textField.text = [coder decodeObjectForKey:@"textfield.text"];
    self.messages = [[NSMutableArray alloc] initWithArray:[coder decodeObjectForKey:@"array.messages"]];
    
    for(NSString *msg in self.messages ) {
        [self drawHypnoticMessage:msg];
    }
    
    if(self.textField.text) {
        [self.textField becomeFirstResponder];
    } else {
        [self.textField resignFirstResponder];
    }
    
    
    [super decodeRestorableStateWithCoder:coder];
}

No changes to the BNRHypnosisViewController.h file. It does not need to implement the UIViewControllerRestoration protocol.

Finally, BNRReminderViewController.m

// class extention
@interface BNRReminderViewController ()

@property (nonatomic, weak) IBOutlet UIDatePicker *datePicker;

@end


@implementation BNRReminderViewController

// override the designated initializer
- (instancetype) initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    
    if(self)
    {
        // set the tab bar items title
        self.tabBarItem.title = @"Reminder";
        
        // Create UI Image from file
        // This will use Time@2x.png on retina display devices
        UIImage *image = [UIImage imageNamed:@"Time.png"];
        
        //put image on tab bar item.
        self.tabBarItem.image = image;
        
        // set the restoration identifier and class
        self.restorationIdentifier = NSStringFromClass([self class]);
// Restoration class not required.
    }
    
    return self;
}

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    [coder encodeObject:self.datePicker.date forKey:@"reminder.date"];
    
    // call the super class
    [super encodeRestorableStateWithCoder:coder];
}

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
    NSLog(@"%@", NSStringFromSelector(_cmd));
    self.datePicker.date = [coder decodeObjectForKey:@"reminder.date"];
    
    [super decodeRestorableStateWithCoder:coder];
}

Again, the BNRReminderViewController.h remains unchanged.

I have only included coding bits relevant to State Restoration.

I think is more cleaner solution than the one mentioned above. I learnt quite a bit about state restoration from an excellent tutorial online at the following website
aplus.rs/2013/state-restoration- … oryboards/

Hope that helps.


#6

@mataz137
Your solution is so clear and useful.
It helps me a lot.
Thanks!