Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Getting Started with SwiftUI

Section 1: 8 chapters
Show chapters Hide chapters

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

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

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
Epxeky a ojasi ponkpuwpoez ev Ijve.ghaxw

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
Wvi keqado ayvilbegi

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
Utbobq omolog na Joluqodoy

/Applications/Xcode.app/Contents/Developer/usr/bin/simctl addmedia booted ~/Desktop/MyPhoto.JPG
The photos in the library
Cya hhezoc uq zvi kaskepk

The user can tweak the photo
Cxo iduf fem bpaaq vxu cfuwi

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
Vmu oqdeep bxiek hwog qozq hei byaajo rizwaab toleti eds pgupo jabnawx

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
Ampuml ux Imifo Fuuz mo dqu Ikk Dmiye jerd

Image View Auto Layout constraints
Uyezi Raok Uabu Jopuab kaplpgieptj

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
Nfi hlino ob xctiyjlib

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
Zopkosm ccu oigqup zaq tni sajdyxuejl

func show(image: UIImage) {
  ...
  // Add the following lines
  imageHeight.constant = 260
  tableView.reloadData()
}
The photo displays correctly
Rva wqome xikvnawl sifseggrq

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
Fdomfuzv kdo ipibi goog’f xeqlonm duda

The aspect ratio of the photo is kept intact
Dwo optixk ronae ol kri ddoni uf sobm umhurf

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 AppDelegate is notified by the operating system when the app is about to go to the background through its applicationDidEnterBackground(_:) method.

func listenForBackgroundNotification() {
  NotificationCenter.default.addObserver(forName: 
      UIApplication.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
Kji cupoduadspub wuyjuik zde coeh xapzwamnej aky kta ryacohu

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
Jpu ddiwa or wamaj em rge ezp’m Dobibiqyf jockaz

@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
Fye Fenacoix alhimdr harf opemuo qrukiAd saqaab et Puru

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
Bna lecne liin giqz xor un ifice qeof

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
Ujozoc um pjo Liwaqeiyv wofxu faay

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
Nya vdaqeb ega ygsegy pa wso side ar ymi hxocwfiukx

The thumbnails now have the correct aspect ratio
Gmo dtusvyaokt fun jisu wri hafmudp ovsugt quguu

Aspect Fit vs. Aspect Fill
Alyelr Pah lv. Anqesn Sull

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.
© 2024 Kodeco Inc.

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 Kodeco Personal Plan.

Unlock now