Taking Pictures and UIImagePickerController

In the choosePhotoSource(_:) method, you will instantiate a UIImagePickerController and present it on the screen. When creating an instance of UIImagePickerController, you must set its sourceType property and assign it a delegate. Because there is set-up work needed for the image picker controller, you need to create and present it programmatically instead of through the storyboard.

Creating a UIImagePickerController

Close the editor showing Main.storyboard. In DetailViewController.swift, add a new method that creates and configures a UIImagePickerController instance. You will need to create the UIImagePickerController instance from more than one place, so abstracting it into a method will help avoid repetition.

Listing 15.1  Adding an image picker controller creation method (DetailViewController.swift)

func imagePicker(for sourceType: UIImagePickerController.SourceType)
                                                        -> UIImagePickerController {
    let imagePicker = UIImagePickerController()
    imagePicker.sourceType = sourceType
    return imagePicker
}

The sourceType is a UIImagePickerController.SourceType enumeration value and tells the image picker where to get images. It has three possible values:

.camera

Allows the user to take a new photo.

.photoLibrary

Prompts the user to select an album and then a photo from that album.

.savedPhotosAlbum

Prompts the user to choose from the most recently taken photos.

These three options are illustrated in Figure 15.5.

Figure 15.5  Examples of the three sourceTypes

Examples of the three sourceTypes

In choosePhotoSource(_:), create an image picker controller instance when the user chooses one of the action sheet options.

Listing 15.2  Creating an image picker controller (DetailViewController.swift)

@IBAction func choosePhotoSource(_ sender: UIBarButtonItem) {
    let alertController = UIAlertController(title: nil,
                                            message: nil,
                                            preferredStyle: .actionSheet)

    alertController.modalPresentationStyle = .popover
    alertController.popoverPresentationController?.barButtonItem = sender

    let cameraAction = UIAlertAction(title: "Camera", style: .default) { _ in
        print("Present camera")
        let imagePicker = self.imagePicker(for: .camera)
    }
    alertController.addAction(cameraAction)

    let photoLibraryAction =
            UIAlertAction(title: "Photo Library", style: .default) { _ in
        print("Present photo library")
        let imagePicker = self.imagePicker(for: .photoLibrary)
    }
    alertController.addAction(photoLibraryAction)

    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
    alertController.addAction(cancelAction)

    present(alertController, animated: true, completion: nil)
}

The first source type, .camera, will not work on a device that does not have a camera. So before using this type, you have to check for a camera by calling the method isSourceTypeAvailable(_:) on the UIImagePickerController class:

    class func isSourceTypeAvailable
        (_ type: UIImagePickerController.SourceType) -> Bool

Calling this method returns a Boolean value indicating whether the device supports the passed-in source type.

Update choosePhotoSource(_:) to only show the camera option if the device has a camera.

Listing 15.3  Checking whether the device has a camera (DetailViewController.swift)

@IBAction func choosePhotoSource(_ sender: UIBarButtonItem) {
    let alertController = UIAlertController(title: nil,
                                            message: nil,
                                            preferredStyle: .actionSheet)

    alertController.modalPresentationStyle = .popover
    alertController.popoverPresentationController?.barButtonItem = sender

    if UIImagePickerController.isSourceTypeAvailable(.camera) {
        let cameraAction = UIAlertAction(title: "Camera", style: .default) { _ in
            let imagePicker = self.imagePicker(for: .camera)
        }
        alertController.addAction(cameraAction)
    }

With that code added, your cameraAction code may not be indented correctly. An easy way to correct indentation is to highlight the code that you want to correct, open the Editor menu, and select StructureRe-Indent. Since this is a tool you will likely want to use often, the keyboard shortcut Control-I is a handy one to remember.

Setting the image picker’s delegate

In addition to a source type, the UIImagePickerController instance needs a delegate. When the user selects an image from the UIImagePickerController’s interface, the delegate is sent the message imagePickerController(_:didFinishPickingMediaWithInfo:). (If the user taps the cancel button, then the delegate receives the message imagePickerControllerDidCancel(_:).)

The image picker’s delegate will be the instance of DetailViewController. At the top of DetailViewController.swift, declare that DetailViewController conforms to the UINavigationControllerDelegate and the UIImagePickerControllerDelegate protocols.

Listing 15.4  Conforming to the necessary delegate protocols (DetailViewController.swift)

class DetailViewController: UIViewController, UITextFieldDelegate,
        UINavigationControllerDelegate, UIImagePickerControllerDelegate {

Why UINavigationControllerDelegate? UIImagePickerController’s delegate property is actually inherited from its superclass, UINavigationController, and while UIImagePickerController has its own delegate protocol, its inherited delegate property is declared to reference an object that conforms to UINavigationControllerDelegate.

Next, set the instance of DetailViewController to be the image picker’s delegate in imagePicker(for:).

Listing 15.5  Assigning the image picker controller delegate (DetailViewController.swift)

func imagePicker(for sourceType: UIImagePickerController.SourceType) ->
        UIImagePickerController {
    let imagePicker = UIImagePickerController()
    imagePicker.sourceType = sourceType
    imagePicker.delegate = self
    return imagePicker
}

Presenting the image picker modally

Once the UIImagePickerController has a source type and a delegate, you can display it by presenting the view controller modally.

In DetailViewController.swift, add code to the end of choosePhotoSource(_:) to present the UIImagePickerController.

Listing 15.6  Presenting the image picker controller (DetailViewController.swift)

if UIImagePickerController.isSourceTypeAvailable(.camera) {
    let cameraAction = UIAlertAction(title: "Camera", style: .default) { _ in
        let imagePicker = self.imagePicker(for: .camera)
        self.present(imagePicker, animated: true, completion: nil)
    }
    alertController.addAction(cameraAction)
}

let photoLibraryAction = UIAlertAction(title: "Photo Library", style: .default) { _ in
    let imagePicker = self.imagePicker(for: .photoLibrary)
    self.present(imagePicker, animated: true, completion: nil)
}
alertController.addAction(photoLibraryAction)

Apple’s documentation for UIImagePickerController mentions that the camera should be presented full screen, and the photo library and saved photos album must be presented in a popover. The only change you need to make to satisfy these requirements is to present the photo library in a popover.

Update the image picker to do just that.

Listing 15.7  Presenting the photo library in a popover (DetailViewController.swift)

let photoLibraryAction = UIAlertAction(title: "Photo Library", style: .default) { _ in
    let imagePicker = self.imagePicker(for: .photoLibrary)
    imagePicker.modalPresentationStyle = .popover
    imagePicker.popoverPresentationController?.barButtonItem = sender
    self.present(imagePicker, animated: true, completion: nil)
}

Build and run the application. Select an Item to see its details and then tap the camera button on the UIToolbar. Choose Photo Library and then select a photo.

If you are working on the simulator, you will notice that the Camera option no longer appears, because the simulator has no camera. However, there are some default images already in the photo library that you can use.

If you have an actual iOS device to run on, you will notice a problem if you try to use the camera. When you select an Item, tap the camera button, and chose Camera, the application crashes.

Take a look at the description of the crash in the console:

    LootLogger[3575:64615] [access] This app has crashed because it attempted to
    access privacy-sensitive data without a usage description.  The app's Info.plist
    must contain an NSCameraUsageDescription key with a string value explaining
    to the user how the app uses this data.

When attempting to access potentially private information, such as the camera, iOS prompts the user to consent to that access. Contained within this prompt is a description of why the application wants to access the information. LootLogger is missing this description, and therefore the application is crashing.

Permissions

There are a number of capabilities on iOS that require user approval before use. The camera is one. Some of the others are:

  • photos

  • location

  • microphone

  • HealthKit data

  • calendar

  • reminders

For each of these, your application must supply a usage description that specifies the reason that your application wants to access the capability or information. This description will be presented to the user when the application attempts the access.

In some cases, iOS manages user privacy without the alert. When selecting a photo from the photo library using UIImagePickerController, users confirm the photo they want to use with the Choose button. No usage description is required. On the other hand, the photo library usage description is required when the application wants to use the Photos framework to access the library silently.

In the project navigator, select the project at the top. In the editor, make sure the LootLogger target is selected and open the Info tab along the top (Figure 15.6).

Figure 15.6  Opening the project info

Opening the project info

Hover over the last entry in this list of Custom iOS Target Properties and click the Opening the project info button. Set the Key of the new entry that appears to NSCameraUsageDescription and the Type to String. You will not find this key in the drop-down menu; you must type in its name. And the key is case sensitive, so make sure to type it in correctly.

When you press Return, the key name in Xcode will change from NSCameraUsageDescription to Privacy – Camera Usage Description. By default, Xcode displays human-readable strings instead of the actual key names. When adding or editing an entry, you can use either the human-readable string or the actual key name. If you would like to view the actual key names in Xcode, Control-click on the key-value table and select Raw Keys & Values.

For the Value, enter This app uses the camera to associate photos with items. This is the string that will be presented to the user. The Custom iOS Target Properties section will look similar to Figure 15.7.

Figure 15.7  Adding the new key

Adding the new key

Build and run the application on a device and navigate to an item. Tap the camera button, select the Camera option, and you will see the permission dialog presented with the usage description that you provided (Figure 15.8). After you tap OK, the UIImagePickerController’s camera interface will appear on the screen, and you can take a picture.

Figure 15.8  Camera privacy alert

Camera privacy alert

Saving the image

Selecting an image dismisses the UIImagePickerController and returns you to the detail view. However, you do not have a reference to the photo once the image picker is dismissed. To fix this, you are going to implement the delegate method imagePickerController(_:didFinishPickingMediaWithInfo:). This method is called on the image picker’s delegate when a photo has been selected.

In DetailViewController.swift, implement imagePickerController(_:didFinishPickingMediaWithInfo:) to put the image into the UIImageView and then call the method to dismiss the image picker.

Listing 15.8  Accessing the selected image (DetailViewController.swift)

func imagePickerController(_ picker: UIImagePickerController,
        didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {

    // Get picked image from info dictionary
    let image = info[.originalImage] as! UIImage

    // Put that image on the screen in the image view
    imageView.image = image

    // Take image picker off the screen - you must call this dismiss method
    dismiss(animated: true, completion: nil)
}

The image that the user selects comes packaged within the info dictionary. This dictionary contains data relevant to the user’s selection, and its contents will vary depending on how the image picker is configured. For example, if the image picker is configured to allow image editing, the dictionary might also contain the .editedImage and .cropRect keys. There are other ways to configure an image picker and other keys that can be returned in the info dictionary, so take a look at the UIImagePickerController documentation if you are interested in learning more.

Build and run the application again. Select a photo. The image picker is dismissed, and you are returned to the DetailViewController’s view, where you will see the selected photo.

LootLogger’s users could have hundreds of items to catalog, and each one could have a large image associated with it. Keeping hundreds of instances of Item in memory is not a big deal. But keeping hundreds of images in memory would be bad: First, you will get a low-memory warning. Then, if your app’s memory footprint continues to grow, the OS will terminate it.

The solution, which you are going to implement in the next section, is to store images to disk and only fetch them into RAM when they are needed. This fetching will be done by a new class, ImageStore. When the application receives a low-memory notification, the ImageStore’s cache will be flushed to free the memory that the fetched images were occupying.

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

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