Saving Images With Core Data

We won’t actually save images in core data but use it as a means for retrieving images from the app’s sandboxed file system. We will generate a unique name for that image and save the .png to the app’s file system. Then save the name of that .png as an attribute value in core data. 

We won’t actually save images in core data but use it as a means for retrieving images from the app’s sandboxed file system. We will generate a unique name for that image and save the .png to the app’s file system. Then save the name of that .png as an attribute value in core data. 

To get started, create a Single View App, make sure “Use Core Data” is checked. Open the .xcdatamodeld file and create a new entity. For simplicity, I am only adding one attribute to the entity to store the image’s name as a string. With the attribute selected, go to the Data Model Inspector in the right column and uncheck “Optional” in the properties section.

With the core data entity setup, we will now create an image controller to interface with the app’s file system. This controller will be responsible for saving, fetching, and deleting images. Create a new swift file and name it ImageController, then import Foundation and UIKit. Declare a new class in this file called “ImageController” and add the following:

static let shared = ImageController()

let fileManager = FileManager.default
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!

We will use the singleton pattern and create a shared initialization for this class. Then create a constant for the app’s file system and a path to store the images. Now we will create a method to save image files, add the following method to your ImageController class.

func saveImage(image: UIImage) -> String? {
    let date = String( Date.timeIntervalSinceReferenceDate )
    let imageName = date.replacingOccurrences(of: ".", with: "-") + ".png"
    
    if let imageData = image.pngData() {
        do {
            let filePath = documentsPath.appendingPathComponent(imageName)
            
            try imageData.write(to: filePath)
            
            print("\(imageName) was saved.")
            
            return imageName
        } catch let error as NSError {
            print("\(imageName) could not be saved: \(error)")
            
            return nil
        }
        
    } else {
        print("Could not convert UIImage to png data.")
        
        return nil
    }
}

This method accepts a UIImage then returns the image name upon a successful save. For simplicity sake, we are using milliseconds since January 1, 2001 to name the image. Then convert the image to a .png and save to the documents path. Next we create a method to fetch images from the document path. Add the following method to your ImageController class.

func fetchImage(imageName: String) -> UIImage? {
    let imagePath = documentsPath.appendingPathComponent(imageName).path
    
    guard fileManager.fileExists(atPath: imagePath) else {
        print("Image does not exist at path: \(imagePath)")
        
        return nil
    }
    
    if let imageData = UIImage(contentsOfFile: imagePath) {
        return imageData
    } else {
        print("UIImage could not be created.")
        
        return nil
    }
}

This method checks that a file exists with the imageName argument. If so, we return the UIImage. The last method for the ImageController is to delete an image. This method accepts the image name and removes it from the documents path. Add the following method to your ImageController class.

func deleteImage(imageName: String) {
    let imagePath = documentsPath.appendingPathComponent(imageName)
    
    guard fileManager.fileExists(atPath: imagePath.path) else {
        print("Image does not exist at path: \(imagePath)")
        
        return
    }
    
    do {
        try fileManager.removeItem(at: imagePath)
        
        print("\(imageName) was deleted.")
    } catch let error as NSError {
        print("Could not delete \(imageName): \(error)")
    }
}

With the image controller setup, we can now make a model controller to interface with core data. This class will have the ability to return an array of saved image objects, save a new object or delete an existing object. Create a new swift file and name it ModelController then import Foundation, Core Data, and UIKit. Add the following to your new model controller class.

static let shared = ModelController()

let entityName = "StoredImage"

private var savedObjects = [NSManagedObject]()
private var images = [UIImage]()
private var managedContext: NSManagedObjectContext!

We are again using the singleton pattern by creating a shared initialization for this class. Set the “entityName” constant to what your core data entity is named, this is case sensitive. Any image names returned from core data will be fetched by the image controller then appended to the images array.

private init() {
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
    managedContext = appDelegate.persistentContainer.viewContext
    
    fetchImageObjects()
}

This initialization gives the model controller access to core data’s managed object context for manipulating the core data graph. It then calls the following method to populate the savedObjects and images arrays.

func fetchImageObjects() {
    let imageObjectRequest = NSFetchRequest<NSManagedObject>(entityName: entityName)
    
    do {
        savedObjects = try managedContext.fetch(imageObjectRequest)
        
        images.removeAll()
        
        for imageObject in savedObjects {
            let savedImageObject = imageObject as! StoredImage
            
            guard savedImageObject.imageName != nil else { return }
            
            let storedImage = ImageController.shared.fetchImage(imageName: savedImageObject.imageName!)
            
            if let storedImage = storedImage {
                images.append(storedImage)
            }
        }
    } catch let error as NSError {
        print("Could not return image objects: \(error)")
    }
}

This method fetches all of the objects from core data that match our entity name then empties the images array before looping through the returned objects. We made the “imageName” attribute not optional so there should be a name stored in each one. Then use our image controller to fetch that image from the file system. To save new image names to core data, add the following method.

func saveImageObject(image: UIImage) {
    let imageName = ImageController.shared.saveImage(image: image)
    
    if let imageName = imageName {
        let coreDataEntity = NSEntityDescription.entity(forEntityName: entityName, in: managedContext)
        let newImageEntity = NSManagedObject(entity: coreDataEntity!, insertInto: managedContext) as! StoredImage
        
        newImageEntity.imageName = imageName
        
        do {
            try managedContext.save()
            
            images.append(image)
            
            print("\(imageName) was saved in new object.")
        } catch let error as NSError {
            print("Could not save new image object: \(error)")
        }
    }
}

This method utilizes the image controller to save a new image to the file system. On a successful save, create a new core data entity and save the image name to core data. To delete an image from the file system and object from core data, add the following method.

func deleteImageObject(imageIndex: Int) {
    guard images.indices.contains(imageIndex) && savedObjects.indices.contains(imageIndex) else { return }
    
    let imageObjectToDelete = savedObjects[imageIndex] as! StoredImage
    let imageName = imageObjectToDelete.imageName
    
    do {
        managedContext.delete(imageObjectToDelete)
        
        try managedContext.save()
        
        if let imageName = imageName {
            ImageController.shared.deleteImage(imageName: imageName)
        }
        
        savedObjects.remove(at: imageIndex)
        images.remove(at: imageIndex)
        
        print("Image object was deleted.")
    } catch let error as NSError {
        print("Could not delete image object: \(error)")
    }
}

This method deletes the object from core data and on a successful delete, passes the image name to the image controller which will delete it from the file system. This article demonstrates a simple approach to storing and retrieving images with core data. The code samples in this article are released under the MIT License.

Leave a Reply

Your email address will not be published. Required fields are marked *