Home iOS & Swift Books UIKit Apprentice

30
Image Picker Written by Matthijs Hollemans & Fahim Farook

Your Tag Locations screen is mostly feature complete — except for the ability to add a photo for a location. Time to fix that!

UIKit comes with a built-in view controller, UIImagePickerController, that lets the user take new photos and videos, or pick them from their Photo Library. You’re going to use it to save a photo along with the location so the user has a nice picture to look at.

This is what your screen will look like when you’re done:

A photo in the Tag Location screen
A photo in the Tag Location screen

In this chapter, you will do the following:

  • Add an image picker: Add an image picker to your app to allow you to take photos with the camera or to select existing images from your photo library.
  • Show the image: Show the picked image in a table view cell.
  • UI improvements: Improve the user interface functionality when your app is sent to the background.
  • Save the image: Save the image selected via the image picker on device so that it can be retrieved later.
  • Edit the image: Display the image on the edit screen if the location has an image.
  • Thumbnails: Display thumbnails for locations on the Locations list screen.

Add an image picker

Just as you need to ask the user for permission before you can get GPS information from the device, you need to ask for permission to access the user’s photo library.

You don’t need to write any code for this, but you do need to declare your intentions in the app’s Info.plist. If you don’t do this, the app will crash with no visible warnings except for a message in the Xcode Console, as soon as you try to use the UIImagePickerController.

Info.plist changes

➤ Open Info.plist and add a new row — either use the plus (+) button on existing rows, or right-click and select Add Row, or use the Editor ▸ Add Item menu option.

Adding a usage description in Info.plist
Itkejk u omepo jemryifheez ad Egho.gyojz

Use camera to add image

➤ In LocationDetailsViewController.swift, add the following extension to the end of the source file:

extension LocationDetailsViewController: UIImagePickerControllerDelegate,
  UINavigationControllerDelegate {
  // MARK: - Image Helper Methods
  func takePhotoWithCamera() {
    let imagePicker = UIImagePickerController()
    imagePicker.sourceType = .camera
    imagePicker.delegate = self
    imagePicker.allowsEditing = true
    present(imagePicker, animated: true, completion: nil)
  }
}
// MARK: - Image Picker Delegates
func imagePickerController(
  _ picker: UIImagePickerController, 
  didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
  dismiss(animated: true, completion: nil)
}

func imagePickerControllerDidCancel(
  _ picker: UIImagePickerController
) {
  dismiss(animated: true, completion: nil)
}
override func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  if indexPath.section == 0 && indexPath.row == 0 {
    . . . 
  } else if indexPath.section == 1 && indexPath.row == 0 {
    takePhotoWithCamera()
  }
}
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Source type 1 not available'
imagePicker.sourceType = .camera
The camera interface
Hva bomisu apvuntipi

Use photo library to add image

You can still test the image picker on the Simulator, but instead of using the camera, you have to use the photo library.

func choosePhotoFromLibrary() {
  let imagePicker = UIImagePickerController()
  imagePicker.sourceType = .photoLibrary
  imagePicker.delegate = self
  imagePicker.allowsEditing = true
  present(imagePicker, animated: true, completion: nil)
}
The iOS photos library on the simulator
Dte eIJ cwetos lizgalm if tre xucoyezij

Adding photos to the simulator

There are several ways you can add new photos to the Simulator. You can go into Safari on the Simulator, search the internet for an image, press down on the image until a menu appears, and then choose Add to Photos.

/Applications/Xcode.app/Contents/Developer/usr/bin/simctl addmedia booted ~/Desktop/MyPhoto.JPG

The user can tweak the photo
Vdi ohub vez tgoeb ska vyubo

Choose between camera and photo library

First, you check whether the camera is available. When it is, you show an action sheet to let the user choose between the camera and the Photo Library.

func pickPhoto() {
  if UIImagePickerController.isSourceTypeAvailable(.camera) {
    showPhotoMenu()
  } else {
    choosePhotoFromLibrary()
  }
}

func showPhotoMenu() {
  let alert = UIAlertController(
    title: nil, 
    message: nil, 
    preferredStyle: .actionSheet)

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

  let actPhoto = UIAlertAction(
    title: "Take Photo", 
    style: .default, 
    handler: nil)
  alert.addAction(actPhoto)

  let actLibrary = UIAlertAction(
    title: "Choose From Library", 
    style: .default, 
    handler: nil)
  alert.addAction(actLibrary)

  present(alert, animated: true, completion: nil)
}
The action sheet that lets you choose between camera and photo library
Swu engeam rquic ybok quzs buo hdaife verraag hoyiza emw vtomi pejrebs

if true || UIImagePickerController.isSourceTypeAvailable(.camera) {
let actPhoto = UIAlertAction(
  title: "Take Photo", 
  style: .default) { _ in
    self.takePhotoWithCamera() 
  }
let actLibrary = UIAlertAction(
  title: "Choose From Library", 
  style: .default) { _ in 
    self.choosePhotoFromLibrary() 
  }
tableView.deselectRow(at: indexPath, animated: true)

Show the image

Now that the user can pick a photo, you should display it somewhere — what’s the point otherwise, right? You’ll change the Add Photo cell to hold the photo and when a photo is picked, the cell will grow to fit the photo and the Add Photo label will disappear.

@IBOutlet var imageView: UIImageView!
@IBOutlet var addPhotoLabel: UILabel!
Adding an Image View to the Add Photo cell
Ighozg ok Inuse Goiz nu lbu Inq Rwudi debg

Image View Auto Layout constraints
Ukari Geit Aiha Cidiec zannzgaegtq

var image: UIImage?
func show(image: UIImage) {
  imageView.image = image
  imageView.isHidden = false
  addPhotoLabel.text = ""
}
func imagePickerController(
  _ picker: UIImagePickerController, 
  didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
  image = info[UIImagePickerController.InfoKey.editedImage] as? UIImage
  if let theImage = image {
    show(image: theImage)
  }
  dismiss(animated: true, completion: nil)
}
The photo is tiny
Lwi cfuzi uh yugl

Resize table view cell to show image

➤ Add a new outlet for the image height constraint to LocationDetailsViewController.swift:

@IBOutlet var imageHeight: NSLayoutConstraint!
Connect the outlet for the constraint
Hedderz yda iormay jiq qjo biqbvsoitw

func show(image: UIImage) {
  ...
  // Add the following lines
  imageHeight.constant = 260
  tableView.reloadData()
}
The photo displays correctly
Rfa rgiqo jedzbucv rondudvjc

UI improvements

The user can take a photo — or pick one — now but the app doesn’t save it to the data store yet. Before you get to that, there are still a few improvements to make to the image picker.

Handle background mode

You saw in the Checklists app that the SceneDelegate is notified by the operating system when the app is about to go to the background through its sceneDidEnterBackground(_:) method.

func listenForBackgroundNotification() {
  NotificationCenter.default.addObserver(
    forName: UIScene.didEnterBackgroundNotification,
    object: nil, 
    queue: OperationQueue.main) { _ in
    if self.presentedViewController != nil {
      self.dismiss(animated: false, completion: nil)
    }
    self.descriptionTextView.resignFirstResponder()
  }
}

Remove notification observers

Now that you’ve add a notification to the NotificationCenter, prior to iOS 9.0, you had to also remove that notification telling the NotificationCenter to stop sending these background notifications when the Tag/Edit Location screen closes. You didn’t want NotificationCenter to send notifications to an object that no longer existed, that was just asking for trouble!

var observer: Any!
func listenForBackgroundNotification() {
  observer = NotificationCenter.default.addObserver(forName: . . .
deinit {
  print("*** deinit \(self)")
  NotificationCenter.default.removeObserver(observer!)
}

Closures and capturing

Remember how in closures you always have to specify self when you want to access an instance variable or call a method? That is because closures capture any variables that are used inside the closure.

The relationship between the view controller and the closure
Bxo sipopeizvtog yemtoum yki buax torbzuckid ivk yxo ksobuyo

func listenForBackgroundNotification() {
  observer = NotificationCenter.default.addObserver(
    forName: UIApplication.didEnterBackgroundNotification, 
    object: nil, 
    queue: OperationQueue.main) { [weak self] _ in

    if let weakSelf = self {
      if weakSelf.presentedViewController != nil {
        weakSelf.dismiss(animated: false, completion: nil)
      }
      weakSelf.descriptionTextView.resignFirstResponder()
    }
  }
}
{ [weak self] _ in
  . . . 
}

Save the image

The ability to pick photos is rather useless if the app doesn’t also save them. So, that’s what you’ll do here.

Data model changes

➤ Open the Data Model editor. Add a photoID attribute to the Location entity and give it the type Integer 32. This is an optional value — not all Locations will have photos — so make sure the Optional box is checked in the Data Model inspector.

@NSManaged public var photoID: NSNumber?
var hasPhoto: Bool {
  return photoID != nil
}
var photoURL: URL {
  assert(photoID != nil, "No photo ID set")
  let filename = "Photo-\(photoID!.intValue).jpg"
  return applicationDocumentsDirectory.appendingPathComponent(filename)
}
var photoImage: UIImage? {
  return UIImage(contentsOfFile: photoURL.path)
}
class func nextPhotoID() -> Int {
  let userDefaults = UserDefaults.standard
  let currentID = userDefaults.integer(forKey: "PhotoID") + 1
  userDefaults.set(currentID, forKey: "PhotoID")
  return currentID
}
<x-coredata://C26CC559-959C-49F6-BEF0-F221D6F3F04A/Location/p1>

Save the image to a file

➤ In LocationDetailsViewController.swift, in the done() method, add the following in between where you set the properties of the Location object and where you save the managed object context:

// Save image
if let image = image {
  // 1
  if !location.hasPhoto {
    location.photoID = Location.nextPhotoID() as NSNumber
  }
  // 2
  if let data = image.jpegData(compressionQuality: 0.5) {
    // 3
    do {
      try data.write(to: location.photoURL, options: .atomic)
    } catch {
      print("Error writing file: \(error)")
    }
  }
}
The photo is saved in the app’s Documents folder
Qya ktuve iq hohib er nka ohr’m Kurovutlr tuccoy

@IBAction func done() {
  . . .
  if let temp = locationToEdit {
    . . .
  } else {
    . . .
    location.photoID = nil           // add this
  }
  . . .

Verify photoID in SQLite

If you have Liya or another SQLite inspection tool, you can verify that each Location object has been given a unique photoID value (in the ZPHOTOID column):

The Location objects with unique photoId values in Liya
Gci Jevoveib ezdednn guhx ugemie kpiniOl fasueh ev Pupa

Edit the image

So far, all the changes you’ve made were for the Tag Location screen and adding new locations. Of course, you should make the Edit Location screen show the photos as well. The change to LocationDetailsViewController is quite simple.

override func viewDidLoad() {
  super.viewDidLoad()

  if let location = locationToEdit {
    title = "Edit Location"
    // New code block
    if location.hasPhoto {
      if let theImage = location.photoImage {
        show(image: theImage)
      }
    }
    // End of new code
  }
  . . .

Clean up on location deletion

There’s another editing operation the user can perform on a location: deletion. What happens to the image file when a location is deleted? At the moment nothing. The photo for that location stays forever in the app’s Documents directory.

func removePhotoFile() {
  if hasPhoto {
    do {
      try FileManager.default.removeItem(at: photoURL)
    } catch {
      print("Error removing file: \(error)")
    }
  }
}
override func tableView(
  _ tableView: UITableView, 
  commit editingStyle: UITableViewCell.EditingStyle, 
  forRowAt indexPath: IndexPath
) {
  if editingStyle == .delete {
    let location = fetchedResultsController.object(at: indexPath)

    location.removePhotoFile()              // add this line   
    managedObjectContext.delete(location)
    . . .

Thumbnails

Now that locations can have photos, it’s a good idea to show thumbnails for these photos in the Locations tab. That will liven up this screen a little… a plain table view with just a bunch of text isn’t particularly exciting.

Storyboard changes

➤ Go to the storyboard editor. In the prototype cell for the Locations scene, remove the leading Auto Layout constraint from each of the two labels, and set X = 76 in the View section of the Size inspector.

The table view cell has an image view
Bho raxhu ceuw zeqs woj ox ofave gieh

Code changes

➤ Go to LocationCell.swift and add the following method:

func thumbnail(for location: Location) -> UIImage {
  if location.hasPhoto, let image = location.photoImage {
    return image
  }
  return UIImage()
}
if location.hasPhoto && let image = location.photoImage
photoImageView.image = thumbnail(for: location)
Images in the Locations table view
Inujiv iq kve Fosimearq mobfi liuz

Extensions

So far you’ve used extensions on your view controllers to group related functionality together, such as delegate methods. But you can also use extensions to add new functionality to classes that you didn’t write yourself. That includes classes such as UIImage from the iOS frameworks.

Thumbnails via UIImage extension

You are going to add an extension to UIImage that lets you resize the image. You’ll use it as follows:

return image.resized(withBounds: CGSize(width: 52, height: 52))
import UIKit

extension UIImage {
  func resized(withBounds bounds: CGSize) -> UIImage {
    let horizontalRatio = bounds.width / size.width
    let verticalRatio = bounds.height / size.height
    let ratio = min(horizontalRatio, verticalRatio)
    let newSize = CGSize(
      width: size.width * ratio, 
      height: size.height * ratio)
    UIGraphicsBeginImageContextWithOptions(newSize, true, 0)
    draw(in: CGRect(origin: CGPoint.zero, size: newSize))
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return newImage!
  }
}
func thumbnail(for location: Location) -> UIImage {
  if location.hasPhoto, let image = location.photoImage {
    return image.resized(
      withBounds: CGSize(width: 52, height: 52))
  }
  return UIImage()
}
The thumbnails now look great
Gko ktafkcoiqv qaw poaj fdoef

Aspect Fit vs. Aspect Fill
Exvoxq Hel sf. Isdoht Tejt

Handling low-memory situations

The UIImagePickerController is very memory-hungry. Whenever the iPhone gets low on available memory, UIKit will send your app a “low memory” warning.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.