Chapter 18. Photo Library and Camera

This chapter has been revised for Early Release. It reflects iOS 14, Xcode 12, and Swift 5.3. But screenshots have not been retaken; they still show the Xcode 11 / iOS 13 interface.

The stored photos and videos accessed by the user through the Photos app constitute the device’s photo library:

  • The PHPickerViewController class (new in iOS 14) can give the user an interface for exploring the photo library and choosing a photo. You’ll need to import PhotosUI.

  • The Photos framework, also known as PhotoKit, lets you access the photo library and its contents programmatically — including the ability to modify a photo’s image. You’ll need to import Photos.

The user’s device may include one or more cameras, and your app might want to let the user take (capture) a photo or video:

  • The UIImagePickerController class can give the user an interface similar to the Camera app, letting the user capture photos and videos.

  • At a deeper level, the AV Foundation framework (Chapter 16) provides direct control over the camera hardware. You’ll need to import AVFoundation.

The two subjects are related, especially because having allowed the user to capture an image, you will typically store it in the photo library, just as the Camera app does. So this chapter treats them together.

Browsing the Photo Library

PHPickerViewController is a view controller providing an interface, similar to the Photos app, in which the user can choose an item from the photo library. (PHPickerViewController is new in iOS 14, superseding the use of UIImagePickerController for this purpose.) You are expected to treat the PHPickerViewController as a presented view controller.

PHPickerViewController Presentation

The first step in using PHPickerViewController is to create a configuration, an instance of PHPickerConfiguration. This is a struct, so in order to set any of its properties you’ll need a var declaration. If you’re going to want to access the user’s selected items directly from the photo library yourself as a PHAsset (as I’ll explain later in this chapter), you’ll need to initialize the configuration with a reference to the photo library:

var config = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())

But in general you won’t need to do that; my examples in this section don’t. Just initialize the configuration using init:

var config = PHPickerConfiguration()

The configuration has just two settable properties:

selectionLimit

How many items the user is permitted to select simultaneously. The default is 1. Specify a higher number to permit limited multiple selection, or 0 to mean unlimited multiple selection.

filter

What types of item the user will see in the picker view. Your choices (PHPickerFilter) are .images, .videos, and .livePhotos — or, to specify a combination of those, supply .any with an array of the permitted types. The default is no filter, meaning all three types will appear in the picker view.

In my tests, .livePhotos can be used positively but not negatively, because .images embraces live photos. In other words, if your filter is just .livePhotos, the user sees only live photos; but if your filter includes .images, the user will see live photos as well. (I regard this as a bug.)

Now create the picker, set its delegate, and present it:

var config = PHPickerConfiguration()
// ... set selectionLimit and filter as desired ...
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
self.present(picker, animated: true)

PHPickerViewController Delegate

When the user has finished working with the PHPickerViewController, if the user actually selected one or more items and did not cancel, the delegate (PHPickerViewControllerDelegate) will receive this message:

  • picker(_ :didFinishPicking:)

You must respond by dismissing the picker, which arrives in the first parameter. The second parameter is an array of PHPickerResult objects, one for each item the user selected; use these to learn of and retrieve the actual chosen images, videos, and live photos.

A PHPickerResult has just two properties:

assetIdentifier

A string suitable for obtaining from the photo library the PHAsset corresponding to the selected item. I’ll talk later about the photo library and PHAssets. In general you won’t need this, and it will be nil unless you initialized the PHPickerConfiguration with the photo library.

itemProvider

An NSItemProvider. This is the same class we talked about in connection with drag and drop in Chapter 10.

The idea behind the use of an NSItemProvider here is that no data has actually been handed to you; that’s a good thing, because photos can be big, and videos can be really big. Instead, the item provider is your gateway to obtain the data if you want it.

To learn what sort of data the item provider can provide, corresponding to what sort of item the user chose, look at the item provider’s registeredTypeIdentifiers, or just ask it whether it can provide data of the corresponding type (prov is the PHPickerResult’s itemProvider):

if prov.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
    // it's a video
} else if prov.canLoadObject(ofClass: PHLivePhoto.self) {
    // it's a live photo
} else if prov.canLoadObject(ofClass: UIImage.self) {
    // it's a photo
}

In that code, the order of the tests is deliberate. A live photo can be supplied in a simple UIImage representation, so if we test for images before live photos, we won’t learn that the result is a live photo.

Dealing with PHPickerViewController Results

In the PHPickerViewController delegate, you don’t have to extract the data for the user’s chosen items immediately. A PHPickerResult is a tiny lightweight object, so there is no penalty if you store it for later retrieval of the data (except that in the meantime the user might alter the photo library in a way that puts the item provider out of date).

But let’s assume that your goal is to extract the data immediately, and in particular that you want to display in your interface whatever the user chose. How would you do that?

The answer, obviously, depends on what sort of item this is. Suppose it’s a UIImage. Then we just ask the item provider to obtain the image and hand it to us. We could then, for example, proceed to display it in a UIImageView (again, prov is the PHPickerResult’s itemProvider):

prov.loadObject(ofClass: UIImage.self) { im, err in
    if let im = im as? UIImage {
        DispatchQueue.main.async {
            myImageView.image = im // or whatever
        }
    }
}

Observe that in that code I’ve stepped out to the main queue before doing anything to the interface. That’s crucial. loadObject(ofClass:) operates in the background, and calls its completion handler on a background thread; but we must talk to the interface on the main thread.

An interesting problem arises if you also want the metadata that accompanies the photo in the photo library. To get it, go back to the item provider and ask for the actual image data; then use the Image I/O framework to extract the metadata as a dictionary (import ImageIO, and see Chapter 23):

let im = UTType.image.identifier
prov.loadDataRepresentation(forTypeIdentifier: im) { data, err in
    if let data = data {
        let src = CGImageSourceCreateWithData(data as CFData, nil)!
        let d = CGImageSourceCopyPropertiesAtIndex(src,0,nil)
            as! [AnyHashable:Any]
        // ...
    }
}

The procedure for a live photo is exactly the same, except that if you want to display the live photo as a live photo, you need a PHLivePhoto:

prov.loadObject(ofClass: PHLivePhoto.self) { livePhoto, err in
    if let photo = livePhoto as? PHLivePhoto {
        DispatchQueue.main.async {
            // show the live photo
        }
    }
}

I’ll talk about how to display a live photo as a live photo later in this chapter.

Finally, what about a video? This is the hard case. In the first place, there is no class that represents a video, so we can’t call loadObject(ofClass:). And even if we could, we wouldn’t want to. The last thing we want on our hands is the entirety of a video’s data held in memory! What we want is the URL of a file on disk. That way, we can hand the URL to an AVPlayer or whatever mechanism we intend to use to display the video, as I described in Chapter 16.

Fortunately, an NSItemProvider will happily write the data directly to disk for us as it retrieves it, handing us the URL; all we have to do is call loadFileRepresentation(forTypeIdentifier:), which takes a completion handler with a URL parameter. However, the file in question is a temporary file that will be erased as soon as the completion handler ends — and we are going to need to step out to the main thread in order to display the video in our interface, so the completion handler will end while we are doing that. One solution would be to copy the file immediately to something that won’t be so temporary; but that would be time-consuming for a large video. A better approach is to step out to the main thread synchronously; that’s a rare thing to do, but it does no harm here, and it preserves the file long enough for our interface to take charge of it (and see Chapter 25 for more on this topic):

let movie = UTType.movie.identifier
prov.loadFileRepresentation(forTypeIdentifier: movie) { url, err in
    if let url = url {
        DispatchQueue.main.sync {
            // ... give the url to an AVPlayer or whatever ...
        }
    }
}

Observe that the call to loadFileRepresentation returns a Progress object that we could use to supply some additional interface if saving the video to disk is likely to take a long time.

A curious twist is that the video might be a live photo to which the Loop or Bounce effect has been applied. Implementing the looping, in that case, is up to us. We can detect this situation by examining the types registered with the item provider; the type I’m specifying is private, but I don’t know how else to learn that the video is supposed to loop:

let loopType = "com.apple.private.auto-loop-gif"
if prov.hasItemConformingToTypeIdentifier(loopType) { // it loops

The video can then be displayed from its URL using an AVPlayerLooper object (mentioned in Chapter 16). Start with an AVQueuePlayer rather than an AVPlayer, configure the AVPlayerLooper and retain it in an instance property, and use the AVQueuePlayer to show the video:

let av = AVPlayerViewController()
let player = AVQueuePlayer(url:url)
av.player = player
self.looper = AVPlayerLooper(
    player: player, templateItem: player.currentItem!)
// ... and so on ...

Photos Framework

The Photos framework (import Photos), also known as PhotoKit, does for the photo library roughly what the Media Player framework does for the music library (Chapter 17), letting your code explore the library’s contents — and then some. You can manipulate albums, add photos, and even perform edits on the user’s photos.

The photo library itself is represented by the PHPhotoLibrary class, and by its shared instance, which you can obtain through the shared method; you do not need to retain the shared photo library instance. Then there are the classes representing the kinds of things that inhabit the library (the photo entities):

PHAsset

A single photo or video file.

PHCollection

An abstract class representing collections of all kinds. Its concrete subclasses are:

PHAssetCollection

A collection of photos. Albums and smart albums are PHAssetCollections.

PHCollectionList

A collection of asset collections. A folder of albums, or a smart folder, is a collection list.

Finer typological distinctions are drawn, not through subclasses, but through a system of types and subtypes, which are properties:

  • A PHAsset has mediaType and mediaSubtypes properties.

  • A PHAssetCollection has assetCollectionType and assetCollectionSubtype properties.

  • A PHCollectionList has collectionListType and collectionListSubtype properties.

So a PHAsset might have a type of .image and a subtype of .photoPanorama; a PHAssetCollection might have a type of .album and a subtype of .albumRegular; and so on. Smart albums on the user’s device help draw further distinctions: a PHAssetCollection with a type of .smartAlbum and a subtype of .smartAlbumPanoramas contains all the user’s panorama photos. A PHAsset’s playbackStyle draws the distinction between a still image and an animated GIF, and between a video and a looped or bounced live photo.

The photo entity classes are actually all subclasses of PHObject, an abstract class that endows them with a localIdentifier property that functions as a persistent unique identifier. (For a PHAsset, this is the same identifier that a PHPickerResult supplies as its assetIdentifier string.)

Access to the photo library requires user authorization. You’ll use the PHPhotoLibrary class for this. New in iOS 14, there are two types of authorization you can talk about, distinguished by the PHAccessLevel enum:

.readWrite

Full authorization for accessing the photo library. To obtain this type of authorization, you will need a key “Privacy — Photo Library Usage Description” (NSPhotoLibraryUsageDescription) in your Info.plist, whose value is some meaningful text that the system authorization request alert can use to explain why your app wants access.

.addOnly

Authorization only to write new items into the photo library. To obtain this type of authorization, you will need a key “Privacy — Photo Library Additions Usage Description” (NSPhotoLibraryAddUsageDescription) in your Info.plist, whose value is some meaningful text that the system authorization request alert can use to explain why your app wants access. This type of authorization existed in earlier versions of iOS, but there was no way to ask for it explicitly or to learn whether it had been granted; you just had to attempt to write to the library and see what happened (I gave an example of doing that in Chapter 16).

The idea here is that .addOnly access might be easier to obtain than .readWrite access, so you’ll ask for .readWrite access only if you really need it. Nevertheless, the user might be hesitant to grant you .readWrite access even if you really do need it. Therefore, new in iOS 14, the user can specify a lower level of .readWrite access. When the dialog appears asking the user whether to grant access, one of the buttons says Select Photos. If the user taps it, an interface similar to the PHPickerViewController appears, where the user can specify which photos the app can access.

Corresponding to these options, in Settings, the possible privacy choices for your app are Add Photos Only, Selected Photos, All Photos, and None. (The effect of these settings is rather confusing, depending on the order in which the user taps the buttons; I’m not going to go into details, but in my view this is an inadequate interface.)

To learn what the current authorization status is, call the PHPhotoLibrary class method authorizationStatus(for:), where the parameter (new in iOS 14) is a PHAccessLevel. In addition to the usual .notDetermined, .restricted, .denied, and .authorized, there is another possibility (new in iOS 14): .limited, corresponding to the Select Photos level of .readWrite access.

If the status is .limited, you can go ahead and access the photo library, but be aware that some types of access won’t succeed. You won’t be able to get information about any assets except those to which the user has granted explicit access and those you add yourself. You won’t be able to create albums, or access albums that the user has created. And you won’t be able to access assets or albums that are shared into the cloud.

Moreover, if the status is .limited, the system may put up the Select Photos interface again from time to time to see whether the user wants to change what photos your app can access. You can prevent that from happening by adding to your Info.plist the key “Prevent limited photos access alert” (PHPhotoLibraryPreventAutomaticLimitedAccessAlert). (This key does not prevent the user from specifying limited access in the initial authorization request alert or the Settings app.) In addition, you can put up the Select Photos interface, to ask the user to extend your access; call the PHPhotoLibrary instance method presentLimitedLibraryPicker(from:), where the parameter is your view controller.

A .limited status does not affect your ability to write into the library as a whole if you have .addOnly access. And the status in general does not affect your ability to use the PHPickerViewController! It is out of process and doesn’t require access permission from the user.

To ask the system to put up the authorization request alert if the status is .notDetermined, call the PHPhotoLibrary class method requestAuthorization(for:handler:), where the first parameter (new in iOS 14) is a PHAccessLevel. The resulting status is reported to you asynchronously in the handler: function. See “Checking for Authorization” for a discussion of authorization strategy.

In the rest of this chapter, for simplicity, I’ll assume that we have full access to the photo library.

Querying the Photo Library

When you want to know what’s in the photo library, start with the photo entity class that represents the type of entity you want to know about. It will supply class methods whose names begin with fetch; you’ll pick the class method that expresses the kind of criteria you’re starting with. So to fetch one or more PHAssets, you’ll call a PHAsset fetch method; you can fetch by local identifier, by media type, or by containing asset collection. Similarly, to fetch one or more PHAssetCollections, you’ll call a PHAssetCollection fetch method; you can fetch by identifier, by type and subtype, or by whether they contain a given PHAsset.

In addition to the various fetch method parameters, you can supply a PHFetchOptions object letting you refine the results even further. You can set its predicate to limit your request results, and its sortDescriptors to determine the results order. Its fetchLimit can limit the number of results returned, and its includeAssetSourceTypes can specify where the results should come from, such as eliminating cloud items.

What you get back from a fetch method query is not images or videos but information. A fetch method returns a list of PHObjects of the type to which you sent the fetch method originally; these refer to entities in the photo library, rather than handing you an entire file (which would be huge and might take considerable time). The list itself is expressed as a PHFetchResult, which behaves very like an array: you can ask for its count, obtain the object at a given index, look for an object within the collection, extract objects into an array, and enumerate the objects with an enumerate method.

Warning

You cannot directly enumerate a PHFetchResult with for...in in Swift, even though you can do so in Objective-C. I regard this as a bug (caused by the fact that PHFetchResult is a generic).

Let’s list all albums created by the user. An album is a PHAssetCollection, so the relevant class is PHAssetCollection:

let result = PHAssetCollection.fetchAssetCollections(
    with: .album, subtype: .albumRegular, options: nil)
let albums = result.objects(at: IndexSet(0..<result.count))
for album in albums {
    let count = album.estimatedAssetCount
    print("(album.localizedTitle!):",
        "approximately (count) photos")
}

In that code, we can learn how many assets are in each album only as its estimatedAssetCount. This is probably the right answer, but to obtain the real count, we’d have to dive one level deeper and fetch the album’s actual assets. Let’s do that: given an album, let’s list its contents. An album’s contents are its assets (photos and videos), so the relevant class is PHAsset:

let result = PHAsset.fetchAssets(in:album, options: nil)
let assets = result.objects(at: IndexSet(0..<result.count))
for asset in assets {
    print(asset.localIdentifier)
}

If the fetch method you need seems not to exist, don’t forget about PHFetchOptions. There is no PHAsset fetch method for fetching from a certain collection all assets of a certain type; you cannot specify, for instance, that you want all photos (but no videos) from the user’s Camera Roll. But you can perform such a fetch by setting a PHFetchOptions object’s predicate property. To illustrate, I’ll fetch ten ordinary photos (no videos, and no HDR photos) from the user’s Recent smart album:

let recentAlbums = PHAssetCollection.fetchAssetCollections(
    with: .smartAlbum, subtype: .smartAlbumRecentlyAdded, options: nil)
guard let rec = recentAlbums.firstObject else {return}
let options = PHFetchOptions()
let pred = NSPredicate(
    format: "mediaType == %d && !((mediaSubtype & %d) == %d)",
    PHAssetMediaType.image.rawValue,
    PHAssetMediaSubtype.photoHDR.rawValue,
    PHAssetMediaSubtype.photoHDR.rawValue)
options.predicate = pred // photos only, please, no HDRs
options.fetchLimit = 10 // let's not take all day about it
let photos = PHAsset.fetchAssets(in:rec, options: options)

Modifying the Library

Structural modifications to the photo library are performed through a change request class corresponding to the class of photo entity we wish to modify. The name of the change request class is the name of a photo entity class followed by “ChangeRequest.” For PHAsset, there’s the PHAssetChangeRequest class — and so on.

To use a change request, you’ll call a performChanges method on the shared photo library. Typically, that method will be performChanges(_:completionHandler:), which takes two functions. The first function, the changes function, is where you describe the changes you want performed; the second function, the completion function, is called back after the changes have been performed.

Each change request class comes with methods that ask for a change of some particular type. Here are some examples:

PHAssetChangeRequest

Class methods include deleteAssets(_:), creationRequestForAssetFromImage(atFileURL:), and so on.

If you’re creating an asset and what you’re starting with is raw data, call PHAssetCreationRequest.forAsset(). That gives you a PHAssetCreationRequest, a subclass of PHAssetChangeRequest that provides instance methods such as addResource(with:data:options:).

PHAssetCollectionChangeRequest

Class methods include deleteAssetCollections(_:) and creationRequestForAssetCollection(withTitle:).

In addition, there are initializers like init(for:), which takes an asset collection, along with instance methods addAssets(_:), removeAssets(_:), and so on.

A creationRequest class method also returns an instance of the change request class. You can throw this away if you don’t need it for anything. Its purpose is to let you perform further changes as part of the same batch. For example, once you have a PHAssetChangeRequest instance, you can use its properties to initialize the asset’s features, such as its creation date or its associated geographical location; those would be read-only if accessed through the PHAsset.

To illustrate, let’s create an album called “Test Album.” An album is a PHAssetCollection, so we start with the PHAssetCollectionChangeRequest class and call its creationRequestForAssetCollection(withTitle:) class method in the performChanges function. This method returns a PHAssetCollectionChangeRequest instance, but we don’t need that instance for anything, so we simply throw it away:

PHPhotoLibrary.shared().performChanges {
    let t = "TestAlbum"
    typealias Req = PHAssetCollectionChangeRequest
    Req.creationRequestForAssetCollection(withTitle:t)
}

(The class name PHAssetCollectionChangeRequest is very long, so I’ve shortened it with a type alias.)

It may appear, in that code, that we didn’t actually do anything — we asked for a creation request, but we didn’t tell it to do any creating. Nevertheless, that code is sufficient; generating the creation request for a new asset collection in the performChanges function constitutes an instruction to create an asset collection.

All the same, that code is rather silly. The album was created asynchronously, so to use it, we need a completion function (see Appendix C). Moreover, we’re left with no reference to the album we created. For that, we need a PHObjectPlaceholder. This minimal PHObject subclass has just one property — localIdentifier, which it inherits from PHObject. That’s sufficient to permit a reference to the created object to survive into the completion function, where we can do something useful with it, such as saving it off to an instance property:

var ph : PHObjectPlaceholder?
PHPhotoLibrary.shared().performChanges {
    let t = "TestAlbum"
    typealias Req = PHAssetCollectionChangeRequest
    let cr = Req.creationRequestForAssetCollection(withTitle:t)
    ph = cr.placeholderForCreatedAssetCollection
} completionHandler: { ok, err in
    if ok, let ph = ph {
        self.newAlbumId = ph.localIdentifier
    }
}

Now suppose we subsequently want to populate our newly created album. Let’s say we want to make the first asset in the user’s Recently Added smart album a member of our new album as well. No problem! First, we need a reference to the Recently Added album; then we need a reference to its first asset; and finally, we need a reference to our newly created album (whose identifier we’ve already captured as self.newAlbumId). Those are all basic fetch requests, which we can perform in succession, and we then use their results to form the change request:

// find Recently Added smart album
let result = PHAssetCollection.fetchAssetCollections(
    with: .smartAlbum, subtype: .smartAlbumRecentlyAdded, options: nil)
guard let rec = result.firstObject else { return }
// find its first asset
let result2 = PHAsset.fetchAssets(in:rec, options: nil)
guard let asset1 = result2.firstObject else { return }
// find our newly created album by its local id
let result3 = PHAssetCollection.fetchAssetCollections(
    withLocalIdentifiers: [self.newAlbumId], options: nil)
guard let alb2 = result3.firstObject else { return }
// ready to perform the change request
PHPhotoLibrary.shared().performChanges {
    typealias Req = PHAssetCollectionChangeRequest
    let cr = Req(for: alb2)
    cr?.addAssets([asset1] as NSArray)
}

A PHObjectPlaceholder has a further use. What if we created, say, an asset collection and wanted to add it to something (presumably to a PHCollectionList), all in one batch request? Requesting the creation of an asset collection gives us a PHAssetCollectionChangeRequest instance; you can’t add that to a collection. And the requested PHAssetCollection itself hasn’t been created yet! The solution is to obtain a PHObjectPlaceholder. Because it is a PHObject, it can be used as the argument of change request methods such as addChildCollections(_:).

Being Notified of Changes

When the library is modified, whether by your code or by some other means while your app is running, any information you’ve collected about the library — information which you may even be displaying in your interface at that very moment — may become out of date. To cope with this possibility, you should, perhaps very early in the life of your app, register a change observer (adopting the PHPhotoLibraryChangeObserver protocol) with the photo library:

PHPhotoLibrary.shared().register(self)

The outcome is that, whenever the library changes, the observer’s photoLibraryDidChange(_:) method is called, with a PHChange object encapsulating a description of the change. The observer can then probe the PHChange object by calling changeDetails(for:). The idea is that if you’re hanging on to information in an instance property, you can use what the PHChange object tells you to modify that information (and possibly your interface). The parameter can be one of two types:

A PHObject

The parameter is a single PHAsset, PHAssetCollection, or PHCollectionList you’re interested in. The result is a PHObjectChangeDetails object, with properties like objectBeforeChanges, objectAfterChanges, and objectWasDeleted.

A PHFetchResult

The result is a PHFetchResultChangeDetails object, with properties like fetchResultBeforeChanges, fetchResultAfterChanges, removedObjects, insertedObjects, and so on.

Suppose my interface is displaying a list of album names, which I obtained originally through a PHAssetCollection fetch request. And suppose that, at the time that I performed the fetch request, I also retained as an instance property (self.albums) the PHFetchResult that it returned. Then if my photoLibraryDidChange(_:) method is called, I can update the fetch result and change my interface accordingly:

func photoLibraryDidChange(_ changeInfo: PHChange) {
    if self.albums !== nil {
        let details = changeInfo.changeDetails(for:self.albums)
        if details !== nil {
            self.albums = details!.fetchResultAfterChanges
            // ... and adjust interface if needed ...
        }
    }
}

New in iOS 14, photoLibraryDidChange(_:) is called when your app has .limited access and the user changes the set of photos to which your app has access in the Select Photos interface. The reason is that, as far as your app is concerned, it is as if the user had altered the contents of the photo library.

Fetching Images

Sooner or later, you’ll probably want to go beyond information about the structure of the photo library and fetch an actual photo or video for display in your app. The process of obtaining an image can be time-consuming: not only may the image data be large, but also it may be stored in the cloud. Therefore you will typically supply a completion function that can be called back asynchronously with the data (see Appendix C).

To obtain an image, you’ll need an image manager, which you’ll get by calling the PHImageManager default class method. You then call a method whose name starts with request, supplying a completion function. For an image, you can ask for a UIImage or the original data; for a video, you can ask for an AVPlayerItem or an AVAsset configured for display of the video, or an AVAssetExportSession suitable for exporting the video to a new file (see Chapter 16). The result comes back to you as a parameter passed into your completion function.

Asking for a UIImage

If you’re asking for a UIImage, information about the image may increase in accuracy and detail in the course of time — with the curious consequence that your completion function may be called multiple times. The idea is to give you some image to display as fast as possible, with better versions of the image arriving later. If you would rather receive just one version of the image, you can specify that through a PHImageRequestOptions object (as I’ll explain in a moment).

The various request methods take parameters letting you refine the details of the data-retrieval process. When asking for a UIImage, you supply these parameters:

targetSize:

The size of the desired image. It is a waste of memory to ask for an image larger than you need for actual display, and a larger image may take longer to supply (and a photo, remember, is a very large image). The image retrieval process performs the desired downsizing so that you don’t have to. For the largest possible size, pass PHImageManagerMaximumSize.

contentMode:

A PHImageContentMode, either .aspectFit or .aspectFill, with respect to your targetSize. With .aspectFill, the image retrieval process does any needed cropping so that you don’t have to.

options:

A PHImageRequestOptions object. This is a value class representing a grab-bag of additional tweaks, such as:

.version

Do you want the original image (.original) or the edited image (.current)?

.resizeMode

Do you want the image sized exactly to your targetSize (.exact), or will you accept a larger version (.fast)?

.normalizedCropRect

Do you want custom cropping?

.isNetworkAccessAllowed, .progressHandler

Do you want the image fetched over the network if necessary, and if so, do you want to install a progress callback function?

.deliveryMode

Do you want one call to your completion function, or many (.opportunistic)? If one, do you want a degraded thumbnail which will arrive quickly (.fastFormat), or the best possible quality which may take some considerable time (.highQualityFormat)?

.isSynchronous

Do you want the image fetched synchronously? If you do, you will get only one call to your completion function — but then you must make your call on a background thread, and the image will arrive on that same background thread (see Chapter 25).

In this simple example, I have a view controller called DataViewController, good for displaying one photo in an image view (self.iv). It has a PHAsset property, self.asset, which is assumed to have been set when this view controller instance was created. In viewDidLoad, I call my setUpInterface utility method to populate the interface:

func setUpInterface() {
    guard let asset = self.asset else { return }
    let opts = PHImageRequestOptions()
    opts.resizeMode = .exact
    PHImageManager.default().requestImage(for: asset,
        targetSize: CGSize(300,300), contentMode: .aspectFit,
        options: opts) { im, info in
            if let im = im {
                self.iv.image = im
            }
    }
}

This may result in the image view’s image being set multiple times, as the requested image is supplied repeatedly, with its quality improving each time; but there is nothing wrong with that. Using this technique with a UIPageViewController, you can easily write an app that allows the user to browse photos one at a time.

The second parameter in an image request’s completion function is a dictionary whose elements may be useful in certain circumstances. Among the keys are:

PHImageResultRequestIDKey

Uniquely identifies a single image request for which this result function is being called multiple times. This value is also returned by the original request method call (I didn’t bother to capture it in the previous example). You can also use this identifier to call cancelImageRequest(_:) if it turns out that you don’t need this image after all.

PHImageCancelledKey

Reports that an attempt to cancel an image request with cancelImageRequest(_:) succeeded.

PHImageResultIsInCloudKey

Warns that the image is in the cloud and that your request must be resubmitted with the PHImageRequestOptions isNetworkAccessAllowed property set to true.

Canceling and caching

If your interface is a table view or collection view, the asynchronous, time-consuming nature of image fetching is clearly significant. As the user scrolls, a cell comes into view and you request the corresponding image. But as the user keeps scrolling, that cell goes out of view, and now the requested image, if it hasn’t arrived, is no longer needed, so you cancel the request. (I’ll tackle the same sort of problem with regard to internet-based images in a table view in Chapter 24.)

There is also a PHImageManager subclass, PHCachingImageManager, that can help do the opposite: you can prefetch some images before the user scrolls to view them, improving response time. For an example that displays photos in a UICollectionView, look at Apple’s PhotoBrowsing sample code (available from the “Browsing and Modifying Photo Albums” help document). It uses the PHImageManager class to fetch individual photos; but for the UICollectionViewCell thumbnails, it uses PHCachingImageManager.

Live photos, videos, and data

If a PHAsset represents a live photo, you can call the PHImageManager requestLivePhoto method, parallel to requestImage; what you get in the completion function is a PHLivePhoto. Here, I’ll fetch a live photo (from a PHAsset called asset), and I’ll display it in the interface; this illustrates the use of a PHLivePhotoView, which displays a live photo as a live photo:

let opts = PHLivePhotoRequestOptions()
opts.deliveryMode = .highQualityFormat
PHImageManager.default().requestLivePhoto(
    for: asset, targetSize: CGSize(300,300),
    contentMode: .aspectFit, options: opts) { photo, info in
        let v = PHLivePhotoView(frame: CGRect(20,20,300,300))
        v.contentMode = .scaleAspectFit
        v.livePhoto = photo
        self.view.addSubview(v)
}

Fetching a video resource is far simpler, and there’s little to say about it. In this example, I fetch a reference to the first video in the user’s photo library and display it in the interface (using an AVPlayerViewController); unlike an image, I am not guaranteed that the result will arrive on the main thread, so I must step out to the main thread before interacting with the app’s user interface:

func fetchMovie() {
    let opts = PHFetchOptions()
    opts.fetchLimit = 1
    let result = PHAsset.fetchAssets(with: .video, options: opts)
    guard let asset = result.firstObject else {return}
    PHImageManager.default().requestPlayerItem(
        forVideo: asset, options: nil) { item, info in
            if let item = item {
                DispatchQueue.main.async {
                    self.display(item:item)
                }
            }
    }
}
func display(item:AVPlayerItem) {
    let player = AVPlayer(playerItem: item)
    let vc = AVPlayerViewController()
    vc.player = player
    vc.view.frame = self.v.bounds
    self.addChild(vc)
    self.v.addSubview(vc.view)
    vc.didMove(toParent: self)
}

You can also access an asset’s various kinds of data directly through the PHAssetResourceManager class. The request method takes a PHAssetResource object based on a PHAsset or PHLivePhoto. You can retrieve an image’s RAW and JPEG data separately. For a list of the data types we’re talking about here, see the documentation on the PHAssetResourceType enum.

Editing Images

Astonishingly, PhotoKit allows you to change an image in the user’s photo library. Why is this even legal? There are two reasons:

  • The user will have to give permission every time your app proposes to modify a photo in the library, and will be shown the proposed modification beforehand.

  • Changes to library photos are undoable, because the original image remains in the database along with the changed image that the user sees, and the user can revert to that original at any time.

How to change a photo image

To change a photo image is a three-step process:

  1. You send a PHAsset this message:

    • requestContentEditingInput(with:completionHandler:)

    Your completion function is called, and is handed a PHContentEditingInput object. This object wraps some image data that you can display to the user (displaySizeImage), along with a pointer to the real image data as a file (fullSizeImageURL).

  2. You create a PHContentEditingOutput object by calling its initializer:

    • init(contentEditingInput:)

    The argument is the PHContentEditingInput object. The PHContentEditingOutput object has a renderedContentURL property, representing a file URL. Your mission is to write the edited photo image data to that URL. What you’ll typically do is:

    1. Fetch the image data from the PHContentEditingInput object’s fullSizeImageURL.

    2. Process the image.

    3. Write the resulting image data to the PHContentEditingOutput object’s renderedContentURL.

  3. You notify the photo library that it should pick up the edited version of the photo. To do so, you call performChanges(_:completionHandler:) and, inside the changes function, create a PHAssetChangeRequest and set its contentEditingOutput property to the PHContentEditingOutput object. The user will now be shown the alert requesting permission to modify this photo; your completion function is then called, with a first parameter of false if the user refuses (or if anything else goes wrong).

Handling the adjustment data

So far, so good. However, if you do only what I have just described, your attempt to modify the photo will fail. The reason is that I have omitted something: before the third step, you must set the PHContentEditingOutput object’s adjustmentData property to a newly instantiated PHAdjustmentData object. The initializer is:

  • init(formatIdentifier:formatVersion:data:)

What goes into the initializer parameters is completely up to you, but the goal is to store with the photo a message to your future self in case you are called upon to edit the same photo again on some later occasion. In that message, you describe to yourself how you edited the photo on this occasion.

Your handling of the adjustment data works in three steps, interwoven with the three steps I already outlined. As you start to edit the photo, first you say whether you can read its existing PHAdjustmentData, and then you do read its existing PHAdjustmentData and use it as part of your editing; when you have finished editing the photo, you make a new PHAdjustmentData, ready for the next time you edit this same photo:

  1. When you call requestContentEditingInput, the first argument (with:) should be a PHContentEditingInputRequestOptions object. You have created this object and set its canHandleAdjustmentData property to a function that takes a PHAdjustmentData and returns a Bool. This Bool will be based mostly on whether you recognize this photo’s PHAdjustmentData as yours — typically because you recognize its formatIdentifier. That determines what image you’ll get when you receive your PHContentEditingInput object:

    Your canHandleAdjustmentData function returns false

    The image you’ll be editing is the edited image displayed in the Photos app.

    Your canHandleAdjustmentData function returns true

    The image you’ll be editing is the original image, stripped of your edits. This is because, by returning true, you are asserting that you can reconstruct your edits based on what’s in the PHAdjustmentData’s data.

  2. When your completion function is called and you receive your PHContentEditingInput object, it has (you guessed it) an adjustmentData property, which is an Optional wrapping a PHAdjustmentData object. If this isn’t nil, and if you edited this image previously, its data is the data you put in the last time you edited this image, and you are expected to extract it and use it to recreate the edited state of the image.

  3. After editing the image, when you prepare the PHContentEditingOutput object, you give it a new PHAdjustmentData object whose data summarizes the new edited state of the photo from your point of view — and so the whole cycle can start again if the same photo is to be edited again later.

Example: Before editing

An actual implementation is quite straightforward and almost pure boilerplate. The details will vary only in regard to the actual editing of the photo and the particular form of the data by which you’ll summarize that editing — so, in constructing an example, I’ll keep that part very simple. Recall my example of a custom “vignette” CIFilter called MyVignetteFilter (“CIFilter and CIImage”). I’ll provide an interface whereby the user can apply that filter to a photo. My interface will include:

  • A slider that allows the user to set the degree of vignetting that should be applied (MyVignetteFilter’s inputPercentage).

  • A button that lets the user remove all vignetting from the photo, even if that vignetting was applied in a previous editing session.

First, I’ll plan the structure of the PHAdjustmentData:

formatIdentifier

This can be any unique string; I’ll use "com.neuburg.matt.PhotoKitImages.vignette", a constant that I’ll store in a property (self.myidentifier).

formatVersion

This is likewise arbitrary; I’ll use "1.0".

data

This will express the only thing about my editing that is adjustable — the inputPercentage. The data will wrap an NSNumber which itself wraps a Double whose value is the inputPercentage.

As editing begins, I construct the PHContentEditingInputRequestOptions object that determines whether a photo’s most recent editing belongs to me. Then, starting with the photo that is to be edited (a PHAsset), I ask for the PHContentEditingInput object:

let options = PHContentEditingInputRequestOptions()
options.canHandleAdjustmentData = { adjustmentData in
    return adjustmentData.formatIdentifier == self.myidentifier
}
var id : PHContentEditingInputRequestID = 0
id = self.asset.requestContentEditingInput(with: options) { input, info in
    // ...
}

In the completion function, I receive my PHContentEditingInput object as a parameter (input). I’m going to need this object later when editing ends, so I immediately store it in a property. I then unwrap its adjustmentData, extract the data, and construct the editing interface; in this case, that happens to be a presented view controller, but the details are irrelevant and are omitted here:

guard let input = input else {
    self.asset.cancelContentEditingInputRequest(id)
    return
}
self.input = input
let im = input.displaySizeImage! // show this to user during editing
if let adj = input.adjustmentData,
adj.formatIdentifier == self.myidentifier {
    if let vigNumber = try? NSKeyedUnarchiver.unarchivedObject(
        ofClass: NSNumber.self, from: adj.data),
    let vigAmount = vigNumber as? Double {
            // ... store vigAmount ...
    }
}
// ... present editing interface, passing it the vigAmount ...

The important thing about that code is how we deal with the adjustmentData and its data. The question is whether we have data, and whether we recognize this as our data from some previous edit on this image. This will affect how our editing interface needs to behave. There are two possibilities:

It’s our data

If we were able to extract a vigAmount from the adjustmentData, then the displaySizeImage is the original, unvignetted image. Therefore, our editing interface initially applies the vigAmount of vignetting to this image — reconstructing the vignetted state of the photo as shown in the Photos app, while allowing the user to change the amount of vignetting, or even to remove all vignetting entirely.

It’s not our data

On the other hand, if we weren’t able to extract a vigAmount from the adjustmentData, then there is nothing to reconstruct; the displaySizeImage is the actual photo image from the Photos app, and our editing interface will apply vignetting to it directly.

Example: After editing

Let’s skip ahead now to the point where the user’s interaction with our editing interface comes to an end. If the user cancelled, that’s all; the user doesn’t want to modify the photo after all. Otherwise, the user either asked to apply a certain amount of vignetting (vignette) or asked to remove all vignetting; in the latter case, I use an arbitrary vignette value of -1 as a signal.

Up to now, our editing interface has been using the displaySizeImage to show the user a preview of what the edited photo would look like. Now the time has come to perform the vignetting that the user is asking us to perform — that is, we must apply this amount of vignetting to the real photo image, which has been sitting waiting for us all this time, untouched, at the PHContentEditingInput’s fullSizeImageURL. This is a big image, which will take significant time to load, to alter, and to save (which is why we haven’t been working with it in the editing interface).

So, depending on the value of vignette requested by the user, I either pass the input image from the fullSizeImageURL through my vignette filter or I don’t:

let inurl = self.input.fullSizeImageURL!
let output = PHContentEditingOutput(contentEditingInput:self.input)
let outurl = output.renderedContentURL
var ci = CIImage(contentsOf: inurl, options: [.applyOrientationProperty:true])!
let space = ci.colorSpace!
if vignette >= 0.0 {
    let vig = MyVignetteFilter()
    vig.setValue(ci, forKey: "inputImage")
    vig.setValue(vignette, forKey: "inputPercentage")
    ci = vig.outputImage!
}

Don’t forget about setting the PHContentEditingOutput’s adjustmentData! My goal here is to send a message to myself, in case I am asked later to edit this same image again, stating what amount of vignetting is already applied to the image. That amount is represented by vignette — so that’s the value I store in the adjustmentData:

let data = try! NSKeyedArchiver.archivedData(
    withRootObject: vignette, requiringSecureCoding: true)
output.adjustmentData = PHAdjustmentData(
    formatIdentifier: self.myidentifier, formatVersion: "1.0", data: data)

Finally, I must write a JPEG to the PHContentEditingOutput’s renderedContentURL:

try! CIContext().writeJPEGRepresentation(of:ci, to:outurl, colorSpace:space)

Some of that code is time-consuming, particularly where I read and write the data with these methods:

  • CIImage init(contentsOf:options:)

  • CIContext writeJPEGRepresentation(of:to:colorSpace:)

So in real life I call it on a background thread (Chapter 25), and I also show a UIActivityIndicatorView to let the user know that work is being done.

We conclude by telling the photo library to retrieve the edited image. This will cause the alert to appear, asking the user whether to allow us to modify this photo. If the user taps Modify, the modification is made, and if we are displaying the image, we should get onto the main thread and redisplay it:

PHPhotoLibrary.shared().performChanges {
    typealias Req = PHAssetChangeRequest
    let req = Req(for: self.asset)
    req.contentEditingOutput = output // triggers alert
} completionHandler: { ok, err in
    if ok {
        // if we are displaying the image, redisplay it on the main thread
    } else {
        // user refused to allow modification, do nothing
    }
}

You can also edit a live photo, using a PHLivePhotoEditingContext: you are handed each frame of the video as a CIImage, making it easy to apply a CIFilter. For a demonstration, see Apple’s Photo Edit sample app (also known as Sample Photo Editing Extension).

Photo Editing Extension

A photo editing extension is photo-modifying code supplied by your app that is effectively injected into the Photos app. When the user edits a photo from within the Photos app, your extension appears as an option and can modify the photo being edited.

To make a photo editing extension, create a new target in your app, specifying iOS → Application Extension → Photo Editing Extension. The template supplies a storyboard containing one scene, along with the code file for a corresponding UIViewController subclass. This file imports not only the Photos framework but also the Photos UI framework, which supplies the PHContentEditingController protocol, to which the view controller conforms. This protocol specifies the methods through which the runtime will communicate with your extension’s code.

A photo editing extension works almost exactly the same way as modifying photo library assets in general, as I described in the preceding section. The chief differences are:

  • You don’t put a Done or a Cancel button into your editing interface. The Photos app will wrap your editing interface in its own interface, providing those buttons when it presents your view.

  • You must situate the pieces of your code so as to respond to the calls that will come through the PHContentEditingController methods.

The PHContentEditingController methods are:

canHandle(_:)

You will not be instantiating PHContentEditingInput; the runtime will do it for you. Therefore, instead of configuring a PHContentEditingInputRequestOptions object and setting its canHandleAdjustmentData, you implement this method; you’ll receive the PHAdjustmentData and return a Bool.

startContentEditing(with:placeholderImage:)

The runtime has obtained the PHContentEditingInput object for you. Now it supplies that object to you, along with a very temporary initial version of the image to be displayed in your interface; you are expected to replace this with the PHContentEditingInput object’s displaySizeImage. Just as in the previous section’s code, you should retain the PHContentEditingInput object in a property, as you will need it again later.

cancelContentEditing

The user tapped Cancel. You may well have nothing to do here.

finishContentEditing(completionHandler:)

The user tapped Done. In your implementation, you get onto a background thread (the template configures this for you) and do exactly the same thing you would do if this were not a photo editing extension — get the PHContentEditingOutput object and set its adjustmentData; get the photo from the PHContentEditingInput object’s fullSizeImageURL, modify it, and save the modified image as a full-quality JPEG at the PHContentEditingOutput object’s renderedContentURL. When you’re done, don’t notify the PHPhotoLibrary; instead, call the completionHandler that arrived as a parameter, handing it the PHContentEditingOutput object.

During the time-consuming part of this method, the Photos app puts up a UIActivityIndicatorView, just as I suggested you might want to do in your own app. When you call the completionHandler, there is no alert asking the user to confirm the modification of the photo; the user is already in the Photos app and has explicitly asked to edit the photo, so no confirmation is needed.

Using the Camera

Use of the camera requires user authorization. You’ll use the AVCaptureDevice class for this (part of the AV Foundation framework; import AVFoundation). To learn what the current authorization status is, call the class method authorizationStatus(forMediaType:). To ask the system to put up the authorization request alert if the status is .notDetermined, call the class method requestAccess(forMediaType:completionHandler:). The media type (AVMediaType) will be .video; this embraces capturing both still photos and movies. Your app’s Info.plist must contain some meaningful text that the system authorization request alert can use to explain why your app wants camera use; the relevant key is “Privacy — Camera Usage Description” (NSCameraUsageDescription).

If your app will let the user capture movies (as opposed to still photos), you will also need to obtain permission from the user to access the microphone. The same methods apply, but with argument .audio. Your app’s Info.plist must contain some explanatory text under the “Privacy — Microphone Usage Description” key (NSMicrophoneUsageDescription). See “Checking for Authorization” for discussion of authorization strategy.

Having included the relevant Info.plist keys, it is quite reasonable not to ask explicitly for authorization, as the system will put up the appropriate authorization dialog on your behalf when the user attempts to capture a photo or a video. But you should still check the authorization status, because there is no point providing the user with any camera interface at all if you have already been denied authorization.

Warning

Use of the camera is greatly curtailed, and is interruptible, under iPad multitasking. Watch WWDC 2015 video 211 for details.

Capture with UIImagePickerController

The simplest way to prompt the user to take a photo or video is with the UIImagePickerController class. It provides an interface that is effectively a limited subset of the Camera app. (In iOS 13 and before, UIImagePickerController was used also to let the user browse the photo library; now that iOS 14 provides the PHPickerViewController, the camera aspect of the UIImagePickerController will be the only aspect you’ll use.)

Here’s how to prepare:

  1. Check isSourceTypeAvailable(_:) for .camera; if it is false, the user’s device has no camera or the camera is unavailable, so stop at this point.

  2. Call availableMediaTypes(for:.camera) to learn whether the user can take a still photo (UTType.image.identifier), a video (UTType.movie.identifier), or both.

  3. Instantiate UIImagePickerController, set its source type to .camera, and set its mediaTypes in accordance with which types you just learned are available. If your setting is an array of both types, the user will see a Camera-like interface allowing a choice of either one.

  4. Set a delegate, adopting UINavigationControllerDelegate and UIImagePickerControllerDelegate.

Now you can present the picker:

let src = UIImagePickerController.SourceType.camera
guard UIImagePickerController.isSourceTypeAvailable(src)
    else {return}
guard = UIImagePickerController.availableMediaTypes(for:src) != nil
    else {return}
let picker = UIImagePickerController()
picker.sourceType = src
picker.mediaTypes = arr
picker.delegate = self
self.present(picker, animated: true)

For video, you can also specify the videoQuality and videoMaximumDuration. Moreover, these additional properties and class methods allow you to discover the camera capabilities:

isCameraDeviceAvailable:

Checks to see whether the front or rear camera is available, using one of these values as argument (UIImagePickerController.CameraDevice):

  • .front

  • .rear

cameraDevice

Lets you learn and set which camera is being used.

availableCaptureModes(for:)

Checks whether the given camera can capture still photos, video, or both. You specify the front or rear camera; returns an array of integers. Possible modes are (UIImagePickerController.CameraCaptureMode):

  • .photo

  • .video

cameraCaptureMode

Lets you learn and set the capture mode (still photo or video).

isFlashAvailable(for:)

Checks whether flash is available.

cameraFlashMode

Lets you learn and set the flash mode (or, for a movie, toggles the LED “torch”). Your choices are (UIImagePickerController.CameraFlashMode):

  • .off

  • .auto

  • .on

When the view controller’s view appears, the user will see the interface for taking a picture, familiar from the Camera app, possibly including flash options, camera selection button, photo/video option (if your mediaTypes setting allows both), and Cancel and shutter buttons. If the user takes a picture, the presented view offers an opportunity to use the picture or to retake it. If you have set the picker’s allowsEditing to true, the user will have an opportunity to crop a still photo.

In the delegate, implement these two methods:

imagePickerControllerDidCancel(_:)

Dismiss the picker.

imagePickerController(_:didFinishPickingMediaWithInfo:)

Obtain what the user captured and dismiss the picker. The info: is a dictionary whose keys are of type UIImagePickerController.InfoKey. You’ll be interested in the following keys (any of which might be nil):

.mediaType

A string, either UTType.image.identifier or UTType.movie.identifier, telling you whether the user captured a still photo or a video.

.originalImage

A UIImage. If the user captured a still image, this is it. However, you should also check whether the .editedImage is not nil; if it is an actual UIImage, because you set allowsEditing to true and the user did in fact crop the image, you should use the .editedImage instead of the .originalImage.

.mediaMetadata

An NSDictionary containing the metadata for a captured image.

.mediaURL

If the user captured a video, the file URL where it has been saved.

The photo library was not involved in the process of media capture, so no user permission to access the photo library is needed. But if you now propose to save the media into the photo library, you will need permission. Suppose that the user takes a still image, and you now want to save it into the user’s Camera Roll album. Creating the PHAsset is sufficient:

func imagePickerController(_ picker: UIImagePickerController,
    didFinishPickingMediaWithInfo
    info: [UIImagePickerController.InfoKey : Any]) {
        var im = info[.originalImage] as? UIImage
        if let ed = info[.editedImage] as? UIImage {
            im = ed
        }
        let m = info[.mediaMetadata] as? NSDictionary
        self.dismiss(animated:true) {
            let mediatype = info[.mediaType]
            guard let type = mediatype as? String else {return}
            switch type {
            case UTType.image.identifier:
                if im != nil {
                    checkForPhotoLibraryAccess {
                        let lib = PHPhotoLibrary.shared()
                        lib.performChanges {
                            typealias Req = PHAssetChangeRequest
                            Req.creationRequestForAsset(from: im!)
                        }
                    }
                }
            default:break
            }
        }
}

In that code, the metadata associated with the photo is received (m), but nothing is done with it, and it is not folded into the PHAsset created from the image (im). To attach the metadata to the photo, use the Image I/O framework (import ImageIO) to make a copy of the image data along with the metadata. Now you can use a PHAssetCreationRequest to make the PHAsset from the data:

let jpeg = im!.jpegData(compressionQuality:1)
let src = CGImageSourceCreateWithData(jpeg as CFData, nil)!
let data = NSMutableData()
let uti = CGImageSourceGetType(src)!
let dest = CGImageDestinationCreateWithData(
    data as CFMutableData, uti, 1, nil)!
CGImageDestinationAddImageFromSource(dest, src, 0, m)
CGImageDestinationFinalize(dest)
let lib = PHPhotoLibrary.shared()
lib.performChanges {
    let req = PHAssetCreationRequest.forAsset()
    req.addResource(with: .photo, data: data as Data, options: nil)
}

You can customize the UIImagePickerController image capture interface. If you need to do that, you should probably consider dispensing entirely with UIImagePickerController and instead designing your own image capture interface from scratch, based around AV Foundation and AVCaptureSession, which I’ll introduce in the next section. Still, it may be that a modified UIImagePickerController is all you need.

In the image capture interface, you can hide the standard controls by setting showsCameraControls to false, replacing them with your own overlay view, which you supply as the value of the cameraOverlayView. That removes the shutter button, so you’re probably going to want to provide some new means of allowing the user to take a picture! You can do that through these methods:

  • takePicture

  • startVideoCapture

  • stopVideoCapture

The UIImagePickerController is a UINavigationController, so if you need additional interface — possibly to let the user vet the captured picture before dismissing the picker — you can push it onto the navigation interface.

Capture with AV Foundation

Instead of using UIImagePickerController, you can control the camera directly using the AV Foundation framework (Chapter 16). You get no help with interface, but you get vastly more power than UIImagePickerController can give you. For stills, you can control focus and exposure directly and independently, and for video, you can determine the quality, size, and frame rate of the resulting movie.

To understand how AV Foundation classes are used for image capture, imagine how the Camera app works. When you are running the Camera app, you have, at all times, a “window on the world” — the screen is showing you what the camera sees. At some point, you might tap the button to take a still image or start taking a video; now what the camera sees goes into a file.

Think of all that as being controlled by an engine. This engine, the heart of all AV Foundation capture operations, is an AVCaptureSession object. It has inputs (such as a camera) and outputs (such as a file). It also has an associated layer in your interface. When you start the engine running, by calling startRunning, data flows from the input through the engine; that is how you get your “window on the world,” displaying on the screen what the camera sees.

As a rock-bottom example, let’s start by implementing just the “window on the world” part of the engine. Our AVCaptureSession is retained in an instance property (self.sess). We also need a special CALayer that will display what the camera is seeing — namely, an AVCaptureVideoPreviewLayer. This layer is not really an AVCaptureSession output; rather, the layer receives its imagery by association with the AVCaptureSession. Our capture session’s input is the default camera. We have no intention, as yet, of capturing anything to a file, so no output is needed:

self.sess = AVCaptureSession()
guard let cam = AVCaptureDevice.default(for: .video),
    let input = try? AVCaptureDeviceInput(device:cam)
    else {return}
self.sess.addInput(input)
let lay = AVCaptureVideoPreviewLayer(session:self.sess)
lay.frame = // ... some reasonable frame ...
self.view.layer.addSublayer(lay)
self.sess.startRunning()

Presto! Our interface now displays a “window on the world,” showing what the camera sees.

Suppose now that our intention is that, while the engine is running and the “window on the world” is showing, the user is to be allowed to tap a button that will capture a still photo. Now we do need an output for our AVCaptureSession. This will be an AVCapturePhotoOutput instance. We should also configure the session with a preset (AVCaptureSession.Preset) to match our intended use of it; in this case, the preset will be .photo.

So let’s modify the preceding code to give the session an output and a preset. We can do this directly before we start the session running. We can also do it while the session is already running (and in general, if you want to reconfigure a running session, doing so while it is running is far more efficient than stopping the session and starting it again), but then we must wrap our configuration changes in beginConfiguration and commitConfiguration. We should also run this sort of code on a dedicated background dispatch queue (see Chapter 25), but I’m not bothering to show that here:

self.sess.beginConfiguration()
guard self.sess.canSetSessionPreset(self.sess.sessionPreset)
    else {return}
self.sess.sessionPreset = .photo
let output = AVCapturePhotoOutput()
guard self.sess.canAddOutput(output)
    else {return}
self.sess.addOutput(output)
self.sess.commitConfiguration()

The session is now running and is ready to capture a photo. The user taps the button that asks to capture a photo, and we respond by telling the session’s photo output to capturePhoto(with:delegate:). To prepare for that, we’re going to need a reference to that output, which we can obtain from the session. Also, the first parameter is an AVCapturePhotoSettings object, which we should create and configure beforehand. So we start out like this:

guard let output = self.sess.outputs[0] as? AVCapturePhotoOutput
    else {return}
let settings = AVCapturePhotoSettings()

What sort of configuration might our AVCapturePhotoSettings need? Well, if we intend to display the user’s captured photo in our interface, we should request a preview image explicitly. It’s a lot more efficient for AV Foundation to create an uncompressed preview image of the correct size than for us to try to display or downsize a huge photo image. Here’s how we might ask for the preview image:

let pbpf = settings.availablePreviewPhotoPixelFormatTypes[0]
let len = // desired maximum dimension
settings.previewPhotoFormat = [
    kCVPixelBufferPixelFormatTypeKey as String : pbpf,
    kCVPixelBufferWidthKey as String : len,
    kCVPixelBufferHeightKey as String : len
]

Another good idea, when configuring the AVCapturePhotoSettings object, is to ask for a thumbnail image. This is different from the preview image: the preview image is for you to display in your interface, but the thumbnail image is stored with the photo and is suitable for rapid display by other applications. Here’s how to request a thumbnail image at a standard size (160×120):

settings.embeddedThumbnailPhotoFormat = [
    AVVideoCodecKey : AVVideoCodecType.jpeg
]

Just to make things more interesting, I’ll also specify explicitly that I want the camera to use automatic flash:

let supported = output.supportedFlashModes
if supported.contains(.auto) {
    settings.flashMode = .auto
}

When the AVCapturePhotoSettings object is fully configured, we’re ready to call capturePhoto(with:delegate:) at last:

output.capturePhoto(with: settings, delegate: self)

In that code, I specified self as the delegate (an AVCapturePhotoCaptureDelegate adopter). Functioning as the delegate, we will now receive a sequence of events. The exact sequence depends on what sort of capture we’re doing; in this case, it will be:

  1. photoOutput(_:willBeginCaptureFor:)

  2. photoOutput(_:willCapturePhotoFor:)

  3. photoOutput(_:didCapturePhotoFor:)

  4. photoOutput(_:didFinishProcessingPhoto:error:)

  5. photoOutput(_:didFinishCaptureFor:)

The for: parameter throughout is an AVCaptureResolvedSettings object, embodying the settings actually used during the capture; for instance, we could use it to find out whether flash was actually used.

The delegate event of most interest to our example is the fourth one. This is where we receive the photo! It will arrive in the second parameter as an AVCapturePhoto object. This object contains a lot of information. It provides the resolved settings in its resolvedSettings property. Its previewPixelBuffer property contains the data for the preview image, if we requested one in our AVCapturePhotoSettings. We can extract the image data from the AVCapturePhoto by calling its fileDataRepresentation method. (There is also a longer form of the same method, fileDataRepresentation(with:), allowing you to do such things as modify the metadata and the thumbnail.)

In this example, we implement the fourth delegate method to store the preview image as a property, for subsequent display in our interface, and then save the actual image as a PHAsset in the user’s photo library:

func photoOutput(_ output: AVCapturePhotoOutput,
    didFinishProcessingPhoto photo:
    AVCapturePhoto, error: Error?) {
        if let cgim =
            photo.previewCGImageRepresentation()?.takeUnretainedValue() {
                let orient = // work out desired UIImage.Orientation
                self.previewImage = UIImage(
                    cgImage: cgim, scale: 1, orientation: orient)
        }
        if let data = photo.fileDataRepresentation() {
            let lib = PHPhotoLibrary.shared()
            lib.performChanges {
                let req = PHAssetCreationRequest.forAsset()
                req.addResource(with: .photo, data: data, options: nil)
            }
        }
    }

Image capture with AV Foundation is a huge subject, and our example of a simple photo capture has barely scratched the surface:

  • AVCaptureVideoPreviewLayer provides methods for converting between layer coordinates and capture device coordinates; without such methods, this can be a very difficult problem to solve.

  • You can scan bar codes, shoot video at 60 frames per second (on some devices), and more.

  • You can turn on the LED “torch” by setting the back camera’s torchMode to AVCaptureTorchModeOn, even if no AVCaptureSession is running.

  • You get direct hardware-level control over the camera focus, manual exposure, and white balance.

  • You can capture bracketed images; starting in iOS 10, you can capture live images on some devices, and you can capture RAW images on some devices; and since iOS 11 even more new features have been introduced, such as depth-based image capture and multicamera capture.

There are very good WWDC videos about all this, stretching back over the past several years, and the AVCam-iOS and AVCamManual sample code examples are absolutely superb, demonstrating how to deal with tricky issues such as orientation that would otherwise be very difficult to figure out.

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

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