Chapter 12. Integrating Your Application

iOS devices offer standard view controllers for, among other things, taking pictures with the camera and sending email from within your own application. The software ecosystem surrounding your application is extremely rich with such built-in services and applications. You should take advantage of these as much as possible. In this chapter, we’ll look at how you can do that.

Users look for application preferences in two main settings: in the application itself, and in the standard iOS Settings application. For simple applications, applications with few preferences, and applications with preferences that need to be modified regularly, you should keep the preferences within the application itself. However, for more complicated applications, applications with complicated or numerous different preferences, and applications with preferences that the user will rarely have to modify, it’s preferable to use the Settings application.

Adding a Preferences panel for your application to the main Settings application is easy. You do this by adding a special Settings.bundle file to your application and then configuring the Root.plist file contained inside the bundle in the Xcode Editor.

When the built-in Settings application launches, it checks each third-party application for the presence of a Settings Bundle. For each bundle it finds, it displays the application’s name and icon on the main page. When the user taps the row belonging to the application, Settings loads the Root.plist Settings Page file and uses that file to display your application’s main page of preferences.

Let’s add a Settings Bundle to the Where Am I? application we wrote in Chapter 10.

Note

Open the Finder and navigate to the location where you saved the latest version of the WhereAmI project. Right-click on the folder containing the project files and select Duplicate. Rename the copy to WhereAmI2. Now open the new version of the project in Xcode and mouse over the blue Project icon at the top of the Project navigator and hit the Enter key, which will make the project name editable. Enter WhereAmI2 as the new project name, and a drop-down will appear, prompting you to approve the changes to the project. Click on Rename when prompted to do so to rename the project.

Open the WhereAmI2 project in Xcode, right-click on the Supporting Files group in the Project Navigator panel, and select New File from the menu. In the template chooser window that appears, look at the lefthand panel and select the Resource category in the iOS section, and then select the Settings Bundle from the main panel and click the Next button (see Figure 12-1).

The settings bundle should appear inside the Supporting Files group. If you click the arrow beside it to expand the bundle, you’ll see the Root.plist file that contains an XML description of the settings root page, and an en.lproj directory containing the localized string resource file (for English). You can add further localizations to your Settings Bundle if needed. Click the Root.plist file to open it in the editor and expand the Preference Items, and you should see something like Figure 12-2.

As you can see, the default Settings Bundle contains some example settings. Click the Run button in the Xcode toolbar to build and deploy the application into iPhone Simulator. Tap the simulator’s Home button to quit the application, and then find the Settings application on the Home screen. Tap the Settings application to open it, and you should see something similar to Figure 12-3.

Since we haven’t added an icon to the application [see the section on Adding an Icon in the next chapter], the space to the left of the WhereAmI entry has a blank icon; if we had added an icon, it would be displayed next to our application name. If you now tap the WhereAmI entry, you’ll be presented with the default Preferences pane generated from the Settings Bundle.

Note

If a file called Icon-Settings.png (a 29×29-pixel image) is located at the top level of your application’s bundle directory (drag it into the top level of your project under Groups & Files and check the box to copy the item), that icon is used to identify your application preferences in the Settings application. If no such image is present, the Settings application uses a scaled-down version of your application’s icon file instead. See the next chapter for more details about adding Icons to your project.

Returning to Xcode, click the Root.plist file inside Settings.bundle to open it in the Xcode Editor, and you’ll see the property list description of the Settings page. Like any property list file, Xcode by default displays the Root.plist file as a key-value pair list. However, you can see the raw XML of the Root.plist property list by right-clicking on the Preference Items key and selecting Property List Type→None (see Figure 12-4).

If you compare Figures 12-2 and 12-3, you can see how the property list file compares to the rendered user interface:

  • Item 0 (PSGroupSpecifier) is a group label whose value is the string Group.
  • Item 1 (PSTextFieldSpecifier) is a text label whose value is the string Name.
  • Item 2 (PSToggleSwitchSpecifier) is a toggle switch labeled “Enabled” with a default value of YES.
  • Item 3 (PSSliderSpecifier) is a slider bar with a minimum value of 0, a maximum value of 1, and a default value of 0.5.

Each interface element in the Settings panel is an item described in the Preference Items array. There are six possible property list keys:

  • Group (PSGroupSpecifier)
  • Title (PSTitleValueSpecifier)
  • Text Field (PSTextFieldSpecifier)
  • Toggle Switch (PSToggleSwitchSpecifier)
  • Multi Value (PSMultiValueSpecifier)
  • Slider (PSSliderSpecifier)

Additionally, although we won’t go into it here, you can point to Child Preference panes (additional settings pages) using the Child Pane (PSChildPaneSpecifier) property list key.

But let’s modify the default property key list provided by Xcode.

Click on Item 3 and press the Backspace key to delete it from the property list file, then do the same for Item 1. You should be left with a Group and a Toggle Switch.

Rename the Group: under Item 0, double-click on the Title property’s value and enter Latitude & Longitude. Keep the Toggle Switch unmodified. After doing this, the Root.plist file should resemble Figure 12-5.

Make sure you’ve saved your changes to the Root.plist file and click the Run button in the Xcode toolbar. Once the application has started, tap the Home button and make your way to the Settings application. Tap the WhereAmI preference entry, and you should now see something closely resembling Figure 12-6. We’re going to use the Preference pane to toggle whether we want the application to display the latitude and longitude on the screen when it displays our map.

Return to Xcode and click on the AppDelegate.m file to open it in the Xcode Editor. Now add the following class method, which initializes the default settings for the application:

+ (void)initialize {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSDictionary *defaultsToRegister =
       [NSDictionary dictionaryWithObject:@"YES" forKey:@"enabled_preference"];
   [defaults registerDefaults:defaultsToRegister];
   [defaults synchronize];
}

If your user has already accessed the application’s settings inside the Settings application before running the application, the default settings will already have been initialized. If this has not been done, the values will not exist and will be set to nil (or in the case of Booleans, to NO). As the application delegate is loaded, this method initializes the user defaults (the initialize: message is sent to each class before it receives any other messages).

Using this method to set the defaults has the unfortunate side effect that you have to specify your defaults in two places: in the Root.plist file, where they properly belong, and in your application delegate, where they don’t.

The right way to deal with this problem is to read in the defaults from the Settings.bundle file, which is stored as part of your application. To do this, replace the initialize: method with the following:

+ (void)initialize {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *settingsBundle =
      [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
    NSDictionary *settings =
      [NSDictionary dictionaryWithContentsOfFile:
         [settingsBundle stringByAppendingPathComponent:@"Root.plist"]];

    NSArray *preferences = [settings objectForKey:@"PreferenceSpecifiers"];
    NSMutableDictionary *defaultsToRegister =
      [[NSMutableDictionary alloc] initWithCapacity:[preferences count]];

    [defaults registerDefaults:defaultsToRegister];
    [defaults synchronize];

}

If your application preferences don’t exist when your application is launched, you can therefore read the values directly from the Settings.bundle file rather than having to store the defaults in two places.

You can check that your preference bundle is working correctly by adding the following into the application delegate’s applicationDidFinishLaunching:withOptions method and looking in the Debug area after the application launches. Add the lines shown in bold:

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    self.window =
        [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    BOOL enabled = [defaults boolForKey:@"enabled_preference"];
    NSLog(@"enabled = %d", enabled);

    self.locationManager = [[CLLocationManager alloc] init];
    if ( [CLLocationManager locationServicesEnabled] ) {
        self.locationManager.delegate = self;
        self.locationManager.distanceFilter = 1000;
        [self.locationManager startUpdatingLocation];
    }

    self.viewController = [[ViewController alloc] initWithNibName:
        @"ViewController" bundle:nil];
    self.window.rootViewController = self.viewController;
    [self.window makeKeyAndVisible];
    return YES;
}

We may have working preferences, but they don’t do anything yet. Let’s change that right now. Click on the ViewController.h interface file to open it in the Xcode Editor, and add the following outlets (shown in bold) to the declaration (inside the curly braces of the @interface block):

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import <CoreLocation/CoreLocation.h>

@interface ViewController : UIViewController {
    IBOutlet UIButton *backgroundButton;
    IBOutlet UILabel *latLabel;
    IBOutlet UILabel *longLabel;
}

@property (strong, nonatomic) IBOutlet MKMapView *mapView;
@property (strong, nonatomic) IBOutlet UILabel *latitude;
@property (strong, nonatomic) IBOutlet UILabel *longitude;

@end

There is no need to make them properties.

Click the ViewController.xib nib file to open it in Interface Builder. Right-click and drag from File’s Owner to connect the backgroundButton outlet to the UIButton we used as a background for the labels, as shown in Figure 12-7; then connect the latLabel and longLabel outlets to the “Latitude” and “Longitude” UILabel elements, respectively.

Save your changes to the nib file and return to Xcode. Then click on the ViewController.h interface file to open it in the editor, and add the following method declaration:

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
#import <CoreLocation/CoreLocation.h>

@interface ViewController : UIViewController {
    IBOutlet UIButton *backgroundButton;
    IBOutlet UILabel *latLabel;
    IBOutlet UILabel *longLabel;
}

@property (strong, nonatomic) IBOutlet MKMapView *mapView;
@property (strong, nonatomic) IBOutlet UILabel *latitude;
@property (strong, nonatomic) IBOutlet UILabel *longitude;

- (void)updateFromDefaults;

@end

Then in the corresponding ViewController.m file, to open it in the Xcode Editor, add the implementation:

- (void)updateFromDefaults {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults synchronize];

    if ( [defaults boolForKey:@"enabled_preference"] ) {
        backgroundButton.hidden = NO;
        latLabel.text = @"Latitude";
        longLabel.text = @"Longitude";
    } else {
        backgroundButton.hidden = YES;
        latLabel.text = @"";
        longLabel.text = @"";
    }
}

and add a viewWillAppear: method:

- (void)viewWillAppear:(BOOL)animated {
    [self updateFromDefaults];
    [super viewWillAppear:animated];
}

This method checks the application preferences to see if Latitude & Longitude are enabled. If they are, we set the text of the labels appropriately and make sure the button is visible. Correspondingly, if Latitude & Longitude are disabled, we hide the button and empty both strings.

The viewWillAppear: method will be called before our view is made visible to the user; however, if the user goes off and takes a phone call, or makes a change to the application’s settings, and then switches back to your application, this method will not be called again. As far as the application is concerned, this view was always visible and has never disappeared. We therefore also must call the updateFromDefaults method from our application delegate in the applicationWillEnterForeground: method.

Open the AppDelegate.m file and add the following call:

- (void)applicationWillEnterForeground:(UIApplication *)application {
    [self.viewController updateFromDefaults];
}

We also need to make a small change to the locationManager:didUpdateToLocation:fromLocation: method. Here we have to stop the application from printing the current latitude and longitude to the screen if Latitude & Longitude are disabled via preferences. Add the lines shown in bold (wrapping the two existing assignments):

- (void)locationManager:(CLLocationManager *)manager
    didUpdateToLocation:(CLLocation *)newLocation
           fromLocation:(CLLocation *)oldLocation {

    double miles = 12.0;
    double scalingFactor =
      ABS( cos(2 * M_PI * newLocation.coordinate.latitude /360.0) );

    MKCoordinateSpan span;
    span.latitudeDelta = miles/69.0;
    span.longitudeDelta = miles/( scalingFactor*69.0 );

    MKCoordinateRegion region;
    region.span = span;
    region.center = newLocation.coordinate;

    [self.viewController.mapView setRegion:region animated:YES];
    self.viewController.mapView.showsUserLocation = YES;

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults synchronize];

    if ( [defaults boolForKey:@"enabled_preference"] ) {
        self.viewController.latitude.text =
          [NSString stringWithFormat:@"%f", newLocation.coordinate.latitude];
        self.viewController.longitude.text =
         [NSString stringWithFormat:@"%f", newLocation.coordinate.longitude];
    } else {
        self.viewController.latitude.text = @"";
        self.viewController.longitude.text = @"";
    }
}

This brackets the lines that set the text of the UILabel elements with an if() block; we set the text of the labels only if Latitude & Longitude are enabled in the preferences.

We’re done here. Make sure all of your changes have been saved, and click the Run button in the Xcode toolbar to compile and deploy your application into iPhone Simulator.

By default, the Latitude & Longitude display is enabled, so everything should appear as before. However, if you disable Latitude & Longitude in Settings and relaunch the Where Am I? application, you’ll see that Latitude & Longitude has disappeared, as shown in Figure 12-8.

The Accounts framework (Accounts.framework) provides a single sign-on model for user accounts. Originally the framework only supported Twitter accounts; however, it is generic, and was built so that Apple can easily add additional account types at a later date without affecting existing code. They duly did so with the arrival of iOS6 and the addition of support for Facebook and Weibo accounts.

Single sign-on improves the user experience, because applications no longer need to prompt a user separately for login information related to an account. It also simplifies development for sign-on to manage the account authorization process for your application. You can use this framework in conjunction with the Twitter framework to access a user’s Twitter account, and you no longer have to include code to handle OAuth or xAuth in your own application.

We can query and store accounts in the account database using the ACAccountStore class, which is part of the Accounts framework.

Calling the requestAccessToAccountsWithType:withCompletionHandler: method on the store will present a dialog to the user confirming whether the application should have access to the accounts of this type. If granted, the application has access to protected properties of and operations on all accounts of the type specified.

    ACAccountStore  *account = [[ACAccountStore alloc] init];
    ACAccountType *accountType = [account accountTypeWithAccountTypeIdentifier:
        ACAccountTypeIdentifierTwitter];

    [account requestAccessToAccountsWithType:accountType
                       withCompletionHandler:^(BOOL granted, NSError *error) {

        if (granted == YES){
           NSArray *array = [account accountsWithAccountType:accountType];
            NSLog(@"%@",array);
        }
    }];

If there are no registered accounts of the requested type in the accounts database, then the returned accounts array will be nil. You can add accounts to the database using the Twitter and Facebook panels in the Settings application (see Figure 12-9).

We’ve already looked at the Social framework earlier in the book where we built a simple table view-based application to retrieve the list of trending topics on Twitter (see Chapter 8). As I mentioned there, that’s not the only thing the Social framework can do; much as the standard view controller can send email, the standard modal view controller allows you to send tweets and post to Facebook.

Let’s recycle our Prototype application one more time.

Right-click or Control-click on the folder containing the project files and select Duplicate. Rename the copied folder to Prototype4. Now open the new version of the project in Xcode and mouse over the blue Project icon at the top of the Project navigator and hit the Enter key, which will make the project name editable. Enter Prototype4 as the new project name, and a drop-down will appear prompting you to approve the changes to the project. Click Rename when prompted to rename the project.

Next, prune back the code:

Add the Social framework to the project. Then click on the ViewController.m implementation file to import the framework into the class:

#import <Social/Social.h>

and add the following code to the pushedGo: method:

- (IBAction)pushedGo:(id)sender {

    SLComposeViewController *socialComposer;
    if ( [SLComposeViewController isAvailableForServiceType:
        SLServiceTypeTwitter] ) {
        socialComposer =
          [SLComposeViewController
            composeViewControllerForServiceType:SLServiceTypeTwitter];
        [socialComposer addImage:[UIImage imageNamed:@"oreilly_logo.gif"]];
        [socialComposer
          addURL:[NSURL URLWithString:@"http://learningiphoneprogramming.com/"]];
        [socialComposer
          setInitialText:@"A tweet from an iOS app and the Twitter framework."];

    }
    socialComposer.completionHandler = ^(SLComposeViewControllerResult result){
        NSString *msg;
        if (result == SLComposeViewControllerResultCancelled) {
            msg = @"Tweet compostion was canceled.";
        } else if (result == SLComposeViewControllerResultDone) {
            msg = @"Tweet composition completed.";
        }
        UIAlertView* alertView =
          [[UIAlertView alloc] initWithTitle:@"Tweet Status"
                                     message:msg
                                    delegate:nil
                           cancelButtonTitle:@"Okay"
                           otherButtonTitles:nil];
        [alertView show];
        [self dismissViewControllerAnimated:YES completion:nil];
    };
    [self presentViewController:socialComposer animated:YES completion:nil];

}

Before running the code, you’ll need to configure a Twitter account (see Figure 12-9 again). If you skip this step, you’ll see the dialog (on the left) in Figure 12-10; otherwise, you’ll see the standard tweet view controller (right).

One of the more interesting features provided by the SDK is the ability for your application to use custom URL schemes to launch other applications, and in turn, to register custom URL schemes of its own. These schemes can be used to launch your application, either from the browser or from another application on the device. Additionally, such schemes are not just limited to launching the application; you can pass additional information to your application via the URL.

Regardless of what you intend to do after a custom URL launches your application, you must first register your custom scheme using your application’s CityGuide-Info.plist file. Let’s do that for our City Guide application.

Open the project in Xcode and click on its CityGuide-Info.plist file to open it in the Standard Editor. Right-click the top row’s Information Property List and select Add Row (see Figure 12-11).

A row will be added and you’ll be prompted to select a key from a drop-down menu. Scroll to the bottom and select “URL types” (see Figure 12-12).

This will create an array key item, so click the disclosure triangle next to “URL types” to expand it.

Click on Item 0 to expand it to show the URL identifier line. The value for this can actually be anything, but it’s normal to use the Bundle Identifier, so double-click the Bundle Identifier value to select it and then copy the identifier string. Then double-click the field to the right of the URL identifier and paste it into the box.

Now right-click on Item 0, and select Add Row. You’ll be presented with a shorter drop-down of possible values; this time select URL Schemes. This will create an array key item. Expand it, double-click the value box for its Item 0, and enter cityguide.

If you’ve followed the procedure correctly, your Info.plist file should now look like mine does in Figure 12-13. We’re done; adding a custom URL scheme to your application really is that easy.

Of course, now that we’ve added the custom URL scheme, we need to modify our application code so that it knows what to do with it. We’re going to modify the City Guide application to take URLs of the form cityguide://<City Name> and open the relevant city page (e.g., the London page for cityguide://London).

We really need to make only a few changes to the City Guide application to implement handling custom URL schemes.

If your application is not running when another application requests it to open a custom URL, then an application:didFinishLaunchingWithOptions: message is sent, which includes a UIApplicationLaunchOptionsURLKey key in the launch options. If the application is already running, and backgrounded by the operating system, then the application:openURL:sourceApplication:annotation: method is invoked in the application delegate. If this method is not implemented, then the deprecated application:handleOpenURL: method is called.

Click the CGAppDelegate.m implementation file to open it in the Xcode Editor, and add the following methods:

- (BOOL)application:(UIApplication *)application
            openURL:(NSURL *)url
  sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {

    return [self application:application handleOpenURL:url];
}

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    [self.viewController displayCityNamed:[url host]];1
    return YES;
}
1

We’ll implement the displayCityNamed: in the CGViewController class to handle displaying the relevant city.

Additionally, you should add the following code to the application:didFinishLaunching WithOptions: method:

- (BOOL)application:(UIApplication *)application
   didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    self.cities = [[NSMutableArray alloc] init];
    NSString *filePath = [self copyDatabaseToDocuments];
    [self readCitiesFromDatabaseWithPath:filePath];

    self.viewController =
      [[CGViewController alloc] initWithNibName:@"CGViewController" bundle:nil];
    self.navController = [[UINavigationController alloc]
    initWithRootViewController:self.viewController];
    self.window.rootViewController = self.navController;
    [self.window makeKeyAndVisible];

    // If the UIApplicationLaunchOptionsURLKey is present,
    // we have been launched with a URL
    if ( [launchOptions objectForKey:UIApplicationLaunchOptionsURLKey] != nil ) {
        NSURL *url =
         (NSURL *)[launchOptions valueForKey:UIApplicationLaunchOptionsURLKey];
        if([[[UIDevice currentDevice] systemVersion] hasPrefix:@"3.2"]) {
            [self application:application handleOpenURL:url];
        }
    }

    return YES;
}

Save your changes and then click the CGViewController.h interface file. Here, we need to declare the displayCityNamed: method:

- (void)displayCityNamed:(NSString *)name;

Then, in the corresponding CGViewController.m implementation file, add the method:

- (void)displayCityNamed:(NSString *)host {

    NSIndexPath *indexPath;
    for( int i = 0; i < cities.count; i++ ) {
        City *thisCity = [cities objectAtIndex:i];
        if( [thisCity.cityName isEqualToString:host] ) {
            indexPath = [NSIndexPath indexPathForRow:i inSection:0];
        }
    }

    // Begin debugging code 1
    UIAlertView *alert =
                  [[UIAlertView alloc]
                     initWithTitle:host
                     message:[NSString stringWithFormat:
                        @"indexPath = %@", indexPath]
                     delegate:nil
                     cancelButtonTitle:nil
                     otherButtonTitles:@"OK", nil];
    [alert show];
    // End debugging code

    CityController *city = [[CityController alloc] initWithIndexPath:indexPath];

    CGAppDelegate *delegate =
    (CGAppDelegate *)[[UIApplication sharedApplication] delegate];
    [delegate.navController pushViewController:city animated:NO];
}
1

Displaying the UIAlertView is purely for debugging purposes to give some feedback. We’re using it because the Debugger console is unavailable, since the application is started by clicking a URL rather than by running under Xcode. It’s not integral to handling the custom URL scheme, and once you understand what’s going on, you can delete this section of the code.

We’re done. Click the Run button to compile and deploy the application into iPhone Simulator. Once the application is launched, quit the application by clicking the Home button and navigate to Safari. Click on the address bar, enter cityguide://London, and click the Go button (or tap the Return key).

If all goes well, Safari should background and the City Guide application will launch. Soon afterward, you should see something similar to Figure 12-14.

This doesn’t work only in Safari; we can now open the City Guide application from other applications using the following snippet of code:

NSString *string = @"cityguide://London"; 1
NSURL *url = [NSURL URLWithString:string];
[[UIApplication sharedApplication] openURL:url];
1

This will open the London city guide in the City Guide application.

In much the same way as the standard view controllers for picking images and sending email and tweets, Apple has provided a standard way to select and play back iPod media inside your own application.

However, things are a little bit more complicated than the other cases we’ve looked at; here, we use an MPMediaPickerController that, via the MPMediaPickerControllerDelegate protocol, returns an MPMediaItemCollection object containing the media items the user has selected, and that can be played using an MPMusicPlayerController object.

Let’s reuse the Prototype application. Open the Finder and navigate to the location where you saved the Prototype project. Right-click the folder containing the project files and select Duplicate; a folder called Prototype copy will be created containing a duplicate of the project. Rename the folder PrototypePlayer, and just as we did in Chapter 7, prune the application down to the stub with the Go button and associated pushedGo: method that we’ll use to trigger the display of our media player.

Note

To prune the Prototype application down to the stub, you will need to:

  1. Delete the WebViewController.h, WebViewController.m, and WebView.xib files from your project.
  2. Remove the #import "WebViewController.h" line from ViewController.m.
  3. Delete the current body of the pushedGo: method.

Next, open the ViewController.h interface file, import the Media Player framework into the interface (.h) files, and declare your class as an MPMediaPickerControllerDelegate:

#import <UIKit/UIKit.h>
#import <MediaPlayer/MediaPlayer.h>

@interface ViewController : UIViewController <MPMediaPickerControllerDelegate>

@property (weak, nonatomic) IBOutlet UIButton *goButton;

- (IBAction)pushedGo:(id)sender;

@end

Save your changes, and open the ViewController.m implementation file. In the pushedGo: method, instantiate an MPMediaPickerController object and present its view modally to the user:

-(IBAction) pushedGo:(id)sender {
   MPMediaPickerController *mediaPicker =
      [[MPMediaPickerController alloc]
        initWithMediaTypes: MPMediaTypeAnyAudio];
   mediaPicker.delegate = self;
   mediaPicker.allowsPickingMultipleItems = YES;
   [self presentViewController:mediaPicker animated:YES completion:nil];
}

Now implement the following two delegate methods:

- (void) mediaPicker:(MPMediaPickerController *) mediaPicker
  didPickMediaItems:(MPMediaItemCollection *) userMediaItemCollection
{
    [self dismissViewControllerAnimated:YES completion: nil];

    MPMusicPlayerController *musicPlayer =
      [MPMusicPlayerController applicationMusicPlayer];
    [musicPlayer setQueueWithItemCollection: userMediaItemCollection];
    [musicPlayer play]; 1
}

- (void) mediaPickerDidCancel: (MPMediaPickerController *) mediaPicker {
    [self dismissModalViewControllerAnimated: YES];
}
1

The MPMusicPlayerController responds to all the messages you might expect (e.g., play, pause, stop, volume). You can link these directly to buttons in your user interface if you want to give users direct control over these functions.

Like the UIImagePickerControllerDelegate methods we met earlier in the book, these two methods are used to dismiss the view controller and handle the returned items.

Note

Save your changes, and click on the Run button in the Xcode toolbar to build and deploy your code. Remember that you’ll need to configure your project [see the section Putting the Application on Your iPhone in Chapter 3] to allow you to deploy the application onto your iPhone or iPod touch so that you can test the application on your device.

Once your application loads, tap the Go button to bring up the MPMediaPickerController, select some songs, and tap the Done button in the navigation bar (see Figure 12-15). Your music should start playing.

Once playback has begun, you need to keep track of the currently playing item and display that to the user, or at the very least provide some way for the user to pause (or stop) playback, or perhaps to change her selection. The MPMusicPlayerController class provides two methods: the beginGeneratingPlaybackNotifications: method and a corresponding endGeneratingPlaybackNotifications: method.

Add this line to the didPickMediaItems: method:

- (void) mediaPicker:(MPMediaPickerController *) mediaPicker
  didPickMediaItems:(MPMediaItemCollection *) userMediaItemCollection {
    [self dismissModalViewControllerAnimated: YES];

    MPMusicPlayerController *musicPlayer =
      [MPMusicPlayerController applicationMusicPlayer];
    [musicPlayer setQueueWithItemCollection: userMediaItemCollection];
    [musicPlayer beginGeneratingPlaybackNotifications];
    [musicPlayer play];
}

When the begin method is invoked, the class will start to generate notifications of when the player state changes and when the current playback item changes, which your application can register to handle by adding itself as an observer using the NSNotificationCenter class:

- (void) mediaPicker:(MPMediaPickerController *) mediaPicker
  didPickMediaItems:(MPMediaItemCollection *) userMediaItemCollection {
    [self dismissModalViewControllerAnimated: YES];

    MPMusicPlayerController *musicPlayer =
    [MPMusicPlayerController applicationMusicPlayer];
    [musicPlayer setQueueWithItemCollection: userMediaItemCollection];
    [musicPlayer beginGeneratingPlaybackNotifications];

    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter
      addObserver:self
         selector:@selector(handleNowPlayingItemChanged:)
             name:@"MPMusicPlayerControllerNowPlayingItemDidChangeNotification"
           object:musicPlayer];

    [notificationCenter
      addObserver:self
         selector:@selector(handlePlaybackStateChanged:)
             name:@"MPMusicPlayerControllerPlaybackStateDidChangeNotification"
           object:musicPlayer];

    [musicPlayer play];

}

This will invoke the selector methods in our class when the appropriate notification arrives. (You could, for example, use the first to update a UILabel in your view telling the user the name of the currently playing song.)

However, for now let’s just implement these methods to print messages to the console log. In the ViewController.h interface file, declare the selector methods:

@interface ViewController : UIViewController <MPMediaPickerControllerDelegate>

@property (weak, nonatomic) IBOutlet UIButton *goButton;

- (IBAction)pushedGo:(id)sender;
- (void)handleNowPlayingItemChanged:(id)notification;
- (void)handlePlaybackStateChanged:(id)notification;

@end

Then, in the ViewController.m implementation file, add the following method. This will be called when the current item being played changes:

- (void)handleNowPlayingItemChanged:(id)notification {
   MPMusicPlayerController *musicPlayer =
      [MPMusicPlayerController applicationMusicPlayer];
   MPMediaItem *currentItem = [musicPlayer nowPlayingItem]; 1
   NSLog(@"%@", currentItem);
}
1

Unusually, the MPMediaItem class has only one instance method: valueForProperty:. This is because the class can wrap a number of media types, and each type can have a fairly wide range of metadata associated with it. You can find a full list of possible keys in the MPMediaItem class reference, but keys include MPMediaItemPropertyTitle and MPMediaItemPropertyArtwork, among others.

While the second method handles changes in state, we can use this to update our user interface (e.g., changing the state of the Play and Stop buttons when the music ends):

- (void)handlePlaybackStateChanged:(id)notification {
    MPMusicPlayerController *musicPlayer =
      [MPMusicPlayerController applicationMusicPlayer];
    MPMusicPlaybackState playbackState = [musicPlayer playbackState];
    if (playbackState == MPMusicPlaybackStatePaused) {
        NSLog(@"Paused");

    } else if (playbackState == MPMusicPlaybackStatePlaying) {
        NSLog(@"Playing");

    } else if (playbackState == MPMusicPlaybackStateStopped) {
        NSLog(@"Stopped");

    }
}

Save your changes, and click the Run button in the Xcode toolbar to build and deploy your code onto your device. Once your application loads, tap the Go button to bring up the MPMediaPickerController again, select some songs, and tap the Done button in the navigation bar. Your music should start playing, and you should see log messages in the Debug area.

Just like the MPMediaPickerController class in the preceding section and the other classes we met earlier in the book, Apple has provided an ABPeoplePickerNavigationController and associated delegate protocol to allow you to both prompt the user for contact information and display contact information to the user. However, in this case, the framework it provides also allows your application to interact with person and group records directly.

To illustrate how to use the ABPeoplePickerNavigationController, we’re going to reuse the Prototype application code yet again. So, open the Finder and navigate to the location where you saved the Prototype project. Right-click on the folder containing the project files and select Duplicate; a folder called Prototype copy will be created containing a duplicate of the project. Rename the folder Prototype5, and just as we did before, prune the application down to the stub with the Go button and associated pushedGo: method that we’ll use to trigger the display of our address book picker.

After you’ve done that, add the AddressBook and AddressBookUI frameworks into the project. Then click on the ViewController.h interface file to open it in the Xcode Editor. We need to declare the class as both an ABPeoplePickerNavigationControllerDelegate and a UINavigationControllerDelegate. Both declarations are necessary for the class to interact with the ABPeoplePickerNavigationController:

#import <UIKit/UIKit.h>
#import <AddressBook/AddressBook.h>
#import <AddressBookUI/AddressBookUI.h>

@interface ViewController : UIViewController
  <UINavigationControllerDelegate,ABPeoplePickerNavigationControllerDelegate>

@property (weak, nonatomic) IBOutlet UIButton *goButton;

-(IBAction) pushedGo:(id)sender;

@end

Now modify the pushedGo: method in the corresponding ViewController.m implementation file:

-(IBAction) pushedGo:(id)sender {
    ABPeoplePickerNavigationController *peoplePicker =
     [[ABPeoplePickerNavigationController alloc] init];
    peoplePicker.peoplePickerDelegate = self; 1
    [self presentModalViewController:peoplePicker animated:YES];
}
1

Unlike most Objective-C classes, the ABPeoplePickerNavigationController uses the peoplePickerDelegate property to specify its delegate rather than the more common delegate property.

Next, add the three mandatory ABPeoplePickerNavigationControllerDelegate methods specified by the delegate protocol:

- (BOOL)peoplePickerNavigationController:
  (ABPeoplePickerNavigationController *)picker
  shouldContinueAfterSelectingPerson:(ABRecordRef)person 1
{
    [self dismissModalViewControllerAnimated:YES];
    return NO;
}

- (BOOL)peoplePickerNavigationController:
  (ABPeoplePickerNavigationController *)picker
  shouldContinueAfterSelectingPerson:(ABRecordRef)person
  property:(ABPropertyID)property
  identifier:(ABMultiValueIdentifier)identifier 2
{
    return NO;
}

- (void)peoplePickerNavigationControllerDidCancel:
  (ABPeoplePickerNavigationController *)picker 3
{
    [self dismissModalViewControllerAnimated:YES];
}
1

If this method returns YES, the picker will continue after the user selects a name from the address book, displaying the person’s details. If the method returns NO, the picker will not continue. If you intend to return NO, you should also dismiss the view controller.

2

This method lets you decide whether the picker should continue after the user selects a name from the address book. The address record is then displayed to the user. If this method returns YES, the picker will continue after the user selects a property (e.g., a mobile phone number, fax number). If the method returns NO, the picker will not continue. If you intend to return NO, you should also dismiss the view controller.

3

This method is called when the user taps the Cancel button in the navigation bar of the picker interface.

We’ve reached a point where you can compile and check the code, but remember that you should also add the AddressBook and AddressBookUI frameworks to the project before clicking the Run button in the Xcode toolbar.

When you do so, you should see the familiar gray screen with the Go button as shown in Figure 12-16; click it and you’ll be presented with a view of the address book. Selecting a name in the address book will dismiss the picker view and return you directly to the main gray screen.

The picker is displayed, but even if the user selects a name from the list, we don’t do anything with the returned record. Let’s add some additional code to the peoplePickerNavigationController:shouldContinueAfterSelectingPerson: method to fix that omission:

- (BOOL)peoplePickerNavigationController:
    (ABPeoplePickerNavigationController *)pickershouldContinueAfterSelectingPerson:
    (ABRecordRef)person  {

    NSString* name =
      (__bridge_transfer NSString *)ABRecordCopyCompositeName(person); 1

    ABMutableMultiValueRef phones =
      ABRecordCopyValue(person, kABPersonPhoneProperty);
    NSArray *numbers =
      (__bridge_transfer NSArray *)ABMultiValueCopyArrayOfAllValues(phones);

    ABMutableMultiValueRef emails = ABRecordCopyValue(person, kABPersonEmailProperty);
    NSString *addresses =
      (__bridge_transfer NSString *)ABMultiValueCopyArrayOfAllValues(emails);

    NSString *note =
      (__bridge_transfer NSString *)ABRecordCopyValue(person, kABPersonNoteProperty);

    NSLog( @"name = %@, numbers = %@, email = %@, note = %@",
           name, numbers, addresses, note );

    [self dismissModalViewControllerAnimated:YES];
    return NO;
}
1

The ABRecordCopyCompositeName() method returns a human-readable name for the record. The __bridge_transfer annotation moves the value into ARC and transfers ownership. It tells ARC that this object is already retained, and that ARC doesn’t need to retain it again.

There are two basic types of properties: single-value and multivalue. Single-value properties contain data that can have only a single value, such as a person’s name. Multivalue properties contain data that can have multiple values, such as a person’s phone number. You can see from the preceding code that single-value and multivalue properties are handled slightly differently.

Make sure you’ve saved your changes and click the Run button in the Xcode toolbar to compile and deploy your application into iPhone Simulator. When the application launches, click the Go button and then select a name from the list. You should see the output in your Debug area.

What if we want to retrieve a specific phone number from the list? It’s easier to let the user select the phone number they need, and that’s where the peoplePickerNavigationController:shouldContinueAfterSelectingPerson:property:identifier: method would come into play (we returned NO from this earlier in this section, so this example does not allow the user to select a number).

A multivalue property is a list of values, but each value also has a text label and an identifier associated with it. This second delegate method provides you with both the property and the identifier for the value (i.e., a specific phone number) that is of interest to the user.

However, if you know which property value you’re looking for inside the multivalue property, you can programmatically retrieve the identifier for that value. For example, here’s how you’d select the mobile phone number from the list of returned phone numbers:

ABMultiValueRef phones = ABRecordCopyValue(person, kABPersonPhoneProperty);

ABMultiValueIdentifier identifier;
for( int i = 0; i < numbers.count; i++ ) {
    if( CFStringCompare( ABMultiValueCopyLabelAtIndex(phones, i),
        kABPersonPhoneMobileLabel, 1 ) == 0 ) {
        identifier = ABMultiValueGetIdentifierAtIndex(phones, i);
    }
}

You can then retrieve the mobile phone number at any time by using the identifier:

NSString *mobile =
  (__bridge_transfer NSString *)
  ABMultiValueCopyValueAtIndex(phones,
    ABMultiValueGetIndexForIdentifier(phones, identifier));
NSLog(@"Mobile = %@", mobile);

Analogously to the MFMailComposeViewController class we talked about in Chapter 7, the MFMessageComposeViewController from the MessageUI framework (you’ll need to import MessageUI/MessageUI.h for this to work) allows you to compose and send text messages:

MFMessageComposeViewController *controller =
      [[MFMessageComposeViewController alloc] init];
if([MFMessageComposeViewController canSendText]) { 1
   controller.body = @"Hello world";
   controller.recipients = [NSArray arrayWithObjects:@"12345678", nil];
   controller.messageComposeDelegate = self;
   [self presentModalViewController:controller animated:YES];
}
1

Always call this method before attempting to present the message compose view controller.

In your header file, make your view controller an MFMessageComposeViewControllerDelegate and a UINavigationControllerDelegate and implement the following callback:

- (void)messageComposeViewController:(MFMessageComposeViewController *)controller
                 didFinishWithResult:(MessageComposeResult)result {
    switch (result) {
        case MessageComposeResultCancelled:
            NSLog(@"Cancelled");
            break;
        case MessageComposeResultFailed:
            NSLog(@"Failed");
            break;
        case MessageComposeResultSent:
            break;
        default:
            break;
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}

which we use to dismiss the modal view controller.

Warning

The MFMessageComposeViewController does not currently support sending multimedia messages.