Use Your Loaf

[[brain engage] write]

UIAlertController Changes in iOS 8

As part of the theme of iOS 8 to make interfaces adaptive there are some major changes to the presentation of view controllers. The new UIPresentationController does a lot of the hard work of animating view controller transitions and adapting to device size changes such as rotation. It also brings some big changes to some old UIKit favourites such as alert views, action sheets, popovers and search bar controllers. This post will be a gentle introduction to this new world by looking at the changes to alert views and action sheets.

UIAlertView - Alerting the Old Way

The last time I wrote about alert views was back in 2011 to describe the UIAlertView changes in iOS 5. The release of iOS 5 brought alert view styles but not much else has changed since then. The code snippet below is all it takes to setup and present an alert view with cancel and OK buttons:

UIAlertView *alertView = [[UIAlertView alloc]
                           initWithTitle:@"DefaultStyle" 
                           message:@"the default alert view style"
                           delegate:self 
                           cancelButtonTitle:@"Cancel" 
                           otherButtonTitles:@"OK", nil];

[alertView show];

The introduction of alert view styles in iOS 5 added a limited ability to create custom alerts by setting the alertViewStyle property. This extended the plain default button-only style to allow plain text input, secure text input or even a login and password input alert:

Basic Two Button Alert Text Input Alert Secure Text Input Alert Login and Password Alert

The UIAlertViewDelegate protocol has callback methods for the button actions and also a method (alertViewShouldEnableOtherButton:) called when a text field changes to allow buttons to be dynamically enabled/disabled.

UIAlertController - Adaptive Alerting

In the new adaptive world of iOS 8 the UIAlertController is a functionally identical, block-based replacement for both UIAlertView and UIActionSheet. Switching between an alert or action sheet is done by setting the preferred style when creating the controller.

A Simple Alert

It is interesting to compare the code required to setup a new style alert to the old UIAlertView. The creation of the basic UIAlertController is very similar to creating an UIAlertView (the alertTitle and alertMessage are both NSString’s):

UIAlertController *alertController = [UIAlertController
                              alertControllerWithTitle:alertTitle
                              message:alertMessage
                              preferredStyle:UIAlertControllerStyleAlert];

There is no delegate, nor do we initially specify the buttons. Note the third parameter which chooses between the alert and action sheet styles.

You add action buttons by creating an instance of UIAlertAction which you then add to the controller. The UIAlertAction consists of a title string, style and a block to execute when the user selects the action. The three possible choices for the UIAlertActionStyle cover default, cancel and destructive actions. To reproduce the classic cancel/ok action sheet we just need to create and add the two alert actions:

UIAlertAction *cancelAction = [UIAlertAction 
            actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel action")
                      style:UIAlertActionStyleCancel
                    handler:^(UIAlertAction *action)
                    {
                      NSLog(@"Cancel action");
                    }];

UIAlertAction *okAction = [UIAlertAction 
            actionWithTitle:NSLocalizedString(@"OK", @"OK action")
                      style:UIAlertActionStyleDefault
                    handler:^(UIAlertAction *action)
                    {
                      NSLog(@"OK action");
                    }];

[alertController addAction:cancelAction];
[alertController addAction:okAction];

Finally we can present the alert view controller as with any other view controller:

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

The display order for the buttons depends on the order they are added to the alert controller. If you follow the iOS Human Interface Guidelines you should make the default action the right button and the cancel button the left button for a two button alert. You can only have one cancel action, if you add a second you will get a runtime exception:

*** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘UIAlertController can only have one action with a style of UIAlertActionStyleCancel’

Destructive actions

Here is a quick example of the third alert action style for destructive actions. The code is the same as before except that we add a “reset” button instead of the “ok” button:

UIAlertAction *resetAction = [UIAlertAction
             actionWithTitle:NSLocalizedString(@"Reset", @"Reset action")
                       style:UIAlertActionStyleDestructive
                     handler:^(UIAlertAction *action)
                     {
                       NSLog(@"Reset action");
                     }];

[alertController addAction:resetAction];
[alertController addAction:cancelAction];

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

Note that this time the destructive action is added first to make it appear on the left.

Text Input Alerts

The greater flexibility of the UIAlertController means that you no longer need to be constrained by the built-in styles for plain text, secure text or login and password input alert views. We can add an arbitrary number of UITextField objects to the alert and use all of the standard UITextField configuration options. When you add the text field to the alert controller you specify a block that is used to configure the text field.

For example, to recreate the old login and password style alert we can add two text fields and configure them with the appropriate placeholder text and set the password field to use secure text entry:

UIAlertController *alertController = [UIAlertController
                    alertControllerWithTitle:alertTitle
                                     message:alertMessage
                              preferredStyle:UIAlertControllerStyleAlert];

[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField)
 {
   textField.placeholder = NSLocalizedString(@"LoginPlaceholder", @"Login");
 }];

[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField)
 {
   textField.placeholder = NSLocalizedString(@"PasswordPlaceholder", @"Password");
   textField.secureTextEntry = YES;
 }];

The values of the text field can be retrieved in the OK action handler:

UIAlertAction *okAction = [UIAlertAction
  actionWithTitle:NSLocalizedString(@"OK", @"OK action")
  style:UIAlertActionStyleDefault
  handler:^(UIAlertAction *action)
  {
    UITextField *login = alertController.textFields.firstObject;
    UITextField *password = alertController.textFields.lastObject;
    ...
  }];

Things get a little more complicated if we want to reproduce the behaviour of the old UIAlertView delegate method alertViewShouldEnableOtherButton:. Assume we only want to enable the OK button if the user has entered at least 3 characters in the login field. There is no equivalent delegate method for UIAlertController so we need to add an observer to the login text field. We can do that with the following code snippet in the configuration block:

[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField)
 {
    ...
    [[NSNotificationCenter defaultCenter] addObserver:self
                            selector:@selector(alertTextFieldDidChange:)
                                name:UITextFieldTextDidChangeNotification
                              object:textField];
 }];

We need to remove the observer when the view controller is dismissed by adding the appropriate code to the handler block for each of the actions (and anywhere else we may dismiss the alert controller). For example in the okAction block we saw earlier:

UIAlertAction *okAction = [UIAlertAction 
  actionWithTitle:NSLocalizedString(@"OK", @"OK action")
  style:UIAlertActionStyleDefault
  handler:^(UIAlertAction *action)
  {
    ...
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                 name:UITextFieldTextDidChangeNotification
                               object:nil];
  }];

Before presenting the alert controller we can disable the OK action:

okAction.enabled = NO;

Then in the notification observer we can check the login text field for content before changing the state back to enabled:

- (void)alertTextFieldDidChange:(NSNotification *)notification
{
  UIAlertController *alertController = (UIAlertController *)self.presentedViewController;
  if (alertController)
  {
    UITextField *login = alertController.textFields.firstObject;
    UIAlertAction *okAction = alertController.actions.lastObject;
    okAction.enabled = login.text.length > 2;
  }
}

The alert view is now presented with the OK button disabled unless there are at least three characters in the login text field:

Action Sheet

The action sheet is used when you need to present the user with a set of choices. Unlike the alert view which is always presented as a modal view the presentation of the action sheet depends on the device size. On an iPhone (compact width) the action sheet rises from the bottom of the screen. On an iPad (regular width) an action sheet is always shown in a popover.

The creation of an action sheet is almost identical to an alert, the only difference being the style:

UIAlertController *alertController = [UIAlertController
               alertControllerWithTitle:alertTitle
                                message:alertMessage
                         preferredStyle:UIAlertControllerStyleActionSheet];

You add actions the same way as you do for alerts so I will abbreviate the code to add three actions:

UIAlertAction *cancelAction = ...;  // UIAlertActionStyleCancel
UIAlertAction *deleteAction = ...;  // UIAlertActionStyleDestructive
UIAlertAction *archiveAction = ...; // UIAlertActionStyleDefault

[alertController addAction:cancelAction];
[alertController addAction:deleteAction];
[alertController addAction:archiveAction];

You cannot add text fields to action sheets, if you try it you will get a runtime exception:

*** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Text fields can only be added to an alert controller of style UIAlertControllerStyleAlert’

If we do nothing more and present this on an iPhone/compact width device it works as expected:

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

The cancel button, if present, is always shown as the bottom of the view regardless of the order it was added to the alert controller. The other actions are shown top to bottom in the order they were added. The iOS Human Interface Guidelines recommend that any destructive action is shown first.

There is a problem with this code when used on an iPad or regular width device it creates a runtime exception:

Terminating app due to uncaught exception ‘NSGenericException’, reason: ‘UIPopoverPresentationController (<_UIAlertControllerActionSheetRegularPresentationController: 0x7fc619588110>) should have a non-nil sourceView or barButtonItem set before the presentation occurs.’

At the time of writing the Apple UICatalog sample code crashes for the same reason when run on an iPad.

As I mentioned before for regular width presentations the action sheet is displayed in a popover. A popover always requires an anchor point which can be a source view or a bar button item. In this case I am using a standard UIButton to trigger the action sheet so I will use it as the anchor point.

A big difference in iOS 8 is that we no longer need to write code to test for the interface idiom. The UIAlertController takes care of adapting to the display environment so we can simply ask it for a popover controller. On an iPhone/compact width device this returns nil. The extra code we need to configure the popover is below:

UIPopoverPresentationController *popover = alertController.popoverPresentationController;
if (popover)
{
    popover.sourceView = sender;
    popover.sourceRect = sender.bounds;
    popover.permittedArrowDirections = UIPopoverArrowDirectionAny;
}

The UIPopoverPresentationController class is also new in iOS 8 and replaces UIPopoverController and for our purposes is functionally equivalent. The action sheet now displays as a popover anchored to the source button:

Note that the UIAlertController is also smart enough to remove the cancel button when using a popover. A user cancels a popover by touching outside of the popover so it is not required.

Dismissing Alert Controllers

Typically the alert controller is dismissed automatically when the user selects an action. It can also be dismissed programmatically, if required, like any other view controller. One common reason can be to remove the alert or action sheet when the app moves to the background. Assuming we are listening for the UIApplicationDidEnterBackgroundNotification notification we can dismiss any presented view controller in the observer (see the example code for the setup of the observer in viewDidLoad):

- (void)didEnterBackground:(NSNotification *)notification
{
  [[NSNotificationCenter defaultCenter] removeObserver:self
                         name:UITextFieldTextDidChangeNotification
                        object:nil];
  [self.presentedViewController dismissViewControllerAnimated:NO completion:nil];
}

Note that to be safe we also make sure to remove any text field observers we may have added to the alert controller.

In Summary

A long post but hopefully it will be another three years before I need to write about alert and action sheets again. The old UIAlertView and UIActionSheet classes still work fine in iOS 8 so if you need to target iOS 7 there is no rush to migrate immediately. The AlertController sample Xcode project for this post can be found in my GitHub CodeExamples repository.

Open Settings URL

A quick tip I picked up from WWDC 2014 session 715 on User Privacy. Starting with iOS 8 there is now a settings launch URL to send the user directly to the settings for an App. The code snippet below will do the trick:

[[UIApplication sharedApplication] openURL:[NSURL URLWithString:
UIApplicationOpenSettingsURLString]];

Updating Privacy Settings

As an example of where the settings URL can be useful I thought I would go back and update the QR Reader project to better handle the camera privacy settings. See the original post on reading QR codes for the full details. In brief the setup of the camera capture session involves the creation of an input capture device:

NSError *error = nil;
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];

The creation of the AVCaptureDeviceInput can fail if the device does not have a suitable camera or if the user refuses to allow access. In iOS 7 asking the user for permission to access the camera was region dependent. The camera privacy settings changed in iOS 8 to be globally consistent and always ask the user the first time an application tries to access the camera:

In the original code I did not spend any time handling failures. This is especially unhelpful as the user is only prompted to allow access the first time. I will fix that now by adding a method showAlertForCameraError: to give the user some feedback when we fail to create the input capture device:

if (deviceInput)
{
  // Device access allowed
}
else
{
  // Device access failed
  [self showAlertForCameraError:error];
}

Show Alert For Camera Error

The NSError object returned when the deviceInputWithDevice:error: method fails gives us all we need to understand the cause of the failure. If the error code is AVErrorApplicationIsNotAuthorizedToUseDevice we know that the user refused to give us permission.

The idea is to show the user an alert with the error message and for a permissions problem allow the user to jump straight to the app settings to change the privacy settings. The code to check the error type and show an alert view is below:

- (void)showAlertForCameraError:(NSError *)error
{
  NSString *buttonTitle = nil;
  NSString *message = error.localizedFailureReason ? error.localizedFailureReason : error.localizedDescription;

  if ((error.code == AVErrorApplicationIsNotAuthorizedToUseDevice) &&
      UIApplicationOpenSettingsURLString)
  {
    buttonTitle = NSLocalizedString(@"AlertViewSettingsButton", @"Settings");
  }

  UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"AlertViewTitleCameraError", @"Camera Error")
                                                    message:message
                                                   delegate:self
                                          cancelButtonTitle:NSLocalizedString(@"AlertViewCancelButton", @"Cancel")
                                          otherButtonTitles:buttonTitle, nil];
  [alertView show];
}

Notes:

  • The NSError object gives us a localized error message which we can show in the alert view. If present I use localizedFailureReason which at least for camera failures seems to more user-friendly. The fallback is to use localizedDescription.
  • We only want to add the Settings button for permission problems if the launch URL is supported, which is not the case for iOS 7, so we check the URL string is not nil.
  • Finally we create and show the alert view with the view controller as the alert view delegate.

Sending the User to Settings

With the view controller set as the UIAlertView delegate we can handle the user “clicking” the settings button by launching the settings URL:

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
  if (buttonIndex == 1)
  {
    NSURL *url = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
    [[UIApplication sharedApplication] openURL:url];
  }
}

The result is to launch the settings app and open the application specific settings page:

Wrapping Up

I have posted the updated sample code to my GitHub code examples repository if you want to take a closer look.

Xcode 6 Objective-C Modernization Tool

Xcode has for a long time included a refactoring tool which will check your code for modern Objective-C features (Edit > Refactor > Convert to Modern Objective-C Syntax…). I always find it interesting to see what Apple is promoting as good practise and even if you don’t trust Xcode to automatically refactor it is a simple way to audit code for potential improvement.

Xcode 6 introduces a few new modernizations but also a lot more flexibility by allowing individual control other which conversions to run:

Unfortunately it is not always obvious from the description what each conversion does. There are some useful details in the Adopting Modern Objective-C guide along with a demo in the WWDC 2014 Session 417 What’s New in LLVM. This post collects my notes on each of the conversions.

@Property Syntax

The introduction of the newer property syntax is hardly news. Xcode 6 has extended the modernization by including two new conversions to infer properties together with a control for the atomicity of the inferred properties.

  • Infer readonly properties (default Yes)
  • Infer readwrite properties (default Yes)

These first two conversions look for missing @property declarations by identifying potential getter and setter methods in a class. For example, for a class with the two methods but no corresponding property.

- (NSString *)name;
- (void)setName:(NSString *)newName;

Xcode will infer the property which it will add to the class interface:

@property (nonatomic, copy) NSString *name;

The property declaration makes the intent of the two methods explicit and allows the compiler to automatically synthesise the accessor methods. Be aware that the modernization tool will not remove the existing methods for you. That would potentially be dangerous if you have custom behaviour. This conversion can also get carried away and propose properties for methods that are not getters or setters which makes it less useful.

  • Atomicity of inferred properties (default NS_NONATOMIC_IOSONLY)

When creating the inferred property declaration this setting allows you to choose whether you want the property to be atomic, nonatomic or to use the macro NS_NONATOMIC_IOSONLY. The latter is a macro which evaluates to nonatomic on iOS and to nothing on OS X. It would be a good choice for code that you expect to build on both iOS and OS X. For iOS only code you can stick with nonatomic.

@property (NS_NONATOMIC_IOSONLY, readonly, copy) NSDate *dueDate;

Designated Initializers

The Infer designated initializer methods conversion identifies and tags the designated initializers of a class with NS_DESIGNATED_INITIALIZER. To understand why that is useful it is worth recapping how object initialization works in Objective-C. Creation of an Objective-C object is a two step process of allocation and then initialization usually written as a single line of code:

MyObject *object = [[MyObject alloc] init];

The initialization method takes care of setting values for any instance variables as well as any other setup tasks for the object. A class can have many initializer methods, by convention named with the prefix init. For example, a class with an instance variable name that must always be set may have an initializer that includes the name:

- (instancetype)init {
  return [self initWithName:@"Unknown"];
}

- (instancetype)initWithName:(NSString *)name {
  self = [super init];
  if (self) {
    _name = [name copy];
  }
  return self;
}

The plain init method is in this case a convenience initializer which simply calls the designated initializer initWithName: with a default value. The designated initializer guarantees the object is fully initialised by sending an initialization message to the superclass. The implementation detail becomes important to a user of the class when they subclass it. The rules for designated initializers in detail:

  • A designated initializer must call (via super) a designated initializer of the superclass. Where NSObject is the superclass this is just [super init].
  • Any convenience initializer must call another initializer in the class - which eventually leads to a designated initializer.
  • A class with designated initializers must implement all of the designated initializers of the superclass.

For a long time there was no way to tell the compiler or user of a class which are the designated initializers (other than in a comment). To correct that situation Clang now has the attribute objc_designated_initializer. In iOS 8 the NS_DESIGNATED_INITIALIZER macro, defined in NSObjCRuntime.h, makes it easy to apply this to a method:

#define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))

So for the earlier example we would have:

- (instancetype)init;
- (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER; 

If a convenience initializer you add does not call a designated initializer you will now get a warning. I have seen people reporting problems with some UIKit classes where Apple has not yet tagged designated initializers so as always you will want to test and file bug reports for unexpected results.

Infer Instancetype for Method Result Type

This migration will replace id with instancetype as the result type of “alloc”, “init” and “new” methods. You may need to manually convert any class factory methods. See this NSHipster article for details on the increased type safety you get from adopting instancetype.

Infer Protocol Conformance

This conversion which is off by default causes Xcode to add missing protocol conformance declarations. For example, a plain view controller which does not declare conformance to any protocols:

@interface UYLViewController : UIViewController 

If this class implements the two mandatory table view data source methods -tableView:numberOfRowsInSection: and -tableView:cellForRowAtIndexPath: it would have the interface statement modified as follows:

@interface UYLViewController : UIViewController<UITableViewDataSource> 

From what I can tell the protocol conformance is inferred if all of the mandatory methods are implemented. I could not for example make it infer compliance to the UITableViewDelegate protocol which has only optional methods.

Objective-C Literals

  • ObjC literals
  • ObjC subscripting

These two migrations were already present in Xcode 5 so I will just include a quick example:

NSNumber *magicNumber = [NSNumber numberWithInteger:42];
NSDictionary *myDictionary = [NSDictionary dictionaryWithObject:magicNumber forKey:@"magic"];

Refactoring to use Objective-C literals and subscripting results in the more compact code:

NSNumber *magicNumber = @42;
NSDictionary *myDictionary = @{@"magic": magicNumber};

Enumerations

  • Use NS_ENUM/NS_OPTIONS macros

The modern NS_ENUM and NS_OPTIONS macros are a quick and easy way to create enumerations that specify the type and size to the compiler. For example, an enumerated type:

enum {
  UYLTypeDefault,
  UYLTypeSmall,
  UYLTypeLarge
};
typedef NSInteger UYLType;

refactored to use NS_ENUM becomes:

typedef NS_ENUM(NSInteger, UYLType)
{
  UYLTypeDefault,
  UYLTypeSmall,
  UYLTypeLarge
};

Likewise for a set of bitmasks defined as follows:

enum
{
  UYLBitMaskA = 0,
  UYLBitMaskB = 1 << 0,
  UYLBitMaskC = 1 << 1,
  UYLBitMaskD = 1 << 2
};
typedef NSUInteger UYLBitMask;

refactored to use NS_OPTIONS becomes:

typedef NS_OPTIONS(NSUInteger, UYLBitMask)
{
  UYLBitMaskA = 0,
  UYLBitMaskB = 1 << 0,
  UYLBitMaskC = 1 << 1,
  UYLBitMaskD = 1 << 2
};

Miscellaneous

  • Add attribute annotations

I was unable to make this migration do anything. The explanatory text suggests it will add attribute annotations to properties and methods but I was unable to discover which attributes it adds and under what situations. Leave a comment if you know…