Episode #266

Working with Images

Series: Hello CloudKit

15 minutes
Published on April 20, 2017

This video is only available to subscribers. Get access to this video and 583 others.

Working with images in CloudKit can be tricky. There's no server code you can add to process images to create multiple versions, for instance. In this episode we'll see how we can pick an image from the user's photo library and upload them to a new Photo record, which contains original and thumbnail versions of the uploaded picture. We'll leverage some helper methods to automatically translate from a UIImage to a CKAsset and vice-versa.

Episode Links

Creating the Photo model

class Photo {
    static let recordType = "Photo"

    let record: CKRecord

    init(fullsizeImage: UIImage, thumbnail: UIImage, restaurantID: CKRecordID) {
        record = CKRecord(recordType: Photo.recordType)
        record["restaurant"] = CKReference(recordID: restaurantID, action: .deleteSelf)

        record["image"] = CKAsset(image: fullsizeImage, compression: 0.9)
        record["thumbnail"] = CKAsset(image: thumbnail, compression: 0.7)
    }
}

This relies on a helper method to create an asset from an in-memory UIImage:

extension CKAsset {
    convenience init(image: UIImage, compression: CGFloat) {
        let fileURL = ImageHelper.saveToDisk(image: image, compression: compression)
        self.init(fileURL: fileURL)
    }

    var image: UIImage? {
        guard let data = try? Data(contentsOf: fileURL),
            let image = UIImage(data: data) else {
                return nil
        }

        return image
    }
}

Selecting the Image to Upload

We'll use the built-in UIImagePickerController class to select an image from the photo library:



    @IBAction func uploadPhoto(_ sender: Any) {
        let picker = UIImagePickerController()
        picker.sourceType = .savedPhotosAlbum
        picker.delegate = self
        present(picker, animated: true, completion: nil)
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        let originalImage = info[UIImagePickerControllerOriginalImage] as! UIImage

        dismiss(animated: true, completion: {
            self.spinner.startAnimating()

            DispatchQueue.global(qos: .userInitiated).async {
                // TODO
            }
        })
    }

    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }    


Once we've grabbed the image, we need to create all the versions of this image. In our case we want a thumbnailed version of this image. We don't have a server for this app, so we'll create the versions on the client and add them to the record. Later, when we fetch the Photo record, we can specify which version we want to use.

Resizing the Photo into Various Sizes

We start with a quick helper method to save a smaller square version of the photo:

struct ImageHelper {
    static func createThumbnail(from image: UIImage, fillingSize size: CGSize) -> UIImage {
        let scale = max(size.width / image.size.width, size.height / image.size.height)
        let width = image.size.width * scale
        let height = image.size.height * scale
        let thumbnailRect = CGRect(
            x: (size.width - width) / 2,
            y: (size.height - height) / 2,
            width: width,
            height: height)
        UIGraphicsBeginImageContext(size)
        image.draw(in: thumbnailRect)
        let thumbnail = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return thumbnail!
    }
}

Creating and Saving the Photo Record

Then we create our Photo instance...

private func createPhoto(image: UIImage) {
    let thumbnail = ImageHelper.createThumbnail(from: image, fillingSize: CGSize(width: 200, height: 200))
    let photo = Photo(fullsizeImage: image, thumbnail: thumbnail, restaurantID: restaurantID)
    savePhoto(photo)
 }

We can then save it on CloudKit:

private func savePhoto(_ photo: Photo) {
    database.save(photo.record) { record, error in
        if let e = error {
            print("Error saving photo: \(e)")
        } else {
            self.prepend(photo: photo)
        }

        DispatchQueue.main.async {
            self.spinner.stopAnimating()
        }
    }
}

The prepend(photo:) method just adds to the local array and inserts an item into the collection view:

private func prepend(photo: Photo) {
    photos.insert(photo.record, at: 0)
    DispatchQueue.main.async {
        self.collectionView?.insertItems(at: [IndexPath(item: 0, section: 0)])
    }
}

This episode uses Ios 10.3, Xcode 8.3.