Save images in SwiftData

Hello everyone, Is there a better solution than my approach out there to convert an image to data and back?

@Model
class User {
    var name: String
    @Attribute(.externalStorage) var image: Data?
    var createdAt: Date
    
    init(name: String, image: Data, createdAt: Date = .now) {
        self.name = name
        self.image = image
        self.createdAt = createdAt
    }
}
if let selectedPhotoData = imageData,
                       let uiImage = UIImage(data: selectedPhotoData) {
                        Image(uiImage: uiImage)
                            .resizable()
                            .scaledToFill()
                            .frame(width: 300, height: 300, alignment: .center)
                            .clipShape(Circle())
                    }
.task(id: selectedPhoto) {
            if let data = try? await selectedPhoto?.loadTransferable(type: Data.self) {
                imageData = data
            }
        }

Accepted Reply

No there isn't I'm afraid. You have to convert to Data.

I would caution against using loadTransferable as the Transferable type is intended to short term storage (copy and paste, drag and drop...etc), and there is no guarantee how that API may change in the future. If you're storing data for extended periods, you should try and ensure consistency of the kind of data stored.

In our app, we have a generic SwiftData entity for storing an image. Any other entity that wants to store an image can maintain a relationship to one of these entities. It saves us having to add logic everywhere we want to store images. The generic image entity takes an NSImage, UIImage or CGImage and grabs the data in PNG format and stores it in the model. Then extensions on those types to initialise them directly from the SwiftData entity. It makes things a little cleaner, but it's essentially the same thing, especially considering we store images in multiple places in our model.

An extension on NSImage to get the PNG data.

extension NSImage {
/// Returns the PNG data for the `NSImage` as a Data object.
///
/// - Returns: A data object containing the PNG data for the image, or nil
/// in the event of failure.
///
	public func pngData() -> Data? {
		guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
			return nil
		}
		
		let bitmapRepresentation = NSBitmapImageRep(cgImage: cgImage)
		return bitmapRepresentation.representation(using: .png, properties: [:])
	}
}

The basic data model for our image. We store a type and some data which is the PNG data.

@Model
final class ImageModel {
/// The type of the image.
///
/// We use different images for different things, so storing an image type
/// lets us differentiate use.
///
	var type: ImageType = ImageType.unknown

/// The image data, stored as a PNG.
///
/// It is tagged with externalStorage to allow the large binary data to be stored
/// externally. 
///
	@Attribute(.externalStorage) var pngData: Data? = nil

/// Initialize the image model.
///
/// - Parameters:
///   - type: The type of image the image model represents.
///   - pngData: The image data in png format.
///
	init(type: ImageType, pngData: Data) {
		self.type = type
		self.pngData = pngData
	}

#if canImport(AppKit)
import AppKit

/// Initialize the image model from an `NSImage`.
///
/// - Parameters:
///   - type: The type of the image the image model represents.
///   - image: The `NSImage` to store in the image model.
///
	convenience init(type: ImageType, image: NSImage) throws {
		guard let pngData = image.pngData() else {
			throw GenericError.failed("Unable to get PNG data for image")
		}
		
		self.init(type: type, pngData: pngData)
	}

#elseif canImport(UIKit)
import UIKit

/// Initialize the image model from a `UIImage`.
///
/// - Parameters:
///   - type: The type of the image the image model represents.
///   - image: The `UIImage` to store in the image model.
///
	convenience init(type: ImageType, image: UIImage) throws {
		guard let pngData = image.pngData() else {
			throw GenericError.failed("Unable to get PNG data for image")
		}
		
		self.init(type: type, pngData: pngData)
	}
#endif
}

Given an ImageModel, initialises a UIImage or NSImage from the data stored in the model.

#if canImport(UIKit)
import UIKit

extension UIImage {
/// Initialize a new `UIImage` using data from an `ImageModel`.
///
/// - Parameters:
///   - model: The image model to load the image from.
///
	convenience init?(loadingDataFrom model: ImageModel) {
		guard let data = model.pngData,
			  data.isEmpty == false
		else {
			return nil
		}
		
		self.init(data: data)
	}
}

#elseif canImport(AppKit)
import AppKit

extension NSImage {
/// Initialize a new `NSImage` using data from an `ImageModel`.
///
/// - Parameters:
///   - model: The image model to load the image from.
///
	convenience init?(loadingDataFrom model: ImageModel) {
		guard let data = model.pngData,
			  data.isEmpty == false
		else {
			return nil
		}
		
		self.init(data: data)
	}
}
#endif
  • Hello, thank you for your detailed feedback. Hopefully they simplify this a bit in the future. But for now I think that looks like the best solution how to fix that.

Add a Comment

Replies

No there isn't I'm afraid. You have to convert to Data.

I would caution against using loadTransferable as the Transferable type is intended to short term storage (copy and paste, drag and drop...etc), and there is no guarantee how that API may change in the future. If you're storing data for extended periods, you should try and ensure consistency of the kind of data stored.

In our app, we have a generic SwiftData entity for storing an image. Any other entity that wants to store an image can maintain a relationship to one of these entities. It saves us having to add logic everywhere we want to store images. The generic image entity takes an NSImage, UIImage or CGImage and grabs the data in PNG format and stores it in the model. Then extensions on those types to initialise them directly from the SwiftData entity. It makes things a little cleaner, but it's essentially the same thing, especially considering we store images in multiple places in our model.

An extension on NSImage to get the PNG data.

extension NSImage {
/// Returns the PNG data for the `NSImage` as a Data object.
///
/// - Returns: A data object containing the PNG data for the image, or nil
/// in the event of failure.
///
	public func pngData() -> Data? {
		guard let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
			return nil
		}
		
		let bitmapRepresentation = NSBitmapImageRep(cgImage: cgImage)
		return bitmapRepresentation.representation(using: .png, properties: [:])
	}
}

The basic data model for our image. We store a type and some data which is the PNG data.

@Model
final class ImageModel {
/// The type of the image.
///
/// We use different images for different things, so storing an image type
/// lets us differentiate use.
///
	var type: ImageType = ImageType.unknown

/// The image data, stored as a PNG.
///
/// It is tagged with externalStorage to allow the large binary data to be stored
/// externally. 
///
	@Attribute(.externalStorage) var pngData: Data? = nil

/// Initialize the image model.
///
/// - Parameters:
///   - type: The type of image the image model represents.
///   - pngData: The image data in png format.
///
	init(type: ImageType, pngData: Data) {
		self.type = type
		self.pngData = pngData
	}

#if canImport(AppKit)
import AppKit

/// Initialize the image model from an `NSImage`.
///
/// - Parameters:
///   - type: The type of the image the image model represents.
///   - image: The `NSImage` to store in the image model.
///
	convenience init(type: ImageType, image: NSImage) throws {
		guard let pngData = image.pngData() else {
			throw GenericError.failed("Unable to get PNG data for image")
		}
		
		self.init(type: type, pngData: pngData)
	}

#elseif canImport(UIKit)
import UIKit

/// Initialize the image model from a `UIImage`.
///
/// - Parameters:
///   - type: The type of the image the image model represents.
///   - image: The `UIImage` to store in the image model.
///
	convenience init(type: ImageType, image: UIImage) throws {
		guard let pngData = image.pngData() else {
			throw GenericError.failed("Unable to get PNG data for image")
		}
		
		self.init(type: type, pngData: pngData)
	}
#endif
}

Given an ImageModel, initialises a UIImage or NSImage from the data stored in the model.

#if canImport(UIKit)
import UIKit

extension UIImage {
/// Initialize a new `UIImage` using data from an `ImageModel`.
///
/// - Parameters:
///   - model: The image model to load the image from.
///
	convenience init?(loadingDataFrom model: ImageModel) {
		guard let data = model.pngData,
			  data.isEmpty == false
		else {
			return nil
		}
		
		self.init(data: data)
	}
}

#elseif canImport(AppKit)
import AppKit

extension NSImage {
/// Initialize a new `NSImage` using data from an `ImageModel`.
///
/// - Parameters:
///   - model: The image model to load the image from.
///
	convenience init?(loadingDataFrom model: ImageModel) {
		guard let data = model.pngData,
			  data.isEmpty == false
		else {
			return nil
		}
		
		self.init(data: data)
	}
}
#endif
  • Hello, thank you for your detailed feedback. Hopefully they simplify this a bit in the future. But for now I think that looks like the best solution how to fix that.

Add a Comment

I am using visionOS.

To your model, I am adding default as nil

@Model
class User {
    var name: String
    @Attribute(.externalStorage) var image: Data?
    var createdAt: Date

    init(name: String, image: Data? = nil, createdAt: Date = .now) {
        self.name = name
        self.image = image
        self.createdAt = createdAt
    }
}

The following code is working for me:

enum ImageConversionError: Error {
    case imageNotFound
    case pngConversionFailed
}

func convertImageToPNG(uiImage: UIImage?) throws -> Data {
    guard let image = uiImage else {
        throw ImageConversionError.imageNotFound
    }
    guard let pngData = image.pngData() else {
        throw ImageConversionError.pngConversionFailed
    }
    return pngData
}

func createUser(name: String, uiImage: UIImage?) -> User {
    do {
        let pngData = try convertImageToPNG(uiImage: uiImage)
        print("Image converted to PNG data successfully.")
        return User(name: name, image: pngData)
    } catch ImageConversionError.imageNotFound {
        print("Error: Image not found.")
    } catch ImageConversionError.pngConversionFailed {
        print("Error: PNG conversion failed.")
    } catch {
        print("An unexpected error occurred: \(error).")
    }
    return User(name: name)
}