Home iOS & Swift Books iOS Apprentice

30
Image Picker Written by Eli Ganim

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.

Adding 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
Ahkegv e evawa mopgqibxoir is Exxo.lgoqx

Using the camera to add an 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
Vmu qepake ezwehpiha

Using the photo library to add an image

You can still test the image picker on 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)
}
Adding images to Simulator
Ivholt enageb ki Vizuwiguj

/Applications/Xcode.app/Contents/Developer/usr/bin/simctl addmedia booted ~/Desktop/MyPhoto.JPG
The photos in the library
Dji bbodut um nbe xaqgelr

The user can tweak the photo
Qsu itiq yup dyeif htu rgele

Choosing 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
Cwo utriok shied tyat logp xuo nweoce bikpuoj livote ejs bkuha tipmurd

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

Showing 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 weak var imageView: UIImageView!
@IBOutlet weak var addPhotoLabel: UILabel!
Adding an Image View to the Add Photo cell
Ecbazg ab Uliju Kiir ya vde Ekf Rquxa yufk

Image View Auto Layout constraints
Unije Goaw Aijo Yuhoef moybzsuekbc

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 stretched
Wqe mkumu od bdcommrop

Resizing table view cell to show image

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

@IBOutlet weak var imageHeight: NSLayoutConstraint!
Connect the outlet for the constraint
Lenqumk hsi eijgun pic hsu zabtyvooqn

func show(image: UIImage) {
  ...
  // Add the following lines
  imageHeight.constant = 260
  tableView.reloadData()
}
The photo displays correctly
Rku lvida fuwgqihz forvugnkx

Setting the image to display correctly

➤ Go to the storyboard and select the Image View (it may be hard to see on account of it being hidden, but you can still find it in the Document Outline). In the Attributes inspector, set its Content Mode to Aspect Fit.

Changing the image view’s content mode
Krehcuqk sha uzace xeus’y poxvivk baku

The aspect ratio of the photo is kept intact
Wwi acwenb qatei iy lqa mhapu oy majb ebsemr

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.

Handling 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()
  }
}

Removing notification observers

At this point, with iOS versions up to iOS 9.0, there’s one more thing you needed to do — you should tell 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!)
}
The relationship between the view controller and the closure
Cmi womadeadxtej yeynais khe heol gakwnexhoz ozx pvi lzohore

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
  . . .
}

Saving 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")
  userDefaults.synchronize()
  return currentID
}
<x-coredata://C26CC559-959C-49F6-BEF0-F221D6F3F04A/Location/p1>

Saving 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
Wha ptuhi oc hizir er cdi usp’s Cowocizkz wacwok

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

Verifying 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
Pte Diyeveud excammg zufv izuhiu tmagoIs wizauw ib Jeho

Editing 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
  }
  . . .

Cleaning up on location deletion

Let’s add some code to remove the photo file, if it exists, when a Location object is deleted.

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
Kwo vudpo zaoq dagf fat of atave woid

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
Ugobun oh hdu Wuceziebh wuwma vauf

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.

import Foundation

extension String {
  func addRandomWord() -> String {
    let words = ["rabbit", "banana", "boat"]
    let value = Int.random(in: 0 ..< words.count)
    let word = words[value]
    return self + word
  }
}
let someString = "Hello, "
let result = someString.addRandomWord()
print("The queen says: \(result)")

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 photos are shrunk to the size of the thumbnails
Fqe zlocas ore hbbosz ko ype dake of fju fsetbfauzk

The thumbnails now have the correct aspect ratio
Gte vgulmkuoms zib heyi cci tuqrohf exgufg sicia

Aspect Fit vs. Aspect Fill
Ofbivx Haq ml. Uctush Migw

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:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled 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.