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 Source Code CKAsset 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)]) } }