WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Day 77: UIImage Codable Conformance

Forums > 100 Days of SwiftUI

Hi All! I'm working on Day 77's Challenge (creating an app that saves an image from the user's photo library and then asks them to rename it), but I've been stuck for a while and could really use some help. I can successfully show and use the Image Picker and display the selected image in ContentView, but when it comes to saving things to the Documents directory I'm completely stumped. Every solution I find on Google goes outside of the scope of what's been taught so I'm hoping somebody can point me in the right direction with the concepts taught in the course, thanks so much in advance to anyone willing to help out!

The main thing I'm struggling with is getting my UserImage struct (shown below) to conform to Codable so that I can save everything to the documents directory, but UserImage includes a UIImage property, which is causing problems since UIImage doesn't conform to Codable. Paul included the following Code as a hint and I'm sure I'm supposed to use it in some way, but I can't figure out where or how to use it (I've tried making a computed property with it and adding it onto the UIImage struct itself):

if let jpegData = yourUIImage.jpegData(compressionQuality: 0.8) {
    try? jpegData.write(to: yourURL, options: [.atomic, .completeFileProtection])
}

Here is my UserImage struct (a type that represents the image that the user selected and its associated info):

import Foundation
import SwiftUI

struct UserImage: Identifiable {
    let id: UUID
    var name: String
    let image: UIImage // I've tried changing this to be a Data type like (I think) Paul suggested and then using the above code as a computed property or function within the struct, but I can't figure out how to make that work
}

Here is ContentView:

struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    var body: some View {
        NavigationView {
            List(viewModel.images) { userImage in
                NavigationLink {
                    Text("image") // Placeholder view
                } label: {
                    Image(uiImage: userImage.image)
                        .resizable()
                        .scaledToFit()
                }
            }
            .onChange(of: viewModel.inputImage) { _ in viewModel.converToSwiftUiImage() }
            .toolbar {
                Button {
                    viewModel.showingImagePicker = true
                } label: {
                    Image(systemName: "plus")
                }
            }
            .sheet(isPresented: $viewModel.showingImagePicker) {
                ImagePicker(image: $viewModel.inputImage)
            }
            .navigationTitle("Image Context")
        }
    }
}

And here is the ViewModel for ContentView:

extension ContentView {
    @MainActor class ViewModel: ObservableObject {
        @Published private(set) var images: [UserImage]

        @Published var showingImagePicker = false
        @Published var showingAddImageDetails = false

        @Published var inputImage: UIImage?
        @Published var image: Image?

        // .documentsDirectory is a custom extension I made that points to the documents directory
        let savePath = FileManager.documentsDirectory.appendingPathComponent("ImageContext")

        init() {
            do {
                let data = try Data(contentsOf: savePath)
                images = try JSONDecoder().decode([UserImage].self, from: data) // I get an error here that says UserImage doesn't conform to Decodable
            } catch {
                images = []
            }
        }

        func save() {
            do { 
                let data = try JSONEncoder().encode(images) // I get an error here that says UserImage doesn't conform to Encodable
                try data.write(to: savePath, options: [.atomicWrite, .completeFileProtection])
            } catch {
                print("Unable to save data.")
            }
        }

        func converToSwiftUiImage() {
            guard let inputImage = inputImage else { return }
            let outputImage = Image(uiImage: inputImage)

            image = outputImage
            updateImages()
        }

        // You can ignore this function, it's messy because I'm focusing on getting Codable to work first
        func updateImages() {
            guard let inputImage = inputImage else { return }
            let newUserImage = UserImage(id: UUID(), name: "Test Image", image: inputImage)

            images.append(newUserImage)
            save()
        }
    }
}

   

You haven't declared UserImage as codable, hence the error messages.

struct UserImage: Identifiable, Codable {
    let id: UUID
    var name: String
    let image: UIImage
}

Also UserImage is a single image, and you are decoding and encoding an array of images, so think about that carefully.

images = try JSONDecoder().decode([UserImage].self, from: data)
let data = try JSONEncoder().encode(images)

Finally, Paul has given you the hint to convert UIImage to data - "having an image to disk requires a small extra step: you need to convert your UIImage to Data by calling its jpegData() method". Maybe you have done that elsewhere (calling the jpegData() method), but it is not in the code sample you presented.

   

Thanks for the reply!

Adding Codable conformance to the struct still produces the error messages, sorry about that I probably should've put that in there to be a bit clearer. I believe that's happening because UIImage doesn't automatically conform to Codable, hence my question.

Also, I'm decoding and encoding an array of UserImage because I'm trying to decode and encode the images property, which is an array of UserImage. I thought it was done that way back in the BucketList project to save multiple locations, so I copied that overall structure to this project since I want to encode and decode multiple images to be displayed to the User and not just one. Is there a different way you'd suggest going about it to achieve that goal?

Lastly, no I don't have the jpegData() method anywhere in my code because I couldn't figure out where to put it or how to integrate it. I'm assuming it will allow UserImage to conform to Codable by handling the UIImage property, but I can't figure out how to integrate it and actually make it do that, which is the main part of my question and the biggest part of the challenge that I'm struggling with.

   

Instead of conforming UIImage to Codable, you should write a custom init(from: Decoder) method on your UserImage struct. You would use the jpegData method there to decode the saved data into your struct. Then a custom encode(to: Encoder) method can be used to take your UIImage and turn it into data to save out to JSON.

It's not really a good idea to conform types you don't control to protocols you don't control, which is why I would not add Codable conformance to UIImage but would use manual conformance on UserImage instead.

1      

Thanks so much guys! I think I figured it out with roosterboy's suggestion because all my errors are gone and photos seem to be getting saved successfully, but would you mind giving me your thoughts on the way I put it all together? Anything that could be cleaner or that you'd do differently? The biggest thing that I'm not sure about is the nil coalescing with the following line in Initializer 2:

let decodedImage = UIImage(data: imageData) ?? UIImage()

Also, why do I need this line in my encode(to:) method (my code compiles just fine without it):

try? jpegData.write(to: savePath, options: [.atomic, .completeFileProtection])

If I'm using this line for encoding (also in my encode(to:) method:

try container.encode(jpegData, forKey: .image)

Here's my updated UserImage struct. Thanks again!

struct UserImage: Codable, Identifiable {
    enum CodingKeys: CodingKey {
        case id, name, image
    }

    let savePath = FileManager.documentsDirectory.appendingPathComponent("ImageContext")

    let id: UUID
    var name: String
    let image: UIImage

    // Initializer 1: For creating a UserImage instance elsewhere in the app
    init(id: UUID, name: String, image: UIImage) {
        self.id = id
        self.name = name
        self.image = image
    }

    // Initializer 2: For decoding the encoded data
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        id = try container.decode(UUID.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)

        let imageData = try container.decode(Data.self, forKey: .image)
        let decodedImage = UIImage(data: imageData) ?? UIImage()
        self.image = decodedImage
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        if let jpegData = image.jpegData(compressionQuality: 0.8) {
            try? jpegData.write(to: savePath, options: [.atomic, .completeFileProtection]) // Do I need this line? Everything works without it
            try container.encode(jpegData, forKey: .image)
        }
    }
}

   

Hacking with Swift is sponsored by Emerge

SPONSORED Why are Swift reference types bad for app startup time, and what’s the performance cost of protocol conformances? That’s just a couple of the topics you can learn about on the Emerge blog — written by the app performance experts behind Emerge’s advanced app optimization and monitoring tools, based on their experience of working at companies like Apple, Airbnb, Snap, and Spotify.

Find out more

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

Reply to this topic…

You need to create an account or log in to reply.

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.