41

I am able to pick and display an image from the photo library, but my goal is to be able to save that picked image or file path to core data so that when that saved record is chosen that image will display also.

I have CoreData working and I am able to display text from CoreData fine it is only the image holding me up.

@IBAction func addPic(sender: AnyObject) {
pickerController.delegate = self
pickerController.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
// 2
self.presentViewController(pickerController, animated: true, completion: nil)

// Displays image
func imagePickerController(picker: UIImagePickerController!,didFinishPickingMediaWithInfo info: NSDictionary!){
image.image = info[UIImagePickerControllerOriginalImage] as? UIImage

self.dismissViewControllerAnimated(true, completion: nil)
R Menke
  • 8,183
  • 4
  • 35
  • 63
turtle02
  • 603
  • 3
  • 10
  • 17

2 Answers2

121

Skip to Processing the Image to find out how to convert UIImage to NSData (which is what Core Data uses)

Or download from github

Core Data Setup:

Set up two entities : Full Resolution and Thumbnail. Full Resolutions is to store the original image. Thumbnail to store a smaller version to be used inside the app. You might use a smaller version in a UICollectionView overview for example.

Images are stored as Binary Data in Core Data. The corresponding type in Foundation is NSData. Convert back to UIImage with UIImage(data: newImageData)

enter image description here


enter image description here


Check the Allows External Storage box for the Binary Data fields. This will automatically save the images in the file system en reference them in Core Data

enter image description here

Connect the two entities, creating a one to one relationship between the two.

enter image description here

Go to Editor en select Create NSManagedObjectSubclass. This will generate files with Classes representing your Managed Object SubClasses. These will appear in your project file structure.

enter image description here


Basic ViewController Setup:

Import the following :

import UIKit
import CoreData

  • Setup two UIButtons and an UIImageView in the Interface Builder
  • Create two dispatch queues, one for CoreData and one for UIImage conversions

class ViewController: UIViewController {

    // imageview to display loaded image
    @IBOutlet weak var imageView: UIImageView!

    // image picker for capture / load
    let imagePicker = UIImagePickerController()

    // dispatch queues
    let convertQueue = dispatch_queue_create("convertQueue", DISPATCH_QUEUE_CONCURRENT)
    let saveQueue = dispatch_queue_create("saveQueue", DISPATCH_QUEUE_CONCURRENT)

    // moc
    var managedContext : NSManagedObjectContext?


    override func viewDidLoad() {
        super.viewDidLoad()

        imagePickerSetup() // image picker delegate and settings

        coreDataSetup() // set value of moc on the right thread

    }

    // this function displays the imagePicker
    @IBAction func capture(sender: AnyObject) { // button action
        presentViewController(imagePicker, animated: true, completion: nil)
    }

    @IBAction func load(sender: AnyObject) { // button action

        loadImages { (images) -> Void in
            if let thumbnailData = images?.last?.thumbnail?.imageData {
                let image = UIImage(data: thumbnailData)
                self.imageView.image = image
            }
        }
    }
}

This function sets a value to managedContext on the correct thread. Since CoreData needs all operations in one NSManagedObjectContext to happen in the same thread.

extension ViewController {
    func coreDataSetup() {
        dispatch_sync(saveQueue) {
            self.managedContext = AppDelegate().managedObjectContext
        }
    }
}

Extend the UIViewController so it conforms to UIImagePickerControllerDelegate and UINavigationControllerDelegate These are needed for the UIImagePickerController.

Create a setup function and also create the delegate function imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?)

extension ViewController : UIImagePickerControllerDelegate, UINavigationControllerDelegate {

    func imagePickerSetup() {

        imagePicker.delegate = self
        imagePicker.sourceType = UIImagePickerControllerSourceType.Camera

    }

    // When an image is "picked" it will return through this function
    func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?) {

        self.dismissViewControllerAnimated(true, completion: nil)
        prepareImageForSaving(image)

    }
}

Immediately dismiss the UIImagePickerController, else the app will appear to freeze.


Processing the Image:

Call this function inside imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage, editingInfo: [String : AnyObject]?).

  • First get the current date with timeIntervalSince1970. This returns an NSTimerInterval in seconds. This converts nicely to a Double. It will serve as a unique id for the images and as a way to sort them.

  • Now is a good time to move to the separate queue and free up the main queue. I used dispatch_async(convertQueue) first to do the heavy lifting on a separate thread.

  • Then you need to convert the UIImage to NSData this is done with UIImageJPEGRepresentation(image, 1). The 1 represents the quality where 1 is the highest and 0 is the lowest. It returns an optional so I used optional binding.

  • Scale the image to a desired thumbnail size and also convert to NSData.

Code:

extension ViewController {

    func prepareImageForSaving(image:UIImage) {

        // use date as unique id
        let date : Double = NSDate().timeIntervalSince1970

        // dispatch with gcd.
        dispatch_async(convertQueue) {

            // create NSData from UIImage
            guard let imageData = UIImageJPEGRepresentation(image, 1) else {
                // handle failed conversion
                print("jpg error")
                return
            }

            // scale image, I chose the size of the VC because it is easy
            let thumbnail = image.scale(toSize: self.view.frame.size)

            guard let thumbnailData  = UIImageJPEGRepresentation(thumbnail, 0.7) else {
                // handle failed conversion
                print("jpg error")
                return
            }

            // send to save function
            self.saveImage(imageData, thumbnailData: thumbnailData, date: date)

        }
    }
}

This function does the actual saving.

  • Go the the CoreData thread with dispatch_barrier_sync(saveQueue)
  • First insert a new FullRes and a new Thumbnail object into the Managed Object Context.
  • Set the values
  • Set the relationship between FullRes and Thumbnail
  • Use do try catch to attempt a save
  • Refresh the Managed Object Context to free up memory

By using dispatch_barrier_sync(saveQueue) we are sure that we can safely store a new image and that new saves or loads will wait until this is finished.

Code:

extension ViewController {

    func saveImage(imageData:NSData, thumbnailData:NSData, date: Double) {

        dispatch_barrier_sync(saveQueue) {
            // create new objects in moc
            guard let moc = self.managedContext else {
                return
            }

            guard let fullRes = NSEntityDescription.insertNewObjectForEntityForName("FullRes", inManagedObjectContext: moc) as? FullRes, let thumbnail = NSEntityDescription.insertNewObjectForEntityForName("Thumbnail", inManagedObjectContext: moc) as? Thumbnail else {
                // handle failed new object in moc
                print("moc error")
                return
            }

            //set image data of fullres
            fullRes.imageData = imageData

            //set image data of thumbnail
            thumbnail.imageData = thumbnailData
            thumbnail.id = date as NSNumber
            thumbnail.fullRes = fullRes

            // save the new objects
            do {
                try moc.save()
            } catch {
                fatalError("Failure to save context: \(error)")
            }

            // clear the moc
            moc.refreshAllObjects()
        }
    }
}

To load an image :

extension ViewController {

    func loadImages(fetched:(images:[FullRes]?) -> Void) {

        dispatch_async(saveQueue) {
            guard let moc = self.managedContext else {
                return
            }

            let fetchRequest = NSFetchRequest(entityName: "FullRes")

            do {
                let results = try moc.executeFetchRequest(fetchRequest)
                let imageData = results as? [FullRes]
                dispatch_async(dispatch_get_main_queue()) {
                    fetched(images: imageData)
                }
            } catch let error as NSError {
                print("Could not fetch \(error), \(error.userInfo)")
                return
            }
        }
    }
}

The functions used to scale the image:

extension CGSize {

    func resizeFill(toSize: CGSize) -> CGSize {

        let scale : CGFloat = (self.height / self.width) < (toSize.height / toSize.width) ? (self.height / toSize.height) : (self.width / toSize.width)
        return CGSize(width: (self.width / scale), height: (self.height / scale))

    }
}

extension UIImage {

    func scale(toSize newSize:CGSize) -> UIImage {

        // make sure the new size has the correct aspect ratio
        let aspectFill = self.size.resizeFill(newSize)

        UIGraphicsBeginImageContextWithOptions(aspectFill, false, 0.0);
        self.drawInRect(CGRectMake(0, 0, aspectFill.width, aspectFill.height))
        let newImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return newImage
    }

}
R Menke
  • 8,183
  • 4
  • 35
  • 63
  • Thanks Man that worked Great. Can you also help me converting it back to UIImage so that I can display it. – turtle02 Jan 18 '15 at 01:15
  • "So I also generate thumbnails and save those in a related entity." Do you mean you save a thumbnail to entity and the large res image to a related entity? So that when you call main entity, large res image isn't called yet? – Chameleon Apr 28 '15 at 02:38
  • 1
    It actually doesn't matter. They are related to each other. But that is correct. I usually have an entity for in app use with all the data that goes with an image. Album title, location, time, thumbnail,.... And another one that contains the full res for exporting. This is also handy if you apply filters. There is no need to apply filters to full res images when the user isn't exporting. He/she might just be playing around with settings. – R Menke Apr 28 '15 at 03:40
  • Thank you R Menke. This was a good tutorial about CoreData in general as well! – Axe Jul 15 '16 at 17:39
  • @RMenke (talking about Core data in general) is good practice to call 'refreshAllObjects' at every save? – Jacopo Penzo Jan 21 '17 at 01:02
  • Thanks. Very nice. Although I am getting crash for every 3rd or 4th capture (downloaded and ran Github code). Error : Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -_referenceData64 only defined for abstract class. Define -[NSTemporaryObjectID_default _referenceData64]!' – Nitish Jul 05 '17 at 12:13
  • Also opening issue on git repository. Hope this can be addressed. – Nitish Jul 05 '17 at 12:13
  • @RMenke : Thanks :) – Nitish Jul 07 '17 at 04:47
  • Cannot convert value of type 'dispatch_queue_concurrent_t.Type' (aka 'OS_dispatch_queue_concurrent.Type') to expected argument type '__OS_dispatch_queue_attr?' – J A S K I E R Oct 08 '18 at 10:02
  • Thanks for the elaborate tutorial. I have made everything work in the current swift version and it runs and builds but when I press the load button, the block: if let thumbnailData = images?.last?.thumbnail?.imageData { let image = UIImage(data: thumbnailData as Data) self.imageView.image = image } is never entered. Would be greatly appreciated if you could update your code! Thanks – Wert Oct 19 '19 at 22:35
  • 1
    @Wert I wont be able to update the code any further, I'm sorry :/ I no longer focus on Swift and iOS and no longer have the time to keep up with all the latest changes. Feel free to suggest edits to my answer to help other people. – R Menke Oct 20 '19 at 10:08
  • 1
    Really nice explanation and tutorial! One remark: For UIGraphicsBeginImageContextWithOptions, I had to set the scaler to 1.0 to get my desired image size. With 0.0, I always got a twice as large image when converting the Data back to an image. – Phil_G Feb 08 '20 at 12:55
2

Core Data isn't meant to save big binary files like images. Use Document Directory in file system instead.

Here is sample code to achieve that.

let documentsDirectory = NSSearchPathForDirectoriesInDomains(.DocumentDirectory,.UserDomainMask, true).first as! String
 // self.fileName is whatever the filename that you need to append to base directory here.

let path = documentsDirectory.stringByAppendingPathComponent(self.fileName)

let success = data.writeToFile(path, atomically: true)
if !success { // handle error }
bummi
  • 27,123
  • 14
  • 62
  • 101
Himanshu
  • 2,832
  • 4
  • 23
  • 51