Autorotation, Popover Controllers, and Modal View Controllers - iOS Programming: The Big Nerd Ranch Guide (2014)

iOS Programming: The Big Nerd Ranch Guide (2014)

17. Autorotation, Popover Controllers, and Modal View Controllers

In the last two chapters, you used Auto Layout to ensure that Homepwner maintains a standard appearance relative to the device’s screen size. For instance, you made sure that the toolbar is always at the bottom and as wide as the screen.

When designing a universal application, you often need the application to behave differently depending on the type of device being used. The different devices have different idioms that users expect, so identical behavior or features on every device would feel foreign at times.

In this chapter, you are going to make four changes to Homepwner’s behavior that will tailor the app’s behavior to whatever device it is running on.

· On iPads only, allow the interface to rotate when the device is upside down.

· On iPads only, show the image picker in a popover controller when the user presses the camera button.

· On iPads only, present the detail interface modally when the user creates a new item.

· On iPhones only, disable the camera button in the detail interface when the device is in landscape orientation.

Thus, you are going to learn how to test for the type of the device and write device-specific code. You will also learn about rotation, popover controllers, and more about modal view controllers.

Autorotation

There are two distinct orientations in iOS: device orientation and interface orientation.

The device orientation represents the physical orientation of the device, whether it is right-side up, upside down, rotated left, rotated right, on its face, or on its back. You can access the device orientation through the UIDevice class’s orientation property.

The interface orientation, by contrast, is a property of the running application. The following list shows all of the possible interface orientations:

UIInterfaceOrientationPortrait

The Home button is below the screen.

UIInterfaceOrientationPortraitUpsideDown

The Home button is above the screen.

UIInterfaceOrientationLandscapeLeft

The device is on its side and the Home button is to the right of the screen.

UIInterfaceOrientationLandscapeRight

The device is on its side and the Home button is to the left of the screen.

When the application’s interface orientation changes, the size of the window for the application also changes. The window will take on its new size and will rotate its view hierarchy. The views in the hierarchy will lay themselves out again according to their constraints.

Open Homepwner.xcodeproj and build the application on the iPad simulator.

While Homepwner is running on the simulator, you can simulate a rotation. Navigate to the BNRDetailViewController. From the simulator’s Hardware menu, select Rotate Left option. The simulator window will rotate, causing your “device” to rotate its window and contents. Because you have configured your constraints properly, the interface looks great in all orientations.

When the device orientation changes, your application is informed about the new orientation. Your application can decide whether to allow its interface orientation to match the new orientation of the device.

Rotate to the left again to put the application in portrait upside-down orientation. This time, the interface does not rotate. Even though the device orientation changed, the interface orientation stayed the same because Homepwner does not allow its interface orientation to be set toUIInterfaceOrientationPortraitUpsideDown. You can change which interface orientations an application supports in the same editor where you universalized Homepwner.

In the Target information’s General tab, look at the Device Orientations section for iPad. Notice that the portrait and the two landscape options are selected, but Upside Down is not. Click on the Upside Down button to toggle it on (Figure 17.1).

Figure 17.1 Let Homepwner be launched upside down

Let Homepwner be launched upside down

Build and run the application on the iPad simulator and rotate the device in the same direction twice using the Hardware menu. Now, the interface will rotate in all directions. It is typical that an iPad application can rotate to all four orientations while an iPhone application can rotate in any orientation other than upside down. Note that the section for iPhone / iPod Deployment Info maintains that this device can only rotate to portrait and the landscape orientations while on the iPhone.

Some applications will want to lock the user to a specific orientation. For example, many games only allow the two landscape orientations, and many iPhone applications will only allow portrait. Toggling these buttons will allow you to choose which orientations are valid for your application.

In addition to the application choosing which interface orientations are acceptable, the view controller that is occupying the screen also gets a say. (In Homepwner, the UINavigationController occupies the screen, except for when the BNRDetailViewController is presented modally.) Each view controller implements a method that returns all of the interface orientations it supports. For the interface orientation to change, both the rootViewController of the application and the application itself (per the Supported Interface Orientations section of the info property list) must agree that the new orientation is OK.

By default, a view controller running on the iPad will allow all orientations. A view controller on the iPhone application will allow all but the upside-down orientation. If you would like to change this, you must override supportedInterfaceOrientations in that view controller. The default implementation of this method looks like this:

- (NSUInteger)supportedInterfaceOrientations

{

if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {

return UIInterfaceOrientationMaskAll;

} else {

return UIInterfaceOrientationMaskAllButUpsideDown;

}

}

This code checks to see whether the application is running on an iPad or an iPhone. You get the current device and then test its userInterfaceIdiom property. The two possible values (as of this writing) are UIUserInterfaceIdiomPhone and UIUserInterfaceIdiomPad.

If, for some reason, your root view controller wanted to appear in landscape left or landscape right only, it could implement the method as follows:

- (NSUInteger)supportedInterfaceOrientations

{

// On all devices, return left and right

return UIInterfaceOrientationMaskLandscapeLeft

| UIInterfaceOrientationMaskLandscapeRight;

}

(If the bitwise-OR (|) operator is unfamiliar to you, check out the section called “For the More Curious: Bitmasks” at the end of this chapter.)

In many apps, the screen is occupied by a UINavigationController or a UITabViewController. UINavigationController uses the supportedInterfaceOrientations inherited from UIViewController. If you want the view controller being displayed by the UINavigationController to determine the autorotation mask, subclass UINavigationController and override it:

@implementation MyNavigationController

- (NSUInteger)supportedInterfaceOrientations

{

return self.topViewController.supportedInterfaceOrientations;

}

@end

UITabViewController asks the view controller for each of its tabs for its supported interface orientations and returns the intersection: That is, the UITabViewController only supports an orientation if all its tabs support it.

Rotation Notification

There will be times when you want to do something special in a view controller when the device orientation changes. In Homepwner, one issue is that on an iPhone, the UIImageView on the BNRDetailViewController becomes too small in landscape orientation. It would make more sense to limit the user to taking and viewing the picture in portrait orientation. To make this happen, you need to hide the image view and disable the camera button when the application is in landscape orientation.

First, you need a pointer to the camera button so that you can send it a message to disable it. Navigate to BNRDetailViewController.m. Then, Option-Click on BNRDetailViewController.xib to open it in the assistant editor.

Now, Control-drag from the camera button on the toolbar to the class extension area of BNRDetailViewController.m to create a weak property outlet named cameraButton. This will create and connect a new property:

@property (weak, nonatomic) IBOutlet UIBarButtonItem *cameraButton;

Now back to your goal: hiding the image view and disabling the camera button only in landscape and only if the device is an iPhone.

When writing code to respond to a change in orientation, you override the UIViewController method willAnimateRotationToInterfaceOrientation:duration:. The message willAnimateRotationToInterfaceOrientation:duration: is sent to a view controller when the interface orientation successfully changes. The new interface orientation value is in the first argument to this method.

In BNRDetailViewController.m, create a new method called prepareViewsForOrientation: to check for the device and then check the interface orientation. If the device is an iPhone and the new orientation is landscape, hide the image view and disable the button. Call this method when the view first comes on screen and again whenever the orientation changes:

- (void)prepareViewsForOrientation:(UIInterfaceOrientation)orientation

{

// Is it an iPad? No preparation necessary

if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {

return;

}

// Is it landscape?

if (UIInterfaceOrientationIsLandscape(orientation)) {

self.imageView.hidden = YES;

self.cameraButton.enabled = NO;

} else {

self.imageView.hidden = NO;

self.cameraButton.enabled = YES;

}

}

- (void)willAnimateRotationToInterfaceOrientation:

(UIInterfaceOrientation)toInterfaceOrientation

duration:(NSTimeInterval)duration

{

[self prepareViewsForOrientation:toInterfaceOrientation];

}

- (void)viewWillAppear:(BOOL)animated

{

[super viewWillAppear:animated];

UIInterfaceOrientation io =

[[UIApplication sharedApplication] statusBarOrientation];

[self prepareViewsForOrientation:io];

...

Build and run the application on the iPhone simulator. On the BNRDetailViewController, add an image to the BNRItem and then rotate to landscape. The image will disappear and the camera button will be grayed out. Upon rotating to portrait again, the image will reappear and the camera button will be enabled again. If you build and run on the iPad, the image view and camera button will always be available and enabled.

When you write code that changes something about a view (like its frame or whether it is hidden) in this method, those changes are animated. The duration argument tells you how long that animation will take. If you are doing some other work on rotation that does not involve views, or you just do not want the views to animate their changes, you can override the willRotateToInterfaceOrientation:duration: method in your view controller. This method gives you the same information at the same time, but your views are not automatically animated.

Additionally, if you want to do something after the rotation is completed, you can override didRotateFromInterfaceOrientation: in your view controller. This method’s argument is the previous interface orientation before the rotation occurred. You can always ask a view controller for its current orientation by sending it the message interfaceOrientation.

You have now written some iPhone-specific code. In the next section, you will add another device-specific feature – showing the image picker in a popover when running on an iPad.

UIPopoverController

With iPad applications, you have a lot more screen space to work with. Let’s take advantage of this by presenting the UIImagePickerController in a UIPopoverController when the user taps the camera button in the detail interface.

A popover controller displays another view controller’s view in a bordered window that floats above the rest of the application’s interface. It is only available on iPads. When you create a UIPopoverController, you set this other view controller as the popover controller’s contentViewController. Popover controllers are useful when giving the user a list of choices (like picking a photo out of the photo library) or some extra information about something that is summarized on the screen. For example, a form may have some buttons next to some of the fields. Tapping on the button would reveal a popover whose contentViewController explains the intended use of that field.

In this section, you will present the UIImagePickerController in a UIPopoverController when the user taps the camera bar button item in the BNRDetailViewController’s view (Figure 17.2).

Figure 17.2 UIPopoverController

UIPopoverController

In the class extension in BNRDetailViewController.m, declare that BNRDetailViewController conforms to the UIPopoverControllerDelegate protocol.

@interface BNRDetailViewController ()

<UINavigationControllerDelegate, UIImagePickerControllerDelegate,

UITextFieldDelegate, UIPopoverControllerDelegate>

Additionally, add a property to hold the popover controller.

@interface BNRDetailViewController ()

<UINavigationControllerDelegate, UIImagePickerControllerDelegate,

UITextFieldDelegate, UIPopoverControllerDelegate>

@property (strong, nonatomic) UIPopoverController *imagePickerPopover;

@property (weak, nonatomic) IBOutlet UITextField *nameField;

In BNRDetailViewController.m, add the following code to the end of takePicture:.

imagePicker.delegate = self;

[self presentViewController:imagePicker animated:YES completion:nil];

// Place image picker on the screen

// Check for iPad device before instantiating the popover controller

if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {

// Create a new popover controller that will display the imagePicker

self.imagePickerPopover = [[UIPopoverController alloc]

initWithContentViewController:imagePicker];

self.imagePickerPopover.delegate = self;

// Display the popover controller; sender

// is the camera bar button item

[self.imagePickerPopover

presentPopoverFromBarButtonItem:sender

permittedArrowDirections:UIPopoverArrowDirectionAny

animated:YES];

} else {

[self presentViewController:imagePicker animated:YES completion:nil];

}

}

Notice that you check the device before creating the UIPopoverController. It is critical to do this. You can only instantiate popover controllers on the iPad family of devices, and trying to create one on an iPhone will throw an exception.

Build and run the application on the iPad simulator or on an iPad. Navigate to the BNRDetailViewController and tap the camera icon. The popover will appear and show the UIImagePickerController’s view.

Dismiss the popover by tapping anywhere on the screen. When a popover is dismissed in this way, it sends the message popoverControllerDidDismissPopover: to its delegate.

In BNRDetailViewController.m, implement popoverControllerDidDismissPopover: to set imagePickerPopover to nil to destroy the popover. You will create a new popover each time the camera button is tapped.

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController

{

NSLog(@"User dismissed popover");

self.imagePickerPopover = nil;

}

The popover should also be dismissed when you select an image from the image picker. In BNRDetailViewController.m, at the end of imagePickerController:didFinishPickingMediaWithInfo:, dismiss the popover when an image is selected.

self.imageView.image = image;

[self dismissViewControllerAnimated:YES completion:nil];

// Do I have a popover?

if (self.imagePickerPopover) {

// Dismiss it

[self.imagePickerPopover dismissPopoverAnimated:YES];

self.imagePickerPopover = nil;

} else {

// Dismiss the modal image picker

[self dismissViewControllerAnimated:YES completion:nil];

}

}

When you explicitly send the message dismissPopoverAnimated: to dismiss the popover controller, it does not send popoverControllerDidDismissPopover: to its delegate, so you must set imagePickerPopover to nil in dismissPopoverAnimated: after explicitly dismissing the popover.

There is a small issue with this code. If the UIPopoverController is visible and the user taps on the camera button again, the application will crash. This crash occurs because the UIPopoverController that is on the screen is destroyed when imagePickerPopover is set to point at the newUIPopoverController in takePicture:. You can ensure that the destroyed UIPopoverController is not visible and cannot be tapped by adding the following code to the top of takePicture: in BNRDetailViewController.m.

- (IBAction)takePicture:(id)sender

{

if ([self.imagePickerPopover isPopoverVisible]) {

// If the popover is already up, get rid of it

[self.imagePickerPopover dismissPopoverAnimated:YES];

self.imagePickerPopover = nil;

return;

}

UIImagePickerController *imagePicker =

[[UIImagePickerController alloc] init];

Build and run the application. Tap the camera button to show the popover and then tap it again – the popover will disappear.

More Modal View Controllers

In this part of the chapter, you will update Homepwner to present the BNRDetailViewController modally when the user creates a new BNRItem (Figure 17.3). When the user selects an existing BNRItem, the BNRDetailViewController will be pushed onto the UINavigationController’s stack as before.

Figure 17.3 New item

New item

To implement this dual usage of BNRDetailViewController, you will give it a new designated initializer, initForNewItem:. This initializer will check whether the instance is being used to create a new BNRItem or to show an existing one. Then it will configure the interface accordingly.

In BNRDetailViewController.h, declare this initializer.

- (instancetype)initForNewItem:(BOOL)isNew;

@property (nonatomic, strong) BNRItem *item;

If the BNRDetailViewController is being used to create a new BNRItem, you want it to show a Done button and a Cancel button on its navigation item. Implement this method in BNRDetailViewController.m.

- (instancetype)initForNewItem:(BOOL)isNew

{

self = [super initWithNibName:nil bundle:nil];

if (self) {

if (isNew) {

UIBarButtonItem *doneItem = [[UIBarButtonItem alloc]

initWithBarButtonSystemItem:UIBarButtonSystemItemDone

target:self

action:@selector(save:)];

self.navigationItem.rightBarButtonItem = doneItem;

UIBarButtonItem *cancelItem = [[UIBarButtonItem alloc]

initWithBarButtonSystemItem:UIBarButtonSystemItemCancel

target:self

action:@selector(cancel:)];

self.navigationItem.leftBarButtonItem = cancelItem;

}

}

return self;

}

In the past, when you have changed the designated initializer of a class from its superclass’s designated initializer, you have overridden the superclass’s initializer to call the new one. In this case, you are just going to make it illegal to use the superclass’s designated initializer by throwing an exception when anyone calls it.

In BNRDetailViewController.m, override UIViewController’s designated initializer.

- (instancetype)initWithNibName:(NSString *)nibNameOrNil

bundle:(NSBundle *)nibBundleOrNil

{

@throw [NSException exceptionWithName:@"Wrong initializer"

reason:@"Use initForNewItem:"

userInfo:nil];

return nil;

}

This code creates an instance of NSException with a name and a reason and then throws the exception. This halts the application and shows the exception in the console.

To confirm that this exception will be thrown, let’s return to where this initWithNibName:bundle: method is currently called – the tableView:didSelectRowAtIndexPath: method of BNRItemsViewController. In this method, BNRItemsViewController creates an instance of BNRDetailViewController and sends it the message init, which eventually calls initWithNibName:bundle:. Therefore, selecting a row in the table view will result in the “Wrong initializer” exception being thrown.

Build and run the application. (You will get warnings that save: and cancel: are not implemented. Ignore them for now.) Tap a row. Your application will halt, and you will see an exception in the console. Notice that the name and the reason are part of the console message.

You do not want to see this exception again, so in BNRItemsViewController.m, update tableView:didSelectRowAtIndexPath: to use the new initializer.

- (void)tableView:(UITableView *)tableView

didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

BNRDetailViewController *detailViewController =

[[BNRDetailViewController alloc] init];

BNRDetailViewController *detailViewController =

[[BNRDetailViewController alloc] initForNewItem:NO];

NSArray *items = [[BNRItemStore sharedStore] allItems];

Build and run the application again. Nothing new and exciting will happen, but your application will no longer crash when you select a row in the table.

Now that you have your new initializer in place, let’s change what happens when the user adds a new item.

In BNRItemsViewController.m, edit the addNewItem: method to create an instance of BNRDetailViewController in a UINavigationController and present the navigation controller modally.

- (IBAction)addNewItem:(id)sender

{

BNRItem *newItem = [[BNRItemStore sharedStore] createItem];

NSInteger lastRow = [[[BNRItemStore sharedStore] allItems] indexOfObject:newItem];

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:lastRow inSection:0];

[self.tableView insertRowsAtIndexPaths:@[indexPath]

withRowAnimation:UITableViewRowAnimationTop];

BNRDetailViewController *detailViewController =

[[BNRDetailViewController alloc] initForNewItem:YES];

detailViewController.item = newItem;

UINavigationController *navController = [[UINavigationController alloc]

initWithRootViewController:detailViewController];

[self presentViewController:navController animated:YES completion:nil];

}

Build and run the application and tap the New button to create a new item. An instance of BNRDetailViewController will slide up from the bottom of the screen with a Done button and a Cancel button on its navigation item. (Tapping these buttons, of course, will throw an exception, since you have not implemented the action methods yet.)

Notice that you are creating an instance of UINavigationController that will never be used for navigation. This gives this view the same title bar across the top that every other view has. It also gives you a place to put the Done and Cancel buttons.

Dismissing modal view controllers

To dismiss a modally-presented view controller, you must send the message dismissViewControllerAnimated:completion: to the view controller that presented it. You have done this before with UIImagePickerController – the BNRDetailViewController presented it, and when the image picker told theBNRDetailViewController it was done, the BNRDetailViewController dismissed it.

Now, you have a slightly different situation. When a new item is created, the BNRItemsViewController presents the BNRDetailViewController modally. The BNRDetailViewController has two buttons on its navigationItem that will dismiss it when tapped: Cancel and Done. There is a problem here: the action messages for these buttons are sent to the BNRDetailViewController, but it is the responsibility of the BNRItemsViewController to do the dismissing. The BNRDetailViewController needs a way to tell the view controller that presented it, “Hey, I’m done, you can dismiss me now.”

Fortunately, every UIViewController has a presentingViewController property that points to the view controller that presented it. The BNRDetailViewController will grab a pointer to its presentingViewController and send it the message dismissViewControllerAnimated:completion:.

In BNRDetailViewController.m, implement the action method for the Done button.

- (void)save:(id)sender

{

[self.presentingViewController dismissViewControllerAnimated:YES

completion:nil];

}

The Cancel button has a little bit more going on. When the user taps the button on the BNRItemsViewController to add a new item to the list, a new instance of BNRItem is created and added to the store, and then the BNRDetailViewController slides up to edit this new item. If the user cancels the item’s creation, then that BNRItem needs to be removed from the store.

At the top of BNRDetailViewController.m, import the header for BNRItemStore.

#import "BNRDetailViewController.h"

#import "BNRItem.h"

#import "BNRImageStore.h"

#import "BNRItemStore.h"

@implementation BNRDetailViewController

Now implement the action method for the Cancel button in BNRDetailViewController.m.

- (void)cancel:(id)sender

{

// If the user cancelled, then remove the BNRItem from the store

[[BNRItemStore sharedStore] removeItem:self.item];

[self.presentingViewController dismissViewControllerAnimated:YES

completion:nil];

}

Build and run the application. Create a new item and tap the Cancel button. The instance of BNRDetailViewController will slide off the screen, and nothing will be added to the table view. Then, create a new item and tap the Done button. The BNRDetailViewController will slide off the screen, and your new BNRItem will appear in the table view.

There is one final note to make. We said that the BNRItemsViewController presents the BNRDetailViewController modally. This is true in spirit, but the actual relationships are more complicated than that.

The BNRDetailViewController’s presentingViewController is really the UINavigationController that has the BNRItemsViewController on its stack. You can tell this is the case because when the BNRDetailViewController is presented modally, it covers up the navigation bar. If the BNRItemsViewControllerwas handling the modal presentation, then the BNRDetailViewController’s view would fit within the view of the BNRItemsViewController, and the navigation bar would not be obscured.

For the purposes of presenting and dismissing modal view controllers, this does not matter; the modal view controller does not care who its presentingViewController is as long as it can send it a message and get dismissed. We will address the more complicated truths about view controller relationships at the end of this chapter.

Modal view controller styles

On the iPhone or iPod touch, a modal view controller takes over the entire screen. This is the default behavior and the only possibility on these devices. On the iPad, you have two additional options: a form sheet style and a page sheet style. You can change the presentation of the modal view controller by setting its modalPresentationStyle property to a pre-defined constant – UIModalPresentationFormSheet or UIModalPresentationPageSheet.

The form sheet style shows the modal view controller’s view in a rectangle in the center of the iPad’s screen and dims out the presenting view controller’s view (Figure 17.4).

Figure 17.4 An example of the form sheet style

An example of the form sheet style

The page sheet style is the same as the default full-screen style in portrait mode. In landscape mode, it keeps its width the same as in portrait mode and dims the left and right edges of the presenting view controller’s view that stick out behind it.

In BNRItemsViewController.m, modify the addNewItem: method to change the presentation style of the UINavigationController that is being presented.

UINavigationController *navController = [[UINavigationController alloc]

initWithRootViewController:detailViewController];

navController.modalPresentationStyle = UIModalPresentationFormSheet;

[self presentViewController:navController animated:YES completion:nil];

Notice that you change the presentation style of the UINavigationController, not the BNRDetailViewController, since it is the one that is being presented modally.

Build and run the application on the iPad simulator or on an iPad. Tap the button to add a new item and watch the modal view controller slide onto the screen. Add some item details and then tap the Done button. The table view reappears, but your new BNRItem is not there. What happened?

Before you changed its presentation style, the modal view controller took up the entire screen, which caused the view of the BNRItemsViewController to disappear. When the modal view controller was dismissed, the BNRItemsViewController was sent the messages viewWillAppear: andviewDidAppear: and took this opportunity to reload its table to catch any updates to the BNRItemStore.

With the new presentation style, the BNRItemsViewController’s view does not disappear when it presents the view controller. So it is not sent the appearance messages when the modal view controller is dismissed, and it does not get the chance to reload its table view.

You have to find another opportunity to reload the data. The code for the BNRItemsViewController to reload its table view is simple. It looks like this:

[self.tableview reloadData];

What you need to do is to package up this code and have it executed right when the modal view controller is dismissed. Fortunately, there is a built-in mechanism in dismissViewControllerAnimated:completion: that you can use to accomplish this.

Completion blocks

In both dismissViewControllerAnimated:completion: and presentViewController:animated:completion:, you have been passing nil as the last argument. Take a look at the type of that argument in the declaration for dismissViewControllerAnimated:completion:.

- (void)dismissViewControllerAnimated:(BOOL)flag

completion:(void (^)(void))completion;

Looks strange, huh? This method expects a block as an argument, and passing a block here is the solution to your problem. So we need to talk about blocks. However, the concepts and syntax of blocks can take a while to get used to, so we are just going to introduce them briefly. We will return to blocks as we progress through the book and address different features of them as needed.

A block is a chunk of code to be executed at a later time. You can put the code to reload the table view into a block and pass it to dismissViewControllerAnimated:completion:. Then that code will be executed right after the modal view controller is dismissed.

In BNRDetailViewController.h, add a new property for a pointer to a block.

@property (nonatomic, copy) void (^dismissBlock)(void);

This says BNRDetailViewController has a property named dismissBlock that points to a block. Like a C function, a block has a return value and a list of arguments. These function-like characteristics are included in the declaration of a block. This particular block returns void and takes no arguments.

You will not create the block object in BNRDetailViewController, though. You have to create it in BNRItemsViewController because the BNRItemsViewController is the only object that knows about its tableView.

In BNRItemsViewController.m, create a block that reloads the BNRItemsViewController’s table and pass the block to the BNRDetailViewController. Do this in the addNewItem: method in BNRItemsViewController.m.

- (IBAction)addNewItem:(id)sender

{

// Create a new BNRItem and add it to the store

BNRItem *newItem = [[BNRItemStore sharedStore] createItem];

BNRDetailViewController *detailViewController =

[[BNRDetailViewController alloc] initForNewItem:YES];

detailViewController.item = newItem;

detailViewController.dismissBlock = ^{

[self.tableView reloadData];

};

UINavigationController *navController = [[UINavigationController alloc]

initWithRootViewController:detailViewController];

Now when the user taps a button to add a new item, a block that reloads the BNRItemsViewController’s table is created and set as the dismissBlock of the BNRDetailViewController. The BNRDetailViewController will hold on to this block until the BNRDetailViewController needs to be dismissed.

At that point, the BNRDetailViewController will pass this block to dismissViewControllerAnimated:completion:.

In BNRDetailViewController.m, modify the implementations of save: and cancel: to send the message dismissViewControllerAnimated:completion: with dismissBlock as an argument.

- (IBAction)save:(id)sender

{

[self.presentingViewController dismissViewControllerAnimated:YES

completion:nil];

[self.presentingViewController dismissViewControllerAnimated:YES

completion:self.dismissBlock];

}

- (IBAction)cancel:(id)sender

{

[[BNRItemStore sharedStore] removeItem:self.item];

[self.presentingViewController dismissViewControllerAnimated:YES

completion:nil];

[self.presentingViewController dismissViewControllerAnimated:YES

completion:self.dismissBlock];

}

Build and run the application. Tap the button to create a new item and then tap Done. The new BNRItem will appear in the table.

Once again, do not worry if the syntax or the general idea of blocks does not make sense at this point. Hold on until Chapter 19, and we will go into more detail there.

Modal view controller transitions

In addition to changing the presentation style of a modal view controller, you can change the animation that places it on screen. Like presentation styles, there is a view controller property (modalTransitionStyle) that you can set with a pre-defined constant. By default, the animation will slide the modal view controller up from the bottom of the screen. You can also have the view controller fade in, flip in, or appear underneath a page curl.

The various transition styles are:

UIModalTransitionStyleCoverVertical

slides up from the bottom

UIModalTransitionStyleCrossDissolve

fades in

UIModalTransitionStyleFlipHorizontal

flips in with a 3D effect

UIModalTransitionStylePartialCurl

presenting view controller is peeled up revealing the modal view controller

Thread-Safe Singletons

Thus far, we have only talked about single-threaded apps. A single-threaded app only uses one core and is executing only one function at a time. Multithreaded apps can execute multiple functions simultaneously on different cores.

In Chapter 11, you created a singleton like this:

+ (instancetype)sharedStore

{

static BNRImageStore *sharedStore = nil;

if (!sharedStore) {

sharedStore = [[self alloc] initPrivate];

}

return sharedStore;

}

// No one should call init

- (instancetype)init

{

@throw [NSException exceptionWithName:@"Singleton"

reason:@"Use +[BNRImageStore sharedStore]"

userInfo:nil];

return nil;

}

- (instancetype)initPrivate

{

self = [super init];

if (self) {

_dictionary = [[NSMutableDictionary alloc] init];

}

return self;

}

This singleton technique is sufficient in a single-threaded app. However, if your app is multithreaded, you could end up creating two instances of BNRImageStore. Or, you might return an instance for use before it gets properly initialized.

You can create a singleton that is thread-safe by using the function dispatch_once to ensure that code is run exactly once.

Open BNRImageStore.m and alter your sharedStore method to make BNRImageStore a thread-safe singleton:

+ (instancetype)sharedStore

{

static BNRImageStore *sharedStore = nil;

if (!sharedStore) {

sharedStore = [[self alloc] initPrivate];

}

static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{

sharedStore = [[self alloc] initPrivate];

});

return sharedStore;

}

Build and run it. You should see no change in behavior, but your sharedStore method is now thread-safe.

Bronze Challenge: Another Thread-Safe Singleton

Update the BNRItemStore class singleton to also use dispatch_once().

Gold Challenge: Popover Appearance

You can change the appearance of a UIPopoverController. Do this for the popover that presents the UIImagePickerController. (Hint: check out the popoverBackgroundViewClass property in UIPopoverController.)

For the More Curious: Bitmasks

Earlier in this chapter, you saw the method supportedInterfaceOrientations that returned all of the acceptable interface orientations for a view controller. While the return value for this method was a single int, this int was somehow capable of indicating every combination of the four possible interface orientation values. This is possible because of something called a bitmask.

To understand the need for a bitmask, consider an alternative solution for a view controller to advertise which interface orientations it supports. A naive approach would be to give each view controller four properties like this:

@property (nonatomic, assign) BOOL canRotateToLandscapeLeft;

@property (nonatomic, assign) BOOL canRotateToLandscapeRight;

@property (nonatomic, assign) BOOL canRotateToPortrait;

@property (nonatomic, assign) BOOL canRotateToPortraitUpsideDown;

With this approach, each time the device rotated and the root view controller was checked to see if the interface orientation should comply, the appropriate property would be checked. A bitmask exists to minimize the amount of code and storage needed to represent a series of on and off switches in a single integer variable.

A bitmask is possible because a computer stores values in binary. Binary numbers are a string of 1s and 0s. Here are a few examples of numbers in base 10 (decimal; the way we think about numbers) and base 2 (binary; the way a computer thinks about numbers):

110 = 000000012

210 = 000000102

1610 = 000100002

2710 = 000110112

3410 = 001000102

When talking about binary numbers, we call each digit a bit. You can think of each bit as an on-off switch, where 1 is “on” and 0 is “off.” When thinking in these terms, we can use an int (which has space for at least 32 bits) as a set of on-off switches. Each position in the number represents one switch – a value of 1 means true, 0 means false. Essentially, we are shoving a ton of BOOLs into a single value.

Notice that in the examples above, numbers like 1, 2 and 16 – which are powers of two – have all zeroes and a single one. Numbers that are not a power of two, like 27 and 34, have multiple ones in their binary representation. Therefore, we can use numbers that are powers of two to represent a single switch in a bitmask. Each one of these switches are known as a mask.

There exists a mask for each possible interface orientation:

UIInterfaceOrientationMaskPortrait = 210 = 000000102

UIInterfaceOrientationMaskPortraitUpsideDown = 410 = 000001002

UIInterfaceOrientationMaskLandscapeRight = 810 = 000010002

UIInterfaceOrientationMaskLandscapeLeft = 1610 = 000100002

We can turn on a switch in a bitmask using the bitwise-OR operation. This operation takes two numbers and produces a result where a bit is set to 1 if either of the original numbers had a 1 in the same position. When you bitwise-OR a number with 2n, it flips on the switch at the nth position. For example, if you bitwise-OR 1 and 16, you get the following:

00000010 ( 210, UIInterfaceOrientationMaskPortrait)

| 00010000 (1610, UIInterfaceOrientationMaskLandscapeLeft)

----------

00010010 (1810, both UIInterfaceOrientationMaskPortrait

and UIInterfaceOrientationMaskLandscapeLeft)

The complement to the bitwise-OR operator is the bitwise-AND (&) operator. When you bitwise-AND two numbers, the result is a number that has a 1 in each bit where there is a 1 in the same position as both of the original numbers.

00010010 (1810, Portrait and Landscape Left)

& 00010000 (1610, Landscape Left)

----------

00010000 (1610, YES)

00010010 (1810, Portrait and Landscape Left)

& 00000100 (410, Upside Down)

----------

00000000 (010, NO)

Since any non-zero number means YES (and zero is NO), we use the bitwise-AND operator to check whether a switch is on or not. Thus, when a view controller’s supportedInterfaceOrientations mask is checked, the code looks like this:

if ([viewController supportedInterfaceOrientations]

& UIInterfaceOrientationMaskLandscapeLeft)

{

// Allow interface orientation to change to landscape left

}

For the More Curious: View Controller Relationships

The relationships between view controllers are important for understanding where and how a view controller’s view appears on the screen. Overall, there are two different types of relationships between view controllers: parent-child relationships and presenting-presenter relationships. Let’s look at each one individually.

Parent-child relationships

Parent-child relationships are formed when using view controller containers. Examples of view controller containers are UINavigationController, UITabBarController, and UISplitViewController (which you will see in Chapter 22). You can identify a view controller container because it has aviewControllers property that is an array of the view controllers it contains.

A view controller container is always a subclass of UIViewController and thus has a view. The behavior of a view controller container is that it selectively adds the views of its viewControllers as subviews of its own view. A container has its own built-in interface, too. For example, aUINavigationController’s view shows a navigation bar and the view of its topViewController.

View controllers in a parent-child relationship form a family. So, a UINavigationController and its viewControllers are in the same family. A family can have multiple levels. For example, imagine a situation where a UITabBarController contains a UINavigationController that contains aUIViewController. These three view controllers are in the same family (Figure 17.5). The container classes have access to their children through the viewControllers array, and the children have access to their ancestors through four properties of UIViewController.

Figure 17.5 A view controller family

A view controller family

Every UIViewController has a parentViewController property. This property holds the closest view controller ancestor in the family. Thus, it could return a UINavigationController, UITabBarController, or a UISplitViewController depending on the makeup of the family tree.

The ancestor-access methods of UIViewController include navigationController, tabBarController, and splitViewController. When a view controller is sent one of these messages, it searches up the family tree (using the parentViewController property) until it finds the appropriate type of view controller container. If there is no ancestor of the appropriate type, these methods return nil.

Presenting-presenter relationships

The other kind of relationship is a presenting-presenter relationship, which occurs when a view controller is presented modally. When a view controller is presented modally, its view is added on top of the view controller’s view that presented it. This is different than a view controller container, which intentionally keeps a spot open on its interface to swap in the views of the view controllers it contains. Any UIViewController can present another view controller modally.

Figure 17.6 Presenting-presenter relationship

Presenting-presenter relationship

There are two built-in properties for managing the relationship between presenter and presentee. A modally-presented view controller’s presentingViewController will point back to the view controller that presented it, while the presenter will keep a pointer to the presentee in itspresentedViewController property (Figure 17.6).

Inter-family relationships

A presented view controller and its presenter are not in the same view controller family. Instead, the presented view controller has its own family. Sometimes, this family is just one UIViewController; other times, this family is made up of multiple view controllers.

Understanding the difference in families will help you understand the values of properties like presentedViewController and navigationController. Consider the view controllers in Figure 17.7. There are two families, each with multiple view controllers. This diagram shows the values of the view controller relationship properties.

Figure 17.7 A view controller hierarchy

A view controller hierarchy

First, notice that the properties for parent-child relationships can never cross over family boundaries. Thus, sending tabBarController to a view controller in Family 2 will not return the UITabBarController in Family 1; it will return nil. Likewise, sending navigationController to the view controller in Family 2 returns its UINavigationController parent in Family 2 and not the UINavigationController in Family 1.

Perhaps the oddest view controller relationships are the ones between families. When a view controller is presented modally, the actual presenter is the oldest member of the presenting family. For example, in Figure 17.7, the UITabBarController is the presentingViewController for the view controllers in Family 2. It does not matter which view controller in Family 1 was sent presentViewController:animated:completion:, the UITabBarController is always the presenter.

This behavior explains why the BNRDetailViewController obscures the UINavigationBar when presented modally but not when presented normally in the UINavigationController’s stack. Even though the BNRItemsViewController is told to do the modal presenting, its oldest ancestor, theUINavigationController, actually carries out the task. The BNRDetailViewController is put on top of the UINavigationController’s view and thus obscures the UINavigationBar.

Notice also that the presentingViewController and presentedViewController are valid for every view controller in each family and always point to the oldest ancestor in the other family.

You can actually override this oldest-ancestor behavior (but only on the iPad). By doing so, you can specify where the views of the presented view controller family appear on the screen. For example, you could present the BNRDetailViewController and its navigationController so that it only obscures the UITableView but not the UINavigationBar.

Every UIViewController has a definesPresentationContext property for this purpose. By default, this property is NO, which means the view controller will always pass presentation off to its next ancestor, until there are no more ancestors left. Setting this property to YES interrupts the search for the oldest ancestor, allowing a view controller to present the modal view controller in its own view (Figure 17.8). Additionally, you must set the modalPresentationStyle for the presented view controller to UIModalPresentationCurrentContext.

Figure 17.8 Presentation context

Presentation context

You can test this out by changing the code in BNRItemsViewController.m’s addNewItem: method.

UINavigationController *navController = [[UINavigationController alloc]

initWithRootViewController:detailViewController];

navController.modalPresentationStyle = UIModalPresentationFormSheet;

navController.modalPresentationStyle = UIModalPresentationCurrentContext;

self.definesPresentationContext = YES;

navController.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;

[self presentViewController:navController animated:YES completion:nil];

}

After building and running on the iPad, tap the + icon. Notice that the BNRDetailViewController does not obscure the UINavigationBar. Make sure you undo this code before moving on to the next chapter.