GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

SOLVED: Core Data - Binary Data - External Storage

Forums > 100 Days of SwiftUI

Hello,

I am working on the Day 77 challenge and using core data to store my information. Basically, i want to store a name (String), id (UUID), and image (Binary Data).

I have no issues with storing the data and fetching.

Like other core data projects, I use nil coalesing for my String when coming out of Core Data and back into my code, but can't figure out how to do the following:

  1. Check if data exists in the image attribute
  2. Use nil coalesing to a placeholder image if an image doesn't exist
  3. If an image exists, convert it from Binary data back to UIImage.

Paul also says "If you do choose to use Core Data, make sure you don’t save the actual image into the database because that’s not efficient. Core Data or not, the optimal thing to do is generate a new UUID for the image filename then write it to the documents directory, and store that UUID in your data model."

I am assuming this means to utilize the external data setting in the core data entity. Is that correct? If so, I am assuming Core Data creates some sort of key that is associated to a location in my documents directory so the image isn't directly stored to Core Data. If this is correct, how do I check to see if that key exists or nil coalesce from there?

This is driving me crazy and I really want embrace Core Data as my data architecture.

I apologize if my phrasing or word usage is wrong.

Please help.

2      

Michael asks about storing images:

Paul also says "If you do choose to use Core Data, make sure you don’t save the actual image into the database because that’s not efficient. Core Data or not, the optimal thing to do is generate a new UUID for the image filename then write it to the documents directory, and store that UUID in your data model."

I am assuming this means to utilize use the external data setting in the core data entity. Is that correct?

No, I don't think you are correct.

You may have an image named "Pascucci_Holiday_in_Capri.jpg" and takes up 1.2MB of space.

@twoStraws suggests you do NOT store this image in CoreData. Instead follow his steps.

  1. Generate a new UUID. It could be: 862F020A-D797-4861-B9AA-9047BDBB768A
  2. Save your image to the documents directory with the name 862F020A-D797-4861-B9AA-9047BDBB768A.jpg
    No, you are NOT saving this to CoreData.
  3. Save the new image name into your CoreData data model.
    myImageFileName = "862F020A-D797-4861-B9AA-9047BDBB768A.jpg"
  4. When you first load the CoreData record, you'll have a very small record from the CoreData.
  5. You'll need to take a second step to load the image file from the document's directory. Good thing you have the name!

So, contrary to the first line in your post, you do NOT want to store the image as binary data in your CoreData record.

Come back and share your success with us! Give some details how you implemented your solution.

2      

Now I feel dumb...I was making this so difficult when it didn't have to be.

As usual @Obelix, your answer makes perfect sense.

I am taking my time with this project and really trying to understand the tools @twostraws taught in this lesson. Once I get it where I'm happy, I will show how I implemented everything.

Thank you so much.

2      

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Transform your career with the iOS Lead Essentials. This Black Friday, unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a free crash course.

Save your spot

Sponsor Hacking with Swift and reach the world's largest Swift community!

Michael shares an observation about my answer to his question:

Now I feel dumb...I was making this so difficult when it didn't have to be.

Michael: I am happy you resolved your approach to solving this challenge.

Good Luck! I hope I didn't give you any impressions that I thought your approach was dumb. I enjoy answering questions and thinking of detailed answers. I enjoy thinking of different ways to explore and explain Swift, design, or architecture concepts that may give new developers heartache. However I've received feedback that my answers are perceived to be condescending, perhaps snarky. I want to remove heartache, not cause it.

I wish you luck on your journey. I will probably be taking a break.

2      

I took no offense at all to your response. It was spot on (as I anticipated it would be) and very much appreciated; certainly not snarky or condescending!

The few questions I have asked on this forum, you (and @roosterboy) have provided great constructive criticism. This is a learning process for me and having knowledgable people to ask questions to when I hit the wall is beyond beneficial.

Personally, and not trying to offend anyone that may read this, I believe some people get offended when they are not confident in themselves. They want validation without criticism. I know I don't know everything but that is not a slight against me. That is why I take on new challenges to learn. I hope one day I will be able to provide the assistance to others that are just starting out that you have to me.

Thank you again for the help and best of luck during your break.

3      

As a follow-up, and for anyone that may need some direction, here is how I solved the problem (with a lot of help from others)...

I created a public function for saving the picture to the documents directory (utilizing Paul's file manager code). This creates a new UUID and uses that as the picture name then returns it back.

public func saveImageToDirectory(image: UIImage) -> String {
    if let data = image.jpegData(compressionQuality: 0.8) {
        let imageID = UUID()
        let filename = FileManager.documentsDirectory.appendingPathComponent("\(imageID).jpg")
        try? data.write(to: filename, options: [.atomic,.completeFileProtection])
        return "\(imageID).jpg"
    }
    return ""
}

Then I have a button in my AddView to add it to the Core Data entity

                Button {
                    picturepath = saveImageToDirectory(image: image)
                    if let location = self.locationFetcher.lastKnownLocation {
                        picturelatitude = location.latitude
                        picturelongitude = location.longitude
                    }
                    guard !picturename.isEmpty else { return }
                    vm.addPictureToEntity(name: picturename, imagePath: picturepath, latitude: picturelatitude, longitude: picturelongitude)
                    dismiss()

My Core Data code is all in it's own file and looks like this...

import SwiftUI
import CoreData

class CoreDataViewModel: ObservableObject {

    // MARK: - PROPERTIES
    @Published var savedPictures: [PictureEntity] = []

    // MARK: - INITIALIZER
    let container = NSPersistentContainer(name: "PicturesContainer")

    init() {
        container.loadPersistentStores { description, error in

            if let error = error {
                print("Failed to load data with error: \(error.localizedDescription)")
            } else {
                print("Successfully loaded core data!")
            }

            self.container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
        }
        fetchPictures()
    }

    // MARK: - METHODS
    func fetchPictures() {
        let request = NSFetchRequest<PictureEntity>(entityName: "PictureEntity")

        do {
            savedPictures = try container.viewContext.fetch(request)
        } catch let error {
            print("Error fetching with error: \(error.localizedDescription)")
        }
    }

    func addPictureToEntity(name: String, imagePath: String, latitude: Double, longitude: Double) {

        let newPicture = PictureEntity(context: container.viewContext)
        newPicture.id = UUID()
        newPicture.name = name
        newPicture.imagepath = imagePath
        newPicture.latitude = latitude
        newPicture.longitude = longitude
        saveData()
    }

    func deletePictureFromEntity(indexSet: IndexSet) {
        guard let index = indexSet.first else { return }
        let picture = savedPictures[index]
        container.viewContext.delete(picture)
        saveData()
    }

    func updatePicture() {

    }

    func saveData() {
        do {
            try container.viewContext.save()
            fetchPictures()
        } catch let error {
            print("Error saving data to entity with error: \(error.localizedDescription)")
        }
    }

}

I'm hoping this helps others out. Constructive criticism is always welcome. Have a great Swift day!

2      

Saving the image and pointing at it with a URL is a valid approach, but you may miss out on the automatic behaviour provided by NSPersistentCloudKitContainer (if you do decide to adopt cloudkit one day).

See using core data with cloudkit WWDC2019.

Moreover, the research this guy did suggests saving blobs internally or externally makes little difference to performance https://www.vadimbulavin.com/how-to-save-images-and-videos-to-core-data-efficiently/

3      

Hi @Icemonster13, @Obelix and everyone...

I'm in the same situation like you, but in my case I've audio files instead of image.

I have followed all the steps that @Obelix has proposed and I have been stuck in point 5: "You'll need to take a second step to load the image file from the document's directory. Good thing you have the name!"

I don't know how to do that from Core Data (I already save the name into my CoreData data model) and I don't see in your code how you have been able to solve it.

Any idea?

Many thanks!

2      

@CacereLucas,

My full project is on GitHub at: https://github.com/Icemonster13/Milestone-Day77-Pictures

However, here is what I think will help you (if I understand what you are asking). I utilize the filename pulled from CoreData and pass it to a public function that looks like this:

public func loadImage(fileName: String) -> UIImage? {
    let fileURL = FileManager.documentsDirectory.appendingPathComponent(fileName)
    do {
        let imageData = try Data(contentsOf: fileURL)
        return UIImage(data: imageData)
    } catch {}
    return nil
}

Let me know if this doesn't help.

3      

I am new in this forum. I have some questions about this forum. These questions are shown below:

https://lotto4d.co/

My question is about this thread. If you have any information about this thread. Please quote me and solve my problem.

2      

Thank you very much @Icemonster13!! I was able to solve it!

The problem was that in the function to load my audios I was fetching from Core Data but without indicating the path of the FileManeger 🫣. Literally I have solved it by adding this single line of code:

let fileURL = FileManager.documentsDirectory.appendingPathComponent(fileName)

Thanks again, you made me see what my mistake was :)

3      

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Transform your career with the iOS Lead Essentials. This Black Friday, unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a free crash course.

Save your spot

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.