JR.
JR.

Reputation: 463

How can I save a SwiftUI image to disk or get the data out?

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

Answers (3)

Thyselius
Thyselius

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

Ahmadreza
Ahmadreza

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

kontiki
kontiki

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

Related Questions