cs193p – Assignment #5 Task #10

Please note, this blog entry is from a previous course. You might want to check out the current one.

Your application must work in both portrait and landscape orientations on both the iPhone and the iPad. Use appropriate platform-specific UI idioms (e.g. don’t let your iPad version look like a gigantic iPhone screen).

Remove everything from the iPad storyboard. Add a new split view controller, but remove all of the controllers but the split view controller itself. Go to the iPhone storyboard. Copy everything and paste it to the iPad story board. Remove both segues to the image view controller. Make the tab-bar view controller the master of the split view controller. Embed the image view controller inside a navigation view controller and make this one the detail view of the split view controller. Finally disable the animation of the activity indicator of the split view:

cs193p – assignment #5 task #10 - iPad storyboard
cs193p – assignment #5 task #10 – iPad storyboard


When a photo gets selected in the table view send that image to the image view controller:

- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    id detail = self.splitViewController.viewControllers[1];
    if ([detail isKindOfClass:[UINavigationController class]]) {
        detail = [((UINavigationController *)detail).viewControllers firstObject];
    }
    [self prepareImageVC:detail
                forPhoto:self.photos[indexPath.row]];
}

The code from task #8 which changed the zoom scale to show as much of the photo as possible while displaying no empty space is flawed in two ways. First, because the image stays on screen all time it is necessary to reset the zoom scale before adjusting the content size of the scroll view:

- (void)setImage:(UIImage *)image
{
    self.scrollView.zoomScale = 1.0;
    ...
}

Second, the height of the status bar contains in landscape mode its width, thus the code needs to be adjusted:

- (void)setZoomScaleToFillScreen
{
    ...
    double hScale = (self.scrollView.bounds.size.height
                     - self.navigationController.navigationBar.frame.size.height
                     - self.tabBarController.tabBar.frame.size.height
                     - MIN([UIApplication sharedApplication].statusBarFrame.size.height,
                           [UIApplication sharedApplication].statusBarFrame.size.width)
                     ) / self.imageView.image.size.height;
    ...
}

Finally, we need a button to reveal the master view in portrait mode. Make the image view controller the delegate for the split view:

@interface ImageVC () <..., UISplitViewControllerDelegate>
...
- (void)awakeFromNib
{
    self.splitViewController.delegate = self;
}

Hide the master view in portrait mode:

- (BOOL)splitViewController:(UISplitViewController *)svc
   shouldHideViewController:(UIViewController *)vc
              inOrientation:(UIInterfaceOrientation)orientation
{
    return UIInterfaceOrientationIsPortrait(orientation);
}

Create a button from the current title of the master view. Because the master view is inside a tab-view and a navigation view controller, search for the current view controller. (It might be necessary to set that title in storyboard, if you have not done so, yet):

- (void)splitViewController:(UISplitViewController *)svc
     willHideViewController:(UIViewController *)aViewController
          withBarButtonItem:(UIBarButtonItem *)barButtonItem
       forPopoverController:(UIPopoverController *)pc
{
    UIViewController *master = aViewController;
    if ([master isKindOfClass:[UITabBarController class]]) {
        master = ((UITabBarController *)master).selectedViewController;
    }
    if ([master isKindOfClass:[UINavigationController class]]) {
        master = ((UINavigationController *)master).topViewController;
    }
    barButtonItem.title = master.title;
    self.navigationItem.leftBarButtonItem = barButtonItem;
}

And remove that button again when in landscape mode:

- (void)splitViewController:(UISplitViewController *)svc
     willShowViewController:(UIViewController *)aViewController
  invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
{
    self.navigationItem.leftBarButtonItem = nil;
}

The table views rotate nicely, you might want to add constraints to keep the activity indicator centered …

The complete code is available on github.

FacebooktwitterredditpinterestlinkedintumblrmailFacebooktwitterredditpinterestlinkedintumblrmail

15 thoughts on “cs193p – Assignment #5 Task #10”

  1. Should the line
    barButtonItem.title = master.title;

    not be replaced by
    if (master) {
    barButtonItem.title = master.title;
    } else {
    barButtonItem.title = @”Top Places”;
    }
    since we do not have a master at all time (as seen in sources)

    1. The code you mention, is part of the splitViewController:… method. When this code gets called, there always should be a master. Of course, if you use it in other more general places, it is sensible to use introspection!

  2. Or maybe?

    if ([master isKindOfClass:[UITabBarController class]]) {

    if(((UITabBarController *) master).selectedViewController) {
    master = ((UITabBarController *) master).selectedViewController;
    } else {
    master = ((UITabBarController *) master).viewControllers[0];
    }
    }

  3. Works like a charm. However, I had no title for my back button when starting in portrait mode on iPad. This can be resolved by testing in WillHideViewController for (image == nil). If so, an alternate text can be added to the button.

    Also, the text of the back button is only updated when the iPad is rotated. Not when a new place has been selected. This means the text can be incorrect. I was able to update the button in setImage. However, that seems unsatisfactory as it means iPad specific code has to be added to setImage. I could not find an alternate method. Any ideas?

    1. ... just guessing … the bar button gets its title from the master … I could be wrong, but that should be a pointer and thus have the correct value when the master changes its title … so, when you rotate the iPad, you force the view to redraw and thus get the correct title … thus a setNeedsDisplay should suffice to redraw the view and the title of the button should update …

  4. That doesn’t work, because WillHideViewController is not called when SetNeedsDisplay is called. Thus, the title, which is set in that method, is not called. Nevertheless, adding the code to setImage seems not to cause problems on iPhone, so it’ll have to do.

  5. In iOS 8. One can use only one storyboard for both iPhone and iPad.
    There is a template called “master-detail” in the creating project window.

    I rewritten this assignment to adapt this change. The good thing is that I succeed in running it in iPad simulator without using “didSelectRowAtIndexPath” and “splitViewController” methods.

    But in iPhone mode, the left corner “Master” (back) button in ImageViewController (detail) is missing.

    I saw carefully the Xcode’s template code “master-detail”. And I can’t figure out the reason…

    I uploaded my code here
    hope someone could look at it.

    1. In iOS 8 you can use a single storyboard, that does not mean you have to. Actually if you want to support older versions you most likely want to use even more story boards.

      Nevertheless, did you check that the splitViewController from your app delegate equals the self.splitViewController from your photo TVC? Is that part of code actually accessed? Should you put the code to put the button back on screen in the image view controller?

      1. In Xcode template, the delegate of splitViewController is itself (splitViewController.delegate = itself in appDelegate). So I didn’t change it.

        I remove this delegation then, add it to the ImageViewController, it doesn’t work either in iPhone mode. But it doesn’t affect the functionality in iPad mode.

        I don’t understand the delegation mechanism. If I put self.splitViewController.delegate = self in ImageViewController. The latter will inform the splitViewController when it do some actions?

        I don’t know how to write the code to put the back button on the left-top corner. And another thing makes me confuse is that, in IOS 8, if we add a splitViewController, we can link the Master View’s tableVIewController with the Detail View’s NavigationViewController by segue action : “show detail”.

        So If it’s a segue mechanism, we don’t need to add the back button by hand I suppose.

  6. I test your code. In iPad mode, the back button doesn’t appear on the navigation bar.

    This is the part in your code to add the back button:

    I think you want to locate the “top places” tab or “recent” tab by:
    if ([master isKindOfClass:[UITabBarController class]]) {
    master = ((UITabBarController *)master).selectedViewController;
    }

    And then locate the TabViewController at the top front:
    if ([master isKindOfClass:[UINavigationController class]]) {
    master = ((UINavigationController *)master).topViewController;
    }
    barButtonItem.title = master.title;
    self.navigationItem.leftBarButtonItem = barButtonItem;

    But you just made thing complicated. In fact you could delete these:
    if ([master isKindOfClass:[UITabBarController class]]) {
    master = ((UITabBarController *)master).selectedViewController;
    }

    if ([master isKindOfClass:[UINavigationController class]]) {
    master = ((UINavigationController *)master).topViewController;
    }

    then add a title to the Master TabViewController

      1. I tried the lecture code, it works. Your code is not exactly that one.

        Because you added two IFs in willHideViewController. With these, the back button doesn’t appear.

        So I just delete them, then add a title to the TabBarViewController. That works!

Leave a Reply

Your email address will not be published.