Silver Challenge: Swapping the Master Button


#1

I’m having a hard time solving this challenge. My first approach to the solution was to check the device orientation and then decide if the barButtonItem must be shown, but none of my multiple approaches worked, so when I look for the solution I found the following code:

When showInfo: is called it calls transferBarButtonToViewController:

[code]- (void)showInfo:(id)sender
{
// Create the channel view controller
ChannelViewController *channelViewController = [[ChannelViewController alloc]
initWithStyle:UITableViewStyleGrouped];

if ([self splitViewController]) {
    [self transferBarButtonToViewController:channelViewController];
    UINavigationController *nvc = [[UINavigationController alloc]
                                   initWithRootViewController:channelViewController];
    
    // Create an array with our nav controller and this new VC's nav controller
    NSArray *vcs = [NSArray arrayWithObjects:[self navigationController], nvc, nil];
    
    // Grab a pointer to the split view controller
    // and reset its view controllers array
    [[self splitViewController] setViewControllers:vcs];
    
    // Make detail view controller the delegate of the split view controller
    [[self splitViewController] setDelegate:channelViewController];
    
    // If a row has been selected, deselect it so that a row
    // is not selected when viewing the info
    NSIndexPath *selectedRow = [[self tableView] indexPathForSelectedRow];
    
    if (selectedRow) {
        [[self tableView] deselectRowAtIndexPath:selectedRow
                                        animated:YES];
    }
} else {
    [[self navigationController] pushViewController:channelViewController
                                           animated:YES];
}

// Give the VD channel object through the protocol message
[channelViewController listViewController:self
                             handleObject:channel];

}
[/code]

The code of transferBarButtonToViewController: is

[code]- (void)transferBarButtonToViewController:(UIViewController *)vc
{
// Get the navigation controller in the detail spot of the view controller
UINavigationController *nvc = [[[self splitViewController] viewControllers] objectAtIndex:1];

// Get the root view controller out of the nav controller
UIViewController *currentVC = [[nvc viewControllers] objectAtIndex:0];

// If it's the same view controller, let's not do anything
if (vc == currentVC) {
    return;
}

// Get that view controller's navigation item
UINavigationItem *currentVCItem = [currentVC navigationItem];

// Tell new view controller to use left bar button item of current nav item
[[vc navigationItem] setLeftBarButtonItem:[currentVCItem leftBarButtonItem]];

// Remove the bar button item from the current view controller's nav item
[currentVCItem setLeftBarButtonItem:nil];

}[/code]

I’m trying to figure out how this code works to solve problem presented in the challenge, but I don’t understand what it does.

When I debug the code I never reach the next condition

// If it's the same view controller, let's not do anything if (vc == currentVC) { return; }

Anyone who help me giving a more detailed explanation of the code.


#2

That conditional statement runs if a webview is already showing when you choose an item from the list. Without that check, the List button would disappear if you tried to browse to another RSSItem link.


#3

Question about splitViewController:willHideViewController:withBarButtonItem:forPopoverController. This method gets called by WebViewController when the app first starts in portrait orientation. This is confusing because the documentation states that this method is called only when rotating from landscape to portrait. Is there a way to get ChannelViewController to also call this method at startup to create the left bar button?

Is this an issue with delegation? I’ve tried setting the delegate of the ChannelViewController in loadView with no success.

Is there a way to manually force ChannelViewController to call this method to enable the left bar button when its view appears?

I tried implementing the previous poster’s solution, but the left button gets permanantly stuck on ChannelViewController and permanantly deleted from WebViewController.

Am I on the wrong track and should I be focusing on the showInfo: method? If so, how do you create the left bar button from inside showInfo:?


#4

I was about to pull my hair out for the last portion of the first silver challenge but I finally figured it out. I was trying to remove the “List” button from the nav bar by trying to determine the device orientation, however when the device/simulator first started, the orientation was given as ‘0’ (i.e. device orientation unknown).

The solution was to place this code in the ListViewDelegateMethod, - (void)listViewController:(ListViewController *)lvc handleObject:(id)object, both in WebViewController and ChannelViewController.

if ([[UIDevice currentDevice] orientation] != 0 && [[UIDevice currentDevice] orientation] != 1 && [[UIDevice currentDevice] orientation] != 2) { [[self navigationItem] setLeftBarButtonItem:nil]; }

The above code basically made sure that if the orientation was either LandscapeLeft or LandscapeRight, the left bar button was removed from the navigation item.

I solved the first part of the silver challenge by creating a UIBartButtonItem property in both ChannelViewController and WebViewController, let’s call it ‘leftButton’, and inside the method

which is implemented inside WebViewController, I assigned the left button = barButtonItem, which is the argument passed through the implemented method. In essence preventing that button from getting destroyed by having a strong pointer pointing at it.

Then inside ChannelViewController, I added the following code:

    leftButton = webViewController.leftButton;
    [[self navigationItem] setLeftBarButtonItem:leftButton];

inside - (void)listViewController:(ListViewController *)lvc handleObject:(id)object. So, every time the ‘info’ button was being pressed, the previous method was being called and the ‘List’ left bar button was being added to the nab bar while in portrait mode.


#5

I realized that only one of WebViewController or ChannelViewController receiving the splitViewController:willHideViewController:… and splitViewController:willShowViewController:… messages and thus the other was unable to update its navigation bar. I figured that ListViewController was always around and thus could act to relay messages to the two detail controllers telling them to update their navigation bars. To implement this, I:

  • added a ChannelViewController * property in ListViewController.h and synthesized it in ListViewController.m analogous to the existing WebViewController * property
  • instantiate a ChannelViewController in NerdFeedAppDelegate.m and set it as ListViewController’s ChannelViewController *:

NerdFeedAppDelegate.m:

#import "ChannelViewController.h"
...
    ChannelViewController *cvc = [[ChannelViewController alloc] initWithStyle:UITableViewStyleGrouped];
    [lvc setChannelViewController:cvc];
  • remove the instantiation of a ChannelViewController from ListViewController.m - (void)showInfo:(id)sender method
    (I named the property channelViewController, the same name as the former local variable, and so didn’t require any other code change here)

  • changed the delegate of UISplitViewController from the WebViewController and ChannelViewController instances in one place in NerdfeedAppDelegate.m ("…setDelegate:lvc…") and in two places in ListViewController ("…setDelegate:self…")

removed the UISplitViewControllerDelegate methods from WebViewController.m and ChannelViewController.m and added replaced them with methods in ListViewController.m

ListViewController.m:

- (void)splitViewController:(UISplitViewController *)svc willHideViewController:(UIViewController *)aViewController
          withBarButtonItem:(UIBarButtonItem *)bbi
       forPopoverController:(UIPopoverController *)pc
{
    NSLog(@"ListViewController got willHideViewController");
    listBarButtonItem = bbi;
    [listBarButtonItem setTitle:@"List"];
    [webViewController listViewController:self showBarButtonItem:listBarButtonItem];
    [channelViewController listViewController:self showBarButtonItem:listBarButtonItem];
}

- (void)splitViewController:(UISplitViewController *)svc willShowViewController:(UIViewController *)aViewController
  invalidatingBarButtonItem:(UIBarButtonItem *)bbi
{
    NSLog(@"ListViewController got willShowViewController");
    if (bbi == listBarButtonItem) {
        listBarButtonItem = nil;
        [webViewController listViewController:self showBarButtonItem:listBarButtonItem];
        [channelViewController listViewController:self showBarButtonItem:listBarButtonItem];
    }
}

listBarButtonItem is a newly added ivar

The UISplitViewControllerDelegate methods implemented in ListViewController call a method I’ve newly added to the ListViewControllerDelegate Protocol

ListViewController.h:

- (void)listViewController:(ListViewController *)lvc showBarButtonItem:(UIBarButtonItem *)bbi;

And implemented in both WebViewController.m and ChannelViewController.m:

- (void)listViewController:(ListViewController *)lvc showBarButtonItem:(UIBarButtonItem *)bbi { [[self navigationItem] setLeftBarButtonItem:bbi]; }

This approach works, and feels reasonably clean to me … any and all feedback on this approach would be greatly appreciated! Thanks,

Dan


#6

My approach was similar to dmddmd’s. Both the WebViewController and the ChannelViewController were instantiated in the App Delegate. Instances were sent to the ListViewController as properties.

I then set the ListViewController as the delegate of the UISplitViewController. This was also done in the App Delegate.

ListViewController implemented UISplitViewControllerDelegate methods as such:

- (void)splitViewController:(UISplitViewController *)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)pc { [barButtonItem setTitle:@"List"]; [[webViewController navigationItem] setLeftBarButtonItem:barButtonItem]; [[channelViewController navigationItem] setLeftBarButtonItem:barButtonItem]; }

- (void)splitViewController:(UISplitViewController *)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { [[webViewController navigationItem] setLeftBarButtonItem:nil]; [[channelViewController navigationItem] setLeftBarButtonItem:nil]; }

In other words, I made ListViewController responsible for setting and unsetting the UIBarButtonItem.


#7

@dmddmd & andre3k1

Sorry guys, I tried to implement each one of your solutions, none of them seems to work correctly.
The ListViewController is indeed the new splitViewController delegate, so the app works as previously, but there is still no “List’” button in the channel view navigation bar (unless we rotate the iPad).

I liked the idea of having the ListViewController be the delegate of the splitViewController, so it manages the LeftBarButtonItem (apparently, Apple proceeds in the same way with its MultipleDetailViews example) but something is still missing.
Maybe something having to do with the navController being reset all the time ?

If someone can give more details about his actually working solution, I’d be grateful …

Thanks


#8

Please note there seems to be some bugginess in the Simulator when it comes to detecting orientation programmatically:

SO: http://stackoverflow.com/search?q=%5Bios-simulator%5D+orientation
Google: https://www.google.com/webhp?q=ios%20simulator%20orientation


#9

@Joe Conway

Hi Joe,
Could you please give us a hint about this ? I’ve been struggling with this one for more than a month now, and I still can’t find the right answer to this challenge …
I’ve been studying Apple’s example (MultipleDetailViews), but in their case, they don’t use a navigationController for the detail views, which enables them to change the detail view controller without resetting the navigationController …

Please ? Pretty please ? Pretty pretty please ? (hail to all the Monkey Island fans :slight_smile:
Fred


#10

Solved this with one line of code in the ListViewController.m showInfo method that passes the webViewController’s leftBarButtonItem to the channelViewController’s leftBarButtonItem every time “Info” is pressed (i.e. everytime showInfo is called):

- (void)showInfo:(id)sender
{
    // Create the channel view controller
    ChannelViewController *channelViewController = [[ChannelViewController alloc] initWithStyle:UITableViewStyleGrouped];
    
    // *** This is the only line that it took to solve Silver Challenge: Swapping the Master Button ***
    channelViewController.navigationItem.leftBarButtonItem = webViewController.navigationItem.leftBarButtonItem;
    
    if ([self splitViewController]) {
        UINavigationController *nvc = [[UINavigationController alloc] initWithRootViewController:channelViewController];
        
        // Create an array with our nav controller and this new VC's nav controller
        NSArray *vcs = [NSArray arrayWithObjects:[self navigationController],nvc, nil];
        
        // Grab a pointer to the list view controller
        // and reset its view controllers array.
        [[self splitViewController] setViewControllers:vcs];
        
        // Make detail view controller the delegate of the split view controller
        [[self splitViewController] setDelegate:channelViewController];
        
        // If a row has been selected, deselect it so that a row
        // is not selected when viewing the info
        NSIndexPath *selectedRow = [[self tableView] indexPathForSelectedRow];
        
        if (selectedRow)
            [[self tableView] deselectRowAtIndexPath:selectedRow animated:YES];
        
    } else {
        [[self navigationController] pushViewController:channelViewController
                                               animated:YES];
    }

    // Give the VC the channel object through the protocol message
    [channelViewController listViewController:self
                                 handleObject:channel];
}

#11

Not so easy …

Try this :

  • start in portrait mode
  • switch to landscape
  • click on info
  • switch back to portrait
  • click on List
  • click on info

The List button has disappeared.

Still, it’s a good idea … I had completely given up this problem, I think I’ll have another look.

Thanks for sharing !


#12

Are you using a single view controller for both orientations?

If so, things can become complicated; better to use different view controllers for different orientations.

See: Creating an Alternate Landscape Interface under the topic Responding to Device Orientation Changes in View Controller Programming Guide for iOS.


#13

What seemed to work for me was storing the button in object that stayed around. I’ve tried 2 solutions and both did work.

  1. Store the button in the ListViewController
  2. Overload the UISplitViewController and store the button here.

In both scenario’s the button must be retrieved when a new details controller is shown and the stored button must nil’ed when rotating back to landscape.


#14

Building on what pschluet said, I made channelViewController a property of the ListViewController like the web view already is and synthesized it. Then I changed the beginning of showInfo: before the if ([self splitViewController]) to

if (!channelViewController) {
        channelViewController =
            [[ChannelViewController alloc] initWithStyle:UITableViewStyleGrouped];
    }
    
    [[channelViewController navigationItem]
        setLeftBarButtonItem:[[webViewController navigationItem] leftBarButtonItem]];

Then I also added

- (void)tableView:(UITableView *)tableView
    didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    ...
    // this is what I added
    [[webViewController navigationItem]
         setLeftBarButtonItem:[[channelViewController navigationItem] leftBarButtonItem]];
    
    [webViewController listViewController:self handleObject:entry];
}

Works for me…


#15

@aledvina

This is brilliant !!
Thanks a lot for sharing !


#16

It turns out there was a subtle bug in my previous response which I fixed via changing viewDidLoad and adding viewDidAppear as follows

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    UINib *nib = [UINib nibWithNibName:@"NerdfeedItemCell" bundle:nil];
    
    [[self tableView] registerNib:nib
           forCellReuseIdentifier:@"NerdfeedItemCell"];
    
    [[self tableView] setDelegate:self];
    
    if (!channelViewController) {
        channelViewController =
        [[ChannelViewController alloc] initWithStyle:UITableViewStyleGrouped];
    }
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear];
    
    if ([[UIDevice currentDevice] orientation] != UIDeviceOrientationLandscapeLeft &&
        [[UIDevice currentDevice] orientation] != UIDeviceOrientationLandscapeRight &&
        [[webViewController navigationItem] leftBarButtonItem])
        [[channelViewController navigationItem]
            setLeftBarButtonItem:[[webViewController navigationItem] leftBarButtonItem]];
}

I then took the channelViewController initialization out of showInfo:. Everything else is the same. The bug showed up depending on the order in which you navigated the app. It looked like it worked in most cases but there was one sequence where it didn’t.


#17

I couldn’t solve this without looking at the solution included in the sample code for the book. The Silver Challenge solution is included in the sample code for Chapter 26, implemented as a category on ListViewController:

[code]
@interface ListViewController()

  • (void)transferBarButtonToViewController:(UIViewController *)vc;
    @end[/code]

Honestly, the implementation hurt my head a bit. It involves grabbing the SplitViewController and then digging into the Detail View, and then getting the UIBarButtonItem from that and transferring it to the UINavigationItem of the new DetailView.

I coded it up a different way, using notifications. Basically, I have a notification named BNRBarButtonItemUpdatedNotification. ListViewController registers to observe that notification and gets the UIBarButtonItem sent along with it, which it stores in an instance variable. (Sorry, maybe that hurts your head!) Anyway, I’m producing part of that implementation below. It begins with a line added to the two methods from the UISplitViewControllerDelegate protocol, and one more method that I add.

[code]

  • (void)splitViewController:(UISplitViewController *)svc
    willHideViewController:(UIViewController *)aViewController
    withBarButtonItem:(UIBarButtonItem *)barButtonItem
    forPopoverController:(UIPopoverController *)pc
    {
    [barButtonItem setTitle:@“List”];
    [[self navigationItem] setLeftBarButtonItem:barButtonItem];

    // Send the notification, with the button
    [self notifyObserversWithUpdatedBarButtonItem:barButtonItem];
    }

  • (void)splitViewController:(UISplitViewController *)svc
    willShowViewController:(UIViewController *)aViewController
    invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
    {
    // Remove popup when rotating to landscape view
    if (barButtonItem == [[self navigationItem] leftBarButtonItem]) {
    [[self navigationItem] setLeftBarButtonItem:nil];

      // Again, send the notification with the (now nil) button
      [self notifyObserversWithUpdatedBarButtonItem:nil];
    

    }
    }

// Here’s the method I add

  • (void)notifyObserversWithUpdatedBarButtonItem:(UIBarButtonItem *)bbi
    {
    NSNotification *note =
    [NSNotification notificationWithName:@"BNRBarButtonItemUpdatedNotification"
    object:bbi];
    [[NSNotificationCenter defaultCenter] postNotification:note];
    }[/code]

I register ListViewController to observe this notification in its initWithStyle: method, and then create a method to handle the notification and an instance variable (listBarButtonItem) to hold the button passed along with the notification:

- (void)barButtonItemUpdated:(NSNotification *)note { listBarButtonItem = [note object]; }

Then, just like in the sample code, I pass the button along to the newly created detail views. This is done in two places in ListViewController: the showInfo: method and tableView:didSelectRowAtIndexPath: method.

Like I said, maybe it seems crazy to you, but it made sense to me.


#18

Thanks for MarioDiana showing that the sample code has the answer.

I’ve read through the code and now understand how to solve this challenge.

The sample code’s way may not be the only way, but it must be the right way. And to me it is pretty straight forward.

In ListViewController.m you declare a class extension, which looks like a category but is not. The code looks like this:

[code]@interface ListViewController ()

  • (void)transferBarButtonToViewController:(UIViewController *)vc;
    @end[/code]

The method transferBarButtonToViewController:(UIViewController *)vc is the key change here. This method takes the barbuttonitem out of current detail view controller and puts it on the view controller that is going to be displayed. If the two view controllers are the same one, it does nothing. Since when the Nerdfeed app is launched the barbuttonitem on the left already exists, this first view controller always has a barbuttonitem called “List”.

In the implementation section implement the method. Here is the code for the method:

[code]- (void)transferBarButtonToViewController:(UIViewController *)vc
{
// Get the navigation controller in the detail spot of the split view controller
UINavigationController *nvc = [[[self splitViewController] viewControllers]
objectAtIndex:1];

// Get the root view controller out of that nav controller
UIViewController *currentVC = [[nvc viewControllers] objectAtIndex:0];

// If it's the same view controller, let's not do anything
if (vc == currentVC)
    return;

// Get that view controller's navigation item
UINavigationItem *currentVCItem = [currentVC navigationItem];

// Tell new view controller to use left bar button item of current nav item
[[vc navigationItem] setLeftBarButtonItem:[currentVCItem leftBarButtonItem]];

// Remove the bar button item from the current view controller's nav item
[currentVCItem setLeftBarButtonItem:nil];

}[/code]

Then go to where you would like the barbuttonitem to be showed, for example, in ShowInfo method of ListViewController.m

if ([self splitViewController]) { [b][self transferBarButtonToViewController:channelViewController];[/b] ... }

Likewise, in (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath of ListViewController.m
add the same line to execute the method:

[code]if (![self splitViewController])
[[self navigationController] pushViewController:webViewController animated:YES];
else {
[self transferBarButtonToViewController:webViewController];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:webViewController];

    NSArray *vcs = [NSArray arrayWithObjects:[self navigationController], nav, nil];
    
    [[self splitViewController] setViewControllers:vcs];
    
    // Make the detail view controller the delegate of the split view controller
    [[self splitViewController] setDelegate:webViewController];
}[/code]

Voila! No problemento anymore!


#19

Here’s a quick way to get this working. Not sure how good it is from a programming perspective.

You can call the split view controller delegate method on the channelViewController to set up the button for you in the showInfo: method. Just send it a reference to the current bar button item. If the bar button item is currently nil (landscape) it won’t set anything.

- (void)showInfo:(id)sender
{
		//...
	if ([self splitViewController]) {
		//...
		// Make sure the button appears when changing view controllers
		[channelViewController splitViewController:[self splitViewController]
							willHideViewController:[self webViewController]
								 withBarButtonItem:[[[self webViewController] navigationItem] leftBarButtonItem]
							  forPopoverController:nil];
		// ...
}

#20

Elegant! I like one line solutions, and this one builds on the Apple implementation expected by the delegate methods willShow and willHide and simply passes the current state from the current detail view to the new view we’re allocating. The only thing I’d add (because I’m trying to practice the message method instead of dot notation) is to implement it like this;


	// Make sure that our new channelViewController has the same left button as the WebViewController
	[[channelViewController navigationItem] setLeftBarButtonItem:[[webViewController navigationItem]leftBarButtonItem]];