Reputation: 463
I'm trying to save a SwiftUI image (not a UIImage from UIKit) to disk.
I've looked everywhere but could not find any documented information on how to do this. I also cannot see a way to extract the Data from the SwiftUI image.
Can anybody help? Thanks in advance.
Upvotes: 14
Views: 16016
Reputation: 954
Here's a class that I wrote, works well in ios 16. It loads the image data and saves it to disk. If the image is already stored on disk it loads the image from disk instead.
Usage:
ImageFromWebOrDisk(baseURL:"https://www.example.com/images/", filename:"myimage.jpg")
.mask(
RoundedRectangle(cornerRadius: 16)
)
Class:
import SwiftUI
struct ImageFromWebOrDisk: View {
var baseURL:String
var filename:String
@State var image: UIImage?
var body:some View {
VStack {
if let image = image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
ProgressView()
//Text("Loading image...")
}
}
.onAppear {
loadImage()
}/*
.onChange(of: filename) { _ in
print("load image: \(filename)")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
self.loadImage()
}
}*/
}
func loadImage() {
let fileURL = getDocumentsDirectory().appendingPathComponent(filename)
if FileManager.default.fileExists(atPath: fileURL.path) {
// Load image from disk
if let imageData = try? Data(contentsOf: fileURL),
let loadedImage = UIImage(data: imageData) {
image = loadedImage
print("Loaded image from disk: \(fileURL)")
return
}
}
// Image not found on disk, fetch from URL
loadImageFromURL(urlString: "\(baseURL)\(filename)") { loadedImage in
guard let loadedImage = loadedImage else {
return
}
print("Loaded from URL: \(fileURL)")
image = loadedImage
saveImageToDisk(image: loadedImage, filename: filename)
}
}
func loadImageFromURL(urlString: String, completion: @escaping (UIImage?) -> Void) {
guard let url = URL(string: urlString) else {
completion(nil)
return
}
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
completion(nil)
return
}
let loadedImage = UIImage(data: data)
completion(loadedImage)
}.resume()
}
func saveImageToDisk(image: UIImage?, filename:String) {
guard let image = image,
let imageData = image.jpegData(compressionQuality: 0.9) else {
return
}
let fileURL = getDocumentsDirectory().appendingPathComponent(filename)
do {
try imageData.write(to: fileURL)
print("Image saved to disk: \(fileURL)")
} catch {
print("Failed to save image to disk: \(error)")
}
}
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
}
UPDATE REQUEST: If someone can make it CHANGE image if url changes, that'd be great.
Right now how I solve it is the following:
if showPhotoWithURL != "" {
ImageFromWebOrDisk(baseURL:GlobalSettings.avatarImagesBase, filename:"\(showPhotoWithURL).jpg")
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width)
} else {
ProgressView()
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.width)
}
And the button action:
Button(action: {
showPhotoWithURL = ""
// to reload the image
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
self.showPhotoWithURL = message.answer.image
}
}
Upvotes: 0
Reputation: 7212
I convert the SwiftUI image to UIImage then save it to UserDefaults.
It's not the best solution, but I didn't find better one:
// reading the saved image from userDefaults
private func getSavedImage(for size: String) -> Image? {
if let data = UserDefaults.standard.data(forKey: size) {
if let image = UIImage(data: data) {
return Image(uiImage: image)
}
}
return nil
}
// Converting SwiftUI Image to UIImage
extension View {
public func toUIImage() -> UIImage {
let controller = UIHostingController(rootView: self)
controller.view.frame = CGRect(x: 0, y: CGFloat(Int.max), width: 1, height: 1)
#if targetEnvironment(macCatalyst)
UIApplication.shared.windows.first!.rootViewController?.view.addSubview(controller.view)
#else
var window: UIWindow? {
guard let scene = UIApplication.shared.connectedScenes.first,
let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
let window = windowSceneDelegate.window else {
return nil
}
return window
}
if let window = window, let rootViewController = window.rootViewController {
var topController = rootViewController
while let newTopController = topController.presentedViewController {
topController = newTopController
}
topController.view.insertSubview(controller.view, at: controller.view.subviews.count)
} else {
print("cant access window")
}
#endif
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
controller.view.sizeToFit()
// here is the call to the function that converts UIView to UIImage: `.asImage()`
let image = controller.view.toUIImage()
controller.view.removeFromSuperview()
return image
}
}
Using the top helper & save the image:
extension Image {
func toData()-> Data {
return self.toUIImage().jpegData(compressionQuality: 1)!
}
func save(for name: String) {
UserDefaults.standard.setValue(self.toData(), forKey: name)
}
}
Upvotes: 1
Reputation: 40489
With SwiftUI, things work a little differently. What you want to do, cannot be done in that fashion. Instead, you need to look at how the image was created and obtain the image data from the same place that your Image()
got it in the first place.
Also if you need the actual binary data to save it to disk, you need a UIImage
(note that I am not saying UIImageView
).
Fortunately, Image can handle UIImage
too, check this other question for that: https://stackoverflow.com/a/57028615/7786555
Upvotes: 6