24. Accessing the Photo Library

All current iOS devices come with at least one camera capable of taking photos and videos. In addition, all iOS devices can sync photos from iTunes on a computer to the Photos app and organize them in albums. Before iOS 4, the only method for developers to access user photos was UIImagePickerController. This approach has some drawbacks; namely, only one photo can be selected at a time, and the developer has no control over the appearance of the UI. With the addition of the AssetsLibrary classes in iOS 4, Apple provided much more robust access to the user’s photos, videos, albums, and events in an app. Although the AssetsLibrary classes were a big improvement over the UIImagePickerController, it was still difficult to navigate a large number of assets or get arbitrarily sized images with good performance. In addition, it was not simple (or in some cases possible) to manipulate the user’s albums and maintain the images within an album.

To address these and other issues, Apple introduced the Photos framework with iOS 8. The Photos framework provides a robust, thread-safe way to access and administer the user’s photo library. The Photos framework provides a method to retrieve arbitrarily sized versions of the images in the photo library, and provides a callback mechanism to notify a delegate when changes to the photo library have occurred. The Photos framework makes accessing photos in iCloud seamless as well; the same operations for accessing and updating the library work regardless of whether the photo is available locally on the device or remotely in iCloud.

The Sample App

The sample app, PhotoLibrary, is a minimal reproduction of some key parts of the iOS Photos app. It provides a tab bar to select between Photos and Albums. The Photos tab will display a collection view of all the images on the device, organized by moments. Tapping on a thumbnail will display a larger representation of the photo, and provide the opportunity to delete the photo from the device.

The Albums tab will display a table of the user-created albums available on the device, including the album name, number of photos, and a representative image. The user can add or remove albums from this view. Tapping an album will show thumbnails of all the photos in the album. Tapping a thumbnail will show a large representation of the photo.

Before running the sample app, prepare a test device by syncing some photos to it and taking some photos. That way, there will be photos in albums and the camera roll. If you use iCloud, turn on My Photo Stream as well.


Note

To use the Photos framework in an app, include @import Photos; in classes that need to access the Photos framework classes. This will automatically add the Photos framework to the project, and import the classes as needed.


The Photos Framework

The Photos framework consists of a group of classes to navigate and manage the collections, photos, and videos on the device. Here are some highlights:

Image PHPhotoLibrary: This represents all the collections and assets on the device and iCloud. A shared instance ([PHPhotoLibrary sharedPhotoLibrary]) can be used to manage changes in a thread-safe way to the photo library, such as adding a new asset or album, or changing or removing an existing asset or album. In addition, the shared instance can be used to register an object as a listener for changes to the photo library, which can be used to keep the user interface in sync with asynchronous changes to the library.

Image PHAssetCollection: This represents a group of photos and videos. It can be created locally on the device, can be synced from a photo album in iPhoto, can be the user’s Camera Roll or Saved Photos album, or can be a smart album containing all photos that match a certain criteria (panoramas, for example). It provides methods for accessing assets in the group and getting information about the group. Asset collections can be organized in collection lists (PHCollectionList).

Image PHAsset: This represents the metadata for a photo or video. It provides class methods to return fetch results to get assets with similar criteria, and provides instance methods with information about the asset, such as date, location, type, and orientation.

Image PHFetchResult: This is a lightweight object that represents an array of assets or asset collections. When requesting assets or asset collections that match a given criteria, the class methods will return a fetch result. The fetch result will manage loading what is needed as requested instead of loading everything into memory at once, so it works very well for large collections of assets. The fetch result is also thread-safe, meaning that the count of objects will not change if changes to the underlying data occur. A class can register to be notified of changes to the photo library; and those changes, delivered in an instance of PHFetchResultChangeDetails, can be used to update a fetch result and any corresponding user interface.

Image PHImageManager: The image manager handles asynchronously fetching and caching image data for an asset. This is especially useful for getting images that match a specific size, or for managing access to image data from iCloud. In addition, the photo library offers PHCachingImageManager to improve scrolling performance when viewing large numbers of assets in a table or collection view.

Using Asset Collections and Assets

Photos.app displays photos on the device organized by “moments”—or photos that took place on the same date in the same location. A moment is represented in the photo library by an instance of PHAssetCollection. Each image displayed for a moment is represented by an instance of PHAsset. User albums in Photos.app are also instances of PHAssetCollection, with images represented by PHAsset. Before accessing the asset collections and assets, an app needs to get the user’s permission to access the photo library.

Permissions

The first time an app tries to access the photo library, the device asks the user for permission (as shown in Figure 24.1).

Image

Figure 24.1 Access permission alert in the PhotoLibrary sample app.

To request permission, the app uses the requestAuthorization: class method on PHPhotoLibrary:

[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
    if (status == PHAuthorizationStatusAuthorized) {
        [self loadAssetCollectionsForDisplay];
    } else {
        ...
    }
}];

That method checks to see whether authorization has already been requested by the app. If not, it presents an alert to the user requesting permission to the photo library. After the user makes a selection, the user alert will occur only once every 24-hour period even if the app is deleted and reinstalled. To test responding to the user alert more than once a day, visit General, Reset in Settings.app, and select Reset Location & Privacy. That erases the system’s memory of all location and privacy settings and requires responding to the user alert for all apps on the device.

If the user has already granted or denied permission, the alert will not be displayed again, and the user’s selection will be returned. After the authorization status is known, the method calls the status handler block, passing in the current status. Note that the status handler can be called on a background thread, so be sure to switch to the main queue before updating the user interface.

If the user denies permission to access the photo library, the alert view presented explains to the user how to restore permission later if desired. To restore permissions, the user needs to navigate to the right spot in Settings.app (see Figure 24.2).

Image

Figure 24.2 Settings.app: photo privacy.

When the user changes the setting, iOS kills the sample app so that it will launch again rather than coming out of the background and requiring an adjustment to the new privacy setting.

Asset Collections

When permission is granted, the app can now access the photo library to display moments, albums, and images. On the Photos tab in the sample app, the ICFPhotosCollectionViewController will get a list of the moments represented on the device with an instance of PHFetchResult in the loadAssetCollectionsForDisplay method:

PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"startDate"
                                                          ascending:YES]];

self.collectionResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeMoment
                                          subtype:PHAssetCollectionSubtypeAny
                                          options:options];

PHFetchResult can be used for both asset collections and assets. It acts like an NSArray, exposing methods such as objectAtIndex: and indexOfObject:, but it is smart enough to pull information about the results only when necessary. The method will then iterate over the moments from the fetch request, and store a fetch result instance in the property collectionAssetResults to get access to the assets in each moment.

self.collectionAssetResults =
[[NSMutableArray alloc] initWithCapacity:self.collectionResult.count];

for (PHAssetCollection *collection in self.collectionResult) {
    PHFetchResult *result = [PHAsset fetchAssetsInAssetCollection:collection
                                                          options:nil];
    [self.collectionAssetResults insertObject:result atIndex:[self.collectionResult indexOfObject:collection]];
}

With the information available about the moments and assets, the view controller can now populate a collection view, showing a section header for each moment, and associated images in each section. For more information on collection views, refer to Chapter 21, “Collection Views.” Determining the number of sections in the collection view is simple; the answer is just the count from the instance of PHFetchResult representing moments:

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    return self.collectionResult.count;
}

Determining the number of items in a section requires looking up the fetch request for the assets in that section, and getting the count of results from it.

- (NSInteger)collectionView:(UICollectionView *)collectionView
     numberOfItemsInSection:(NSInteger)section {

    PHFetchResult *result = (PHFetchResult *)[self.collectionAssetResults objectAtIndex:section];

    return result.count;
}

To display the dates and location information in the section header for a moment, the collectionView:viewForSupplementaryElementOfKind:atIndexPath: method gets the instance of PHAssetCollection representing the moment for the section indicated by the provided index path:

PHAssetCollection *moment = [self.collectionResult objectAtIndex:indexPath.section];

[headerView.titleLabel setText: [NSString stringWithFormat:@"%@ - %@", [self.momentDateFormatter stringFromDate:moment.startDate], [self.momentDateFormatter stringFromDate:moment.endDate]]];

[headerView.subTitleLabel setText:moment.localizedTitle];

The method can then update the section header using the startDate, endDate, and localizedTitle properties from the PHAssetCollection instance to display the date range of the moment and the location of the moment (see Figure 24.3).

Image

Figure 24.3 Moment Asset Collections in the PhotoLibrary sample app.

The Albums tab in the sample app displays any custom albums the user has added to the photo library in a table view, as shown in Figure 24.4. The list of albums is retrieved with a fetch result:

self.albumsFetchResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum
                                          subtype:PHAssetCollectionSubtypeAny
                                          options:nil];

Image

Figure 24.4 Albums in the PhotoLibrary sample app.

For each album in the table view, the localizedTitle is used to display the album name, and the count of assets in the album is determined using the estimatedAssetCount from the asset collection.

PHAssetCollection *album = [self.albumsFetchResult objectAtIndex:indexPath.row];

[cell.textLabel setText:album.localizedTitle];

if (album.estimatedAssetCount != NSNotFound)
{
    NSString *albumPlural = album.estimatedAssetCount > 1 ? @"s" : @"";

    NSString *subTitle = [NSString stringWithFormat:@"%lu Photo%@", (unsigned long)album.estimatedAssetCount, albumPlural];

    [cell.detailTextLabel setText:subTitle];

} else
{
    [cell.detailTextLabel setText:@"-- empty --"];
}

When the user touches an album, the sample app will display all the assets for that album. Because the sample project uses storyboarding for navigation, a segue is set up from the table cell to the ICFAlbumCollectionViewController. The segue is named showAlbum. In ICFAlbumCollectionViewController, the prepareForSegue:sender: method sets up the destination view controller.

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"showAlbum"])
    {

        ICFAlbumCollectionViewController *controller = (ICFAlbumCollectionViewController *)segue.destinationViewController;

        NSIndexPath *tappedPath = [self.tableView indexPathForSelectedRow];

        PHAssetCollection *tappedCollection = [self.albumsFetchResult objectAtIndex:tappedPath.row];

        [controller setSelectedCollection:tappedCollection];
    }
}

The prepareForSegue:sender: method first checks that the segue’s identifier is equal to showAlbum, since this method will be called for any segue set up for ICFAlbumCollectionViewController. Then, it determines the index path for the tapped row in the table, and uses the row to get the associated asset collection from the fetch result. It sets the selected asset collection in the destination view controller, which will be used to display the assets in the collection.

Assets

In the ICFPhotosCollectionViewController an asset needs to be accessed for each cell in the collection view in order to display the asset’s image. In collectionView:cellForItemAtIndexPath: the method will use the section of the indexPath to determine which fetch result to look in for the asset, and then the row of the indexPath to get the specific asset.

PHFetchResult *result = self.collectionAssetResults[indexPath.section];
PHAsset *asset = result[indexPath.row];

The asset is metadata describing the image. An image representing the asset in a desired size can be requested from the photo library. Since an image of the right size to display might not be available immediately, it might be necessary to resize a local version or download a version from iCloud. PHImageManager is designed to meet this need, and provide an image asynchronously to match what is needed for display. To request an image, provide a target size, a content mode (see Chapter 20, “Working with Images and Filters,” for more information on content modes), options for the image fetching (PHImageRequestOptions), and a result handler block. The method will return a PHImageRequestID, which can be used to cancel the request.

__weak ICFPhotosCollectionViewCell *weakCell = cell;
PHImageManager *imageManager = [PHImageManager defaultManager];

PHImageRequestID requestID =
[imageManager requestImageForAsset:asset
                        targetSize:CGSizeMake(50, 50)
                       contentMode:PHImageContentModeAspectFill
                           options:nil
                     resultHandler:^(UIImage *result, NSDictionary *info){
                         [weakCell.assetImageView setImage:result];
                         [weakCell setNeedsLayout];
                     }];

cell.requestID = requestID;

The image manager will return a low-quality approximation of the image right away to the resultHandler block if a local version is not available to the result handler (and will note that the image is low quality by including the PHImageResultIsDegradedKey in the info dictionary), and will call the result handler again with a higher-quality image when it is available. The result handler will be executed on the same queue that it was called on, so it is safe to update the user interface without switching to the main queue if the request is made from the main queue. If the request is issued from a background queue, the synchronous property of PHImageRequestOptions can be set to YES to block the background queue until the request returns.

If the cell is scrolled off the screen before the image request can be fulfilled, the request can be cancelled in the prepareForReuse method of the cell:

- (void)prepareForReuse {
    self.assetImageView.image = nil;

    PHImageManager *imageManager = [PHImageManager defaultManager];
    [imageManager cancelImageRequest:self.requestID];
}

PHAsset instances are thread-safe, and can be passed around as needed. If the user taps a cell, a segue will fire to present a full-screen view of the asset.

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"showImage"]) {

        ICFAssetViewController *controller = (ICFAssetViewController *)segue.destinationViewController;

        NSIndexPath *indexPath = [self.collectionView indexPathsForSelectedItems][0];
        PHFetchResult *result = self.collectionAssetResults[indexPath.section];
        controller.asset = result[indexPath.row];
    }
}

Then, the detail view (ICFAssetViewController) can request a full-screen image from the PHImageManager for display.

PHImageManager *imageManager = [PHImageManager defaultManager];
[imageManager requestImageForAsset:self.asset
                        targetSize:self.assetImageView.bounds.size
                       contentMode:PHImageContentModeAspectFit
                           options:nil
                     resultHandler:^(UIImage *result, NSDictionary *info){
                         [self.assetImageView setImage:result];
                         [self.assetImageView setNeedsLayout];
                     }];

Changes in the Photo Library

The photo library supports making changes to assets, asset collections, and collection lists in a robust, thread-safe manner. This includes adding, changing, or removing objects in the photo library. For assets, this can include not just adding a new asset or removing an existing asset, but also applying edits and filters to existing changes. The sample app illustrates adding and removing asset collections and assets; the same general approach applies to all photo library changes. To make a change to the photo library, an app needs to have the photo library object perform a change request, and then provide a listener to handle the changes after they are completed.

Asset Collection Changes

In the Albums tab, there is an add (+) button in the navigation bar. Tapping the add button will ask the user for the name of a new album, as shown in Figure 24.5.

Image

Figure 24.5 Add Album in the PhotoLibrary sample app.

If the user selects Add Album, the action handler attempts to create an album with the name provided in the alert view.

UITextField *albumNameTextField = addAlbumAlertController.textFields.firstObject;
NSString *newAlbumName = albumNameTextField.text;
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
    [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:newAlbumName];
} completionHandler:^(BOOL success, NSError *error) {
    if (!success) {
        NSLog(@"Error encountered adding album: %@",error.localizedDescription);
    }
}];

To add a new album, a change request is needed. There are change requests to handle creating new albums, changing existing albums, and deleting albums. The change request is passed to the photo library in a performChanges block, which will then respond with a completionHandler indicating whether the changes were successfully made to the library. Similarly, the Edit button will enable the user to delete an album from the list of albums shown in the table view. In the tableView:commitEditingStyle:forRowAtIndexPath: method, the album for the selected row will be deleted.

if (editingStyle == UITableViewCellEditingStyleDelete) {

    PHAssetCollection *albumToBeDeleted = [self.albumsFetchResult objectAtIndex:indexPath.row];

    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetCollectionChangeRequest deleteAssetCollections:@[albumToBeDeleted]];
    } completionHandler:^(BOOL success, NSError *error) {
        if (!success) {
            NSLog(@"Error encountered adding album: %@",error.localizedDescription);
        }
    }];
}

Note that the completion handler does not provide any information about the actual changes completed; for that information, a view controller needs to register as an observer for the PHPhotoLibraryChangeObserver protocol.

[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];

Then, the photoLibraryDidChange method needs to be implemented. That method will receive an instance of PHChange, which can tell whether there are changes that affect any given fetch result, and if so can provide all the specific changes. For the album view, the method checks to see whether any of the albums in the fetch result are affected.

PHFetchResultChangeDetails *changesToFetchResult = [changeInstance changeDetailsForFetchResult:self.albumsFetchResult];

If the fetch results were affected, they need to be updated to reflect the new state of the photo library. PHFetchResultChangeDetails has a simple way to accomplish this task; it provides both the before and the after views of the fetch result.

self.albumsFetchResult = [changesToFetchResult fetchResultAfterChanges];

Now that the fetch result is up-to-date, the change details can be used to update the table view so that it matches the fetch result. All the details needed to easily update a table view or collection view are provided.

if ([changesToFetchResult hasIncrementalChanges])
{
    [self.tableView beginUpdates];

    [[changesToFetchResult removedIndexes] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {

        NSIndexPath *indexPathToRemove = [NSIndexPath indexPathForRow:idx inSection:0];

        [self.tableView deleteRowsAtIndexPaths:@[indexPathToRemove]
                              withRowAnimation:UITableViewRowAnimationAutomatic];
    }];

    [[changesToFetchResult insertedIndexes] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {

        NSIndexPath *indexPathToInsert = [NSIndexPath indexPathForRow:idx inSection:0];

        [self.tableView insertRowsAtIndexPaths:@[indexPathToInsert]
                              withRowAnimation:UITableViewRowAnimationAutomatic];
    }];

    [self.tableView endUpdates];
}

The method checks to see whether the change details include incremental changes. If so, the method iterates over the removed indexes, creates an index path for each removed index, and deletes it from the table view with animation. Next, the method iterates over the inserted indexes, creates an index path for each added row, and inserts them into the table view with animation. The indexes are provided in the change details to support this pattern; note that the row deletions must take place first, or the indexes for additions and changes will not be correct. After the changes are complete, the table view animates all the changes and reflects the new state of the photo library.

Asset Changes

In the Albums tab in the sample app, select an album to view the assets in that album. The add button in the navigation bar will initiate the process of adding an image to the photo library, and adding that new image to the selected album. Tapping the add button will present a UIImagePickerController for the camera, so the sample app must be run on a device to test this feature. Note that an image can be added to the photo library from any external source; the image picker is just a convenient way to demonstrate the capability.

When the user takes a picture from the camera, the image is delivered to the picker’s delegate method.

UIImage *selectedImage = [info objectForKey:UIImagePickerControllerOriginalImage];

To add the image to the photo library, a change request needs to be passed to the photo library in the performChanges block:

[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{

    PHAssetChangeRequest *addImageRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:selectedImage];

    ...
} completionHandler:^(BOOL success, NSError *error) {
    if (!success) {
        NSLog(@"Error creating new asset: %@", error.localizedDescription);
    }
}];

Because the PHAsset representing the newly added image will not be available until the photoLibraryDidChange: method is called, how can the new image be added to the album? The answer is a placeholder object. The photo library can provide a placeholder object for new objects created in a change request so that those new objects can be used in the same change request.

PHObjectPlaceholder *addedImagePlaceholder = [addImageRequest placeholderForCreatedAsset];

The placeholder object can then be used to add the asset that is going to be created to the album:

PHAssetCollectionChangeRequest *addImageToAlbum = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:self.selectedCollection];

[addImageToAlbum addAssets:@[addedImagePlaceholder]];

When the photo library has completed the changes, it calls the photoLibraryDidChange: delegate method on an arbitrary queue. After switching to the main queue, the method will check whether there are changes to the fetch result for the assets in the album.

PHFetchResultChangeDetails *changesToFetchResult = [changeInstance changeDetailsForFetchResult:self.assetResult];

If there are changes, the method updates the fetch result and the table view:

if (changesToFetchResult)
{
    self.assetResult = [changesToFetchResult fetchResultAfterChanges];

    if ([changesToFetchResult hasIncrementalChanges]) {
        NSMutableArray *indexPathsToInsert = [[NSMutableArray alloc] init];

        [[changesToFetchResult insertedIndexes] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {

            NSIndexPath *indexPathToInsert = [NSIndexPath indexPathForRow:idx inSection:0];

            [indexPathsToInsert addObject:indexPathToInsert];

        }];

        [self.collectionView insertItemsAtIndexPaths:indexPathsToInsert];
    }
}

The new cell in the collection view will be added with animation, and the new image will be displayed.

When the user taps on an image in the album view or in the moments view, the image will be displayed in a full-screen format in the ICFAssetViewController. There is a delete button in that view that allows the user to delete an asset from the photo library.

 [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
    [PHAssetChangeRequest deleteAssets:@[self.asset]];
} completionHandler:^(BOOL success, NSError *error) {
    if (success) {
        [self.navigationController popViewControllerAnimated:YES];
    } else {
        NSLog(@"Error deleting asset: %@",error.localizedDescription);
    }
}];

When the user taps the Delete button and the delete asset change request is performed, the photo library will present a confirmation alert, as shown in Figure 24.6. If the user selects Don’t Allow, no action will be taken. If the user selects Delete, the asset will be deleted from the photo library, and any registered listeners will be notified.

Image

Figure 24.6 Deleting a photo in the PhotoLibrary sample app.

Dealing with Photo Stream

Photo Stream is a photo-syncing feature that is part of Apple’s iCloud service. When an iCloud user adds a photo to a Photo Stream–enabled device, that photo is instantly synced to all the user’s other Photo Stream–enabled devices. For example, if the user has an iPhone, an iPad, and a Mac, and takes a photo on the iPhone, the photo will be visible immediately on the iPad (in the Photos app) and the Mac (in iPhoto or Aperture) with no additional effort required.

To use Photo Stream, the user needs to have an iCloud account. An iCloud account can be created free on an iOS device. Visit Settings, iCloud. Create a new account or enter iCloud account information. After the iCloud account information is entered on the device, Photo Stream can be turned on (see Figure 24.7).

Image

Figure 24.7 Settings: iCloud Photos.

When Photo Stream is enabled, moments are synced across devices automatically. No additional code is required in the sample app to display or handle moments from other devices. When requesting images for display from PHImageManager, options can be specified to either explicitly allow or prevent network access as desired.

Summary

This chapter explained how to access the photo library using the Photos framework introduced in iOS 8. It detailed the classes available to access the photo library, and showed how to handle permissions for the photo library. It covered how to work with the Photos framework classes to display images using the same organizational structures that Photos.app uses. This chapter discussed getting properly sized images asynchronously and over the network. Next, the chapter explored details of changing the photo library, including adding and deleting asset collections, and adding and deleting assets. Finally, this chapter explained how to enable iCloud Photo Stream to include remote photos in the photo library.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.198.21