1

I add a photo to the view, but after closing the application, the photo is removed, how can I save the photo? Is there some kind of property wrapper? Thank you in advance!

ContentView (This is where the photos that the user has selected are stored):

struct ContentView: View {
    
    @State private var showSheet: Bool = false
    @State private var showImagePicker: Bool = false
    @State private var sourceType: UIImagePickerController.SourceType = .camera
    
    @State private var image: UIImage? 
    
    var body: some View {
        
            
            VStack {
                
                Image(uiImage: image ?? UIImage(named: "unknown")!)
                    .resizable()
                    .frame(width: 300, height: 300)
                
                Button("change your avatar") {
                    self.showSheet = true
                }.padding()
                    .actionSheet(isPresented: $showSheet) {
                        ActionSheet(title: Text("Changing the avatar"), message: Text("Choosing a photo"), buttons: [
                            .default(Text("Gallery")) {
                                self.showImagePicker = true
                                self.sourceType = .photoLibrary
                            },
                            .default(Text("Camera")) {
                                self.showImagePicker = true
                                self.sourceType = .camera
                            },
                            .cancel()
                        ])
                }
                
            }
            .padding(.bottom, 70)
            .sheet(isPresented: $showImagePicker) {
            ImagePicker(image: self.$image, isShown: self.$showImagePicker, sourceType: self.sourceType)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

ImagePicker (The functions of the camera and gallery are spelled out here):

import Foundation
import SwiftUI

class ImagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
    
    @Binding var image: UIImage?
    @Binding var isShown: Bool
    
    init(image: Binding<UIImage?>, isShown: Binding<Bool>) {
        _image = image
        _isShown = isShown
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        if let uiImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            image = uiImage
            isShown = false
        }
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        isShown = false 
    }
}


struct ImagePicker: UIViewControllerRepresentable {
    
    typealias UIViewControllerType = UIImagePickerController
    typealias Coordinator = ImagePickerCoordinator
    
    @Binding var image: UIImage?
    @Binding var isShown: Bool
    
    var sourceType: UIImagePickerController.SourceType = .camera
    
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
    }
    
    func makeCoordinator() -> ImagePicker.Coordinator {
        return ImagePickerCoordinator(image: $image, isShown: $isShown)
    }
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        
        let picker = UIImagePickerController()
        picker.sourceType = sourceType
        picker.delegate = context.coordinator
        return picker
        
    }
    
}

3 Answers3

1

You can store files in caches and documents directories.

First you have to retrieve their urls (here cachesDirectory):

var fm = FileManager.default
var cachesDirectoryUrl: URL {
   let urls = fm.urls(for: .cachesDirectory, in: .userDomainMask)
   return urls[0]
}

You cannot write an UIImage, you must first retrieve its data.

let data = uiImage.pngData()

Then build a path, where this data will be written. And create the file.

let fileUrl = cachesDirectoryUrl.appendingPathComponent("\(name).png")
let filePath = fileUrl.path
fm.createFile(atPath: filePath, contents: data)

To read, the principle is the same. We get the path and try to read the data. The Data constructor can throw an error. Here I am not dealing with the error. Then all you have to do is use this data to build an UIImage (the UIImage constructor does not throw an error but returns nil if it fails).

if fm.fileExists(atPath: filePath),
   let data = try? Data(contentsOf: fileUrl){
     return UIImage(data: data)
}

Let's put all of this in a class that will help us to handle these reads / writes.

class ImageManager {
    static var shared = ImageManager()
    var fm = FileManager.default
    var cachesDirectoryUrl: URL {
        let urls = fm.urls(for: .cachesDirectory, in: .userDomainMask)
        return urls[0]
    }
    
    init() {
        print(cachesDirectoryUrl)
    }
    
    func getImage(name: String) -> UIImage? {
        let fileUrl = cachesDirectoryUrl.appendingPathComponent("\(name).png")
        let filePath = fileUrl.path
        if fm.fileExists(atPath: filePath),
           let data = try? Data(contentsOf: fileUrl){
            return UIImage(data: data)
        }
        return nil
    }
    
    func writeImage(name: String, uiImage: UIImage) {
        let data = uiImage.pngData()
        let fileUrl = cachesDirectoryUrl.appendingPathComponent("\(name).png")
        let filePath = fileUrl.path
        fm.createFile(atPath: filePath, contents: data)
    }
}

Warning: if the images are large, or if you need to access a lot of images at the same time, it can be useful to start these reads / writes asynchronously. But it's another problem...

Adrien
  • 1,579
  • 6
  • 25
1

You could've use @AppStorage("key") instead of @State for something simpler like String or Int, it'll save it to UserDefaults

But UserDefaults cannot work directly with UIImage(and so @AppStorage cannot too)

Sadly AppStorage cannot be successfully extended to add support for UIImage

  1. You can use AppStorageCompat which is a port of AppStorage to iOS 13, and as it has open source code it can be easily extended to store UIImage by converting to Data:
extension AppStorageCompat where Value == UIImage? {
    public init(wrappedValue: Value, _ key: String, store: UserDefaults? = nil) {
        let store = (store ?? .standard)
        let initialValue = (store.value(forKey: key) as? Data)
            .flatMap(UIImage.init(data:))
            ?? wrappedValue
        self.init(value: initialValue, store: store, key: key, transform: {
            $0 as? Value
        }, saveValue: { newValue in
            store.setValue(newValue?.pngData(), forKey: key)
        })
    }
}

//usage
@AppStorageCompat("userDefaultsKey")
private var image: UIImage? = nil
  1. If you're looking for a more stable solution, storing images on the disk without UserDefaults will give you better performance. You can create your own @propertyWrapper, like this:
@propertyWrapper struct ImageStorage: DynamicProperty  {
    private static let storageDirectory = FileManager.default
        .documentsDirectory()
        .appendingPathComponent("imageStorage")
    
    private static func storageItemUrl(_ key: String) -> URL {
        storageDirectory
            .appendingPathComponent(key)
            .appendingPathExtension("png")
    }
    private let backgroundQueue = DispatchQueue(label: "ImageStorageQueue")
    
    private let key: String
    
    init(wrappedValue: UIImage?, _ key: String) {
        self.key = key
        _wrappedValue = State(initialValue: UIImage(contentsOfFile: Self.storageItemUrl(key).path) ?? wrappedValue)
    }
    
    @State
    var wrappedValue: UIImage? {
        didSet {
            let image = wrappedValue
            backgroundQueue.async {
                // png instead of pngData to fix orientation https://stackoverflow.com/a/42098812/3585796
                try? image?.png()
                    .flatMap {
                        let url = Self.storageItemUrl(key)
                        // createDirectory will throw an exception if directory exists but this is fine
                        try? FileManager.default.createDirectory(at: Self.storageDirectory)
                        // removeItem will throw an exception if there's not file but this is fine
                        try? FileManager.default.removeItem(at: url)
                        
                        try $0.write(to: url)
                    }
            }
        }
    }

    var projectedValue: Binding<UIImage?> {
        Binding(
            get: { self.wrappedValue },
            set: { self.wrappedValue = $0 }
        )
    }
}

extension FileManager {
    func documentsDirectory() -> URL {
        urls(for: .documentDirectory, in: .userDomainMask)[0]
    }
}

extension UIImage {
    func png(isOpaque: Bool = true) -> Data? { flattened(isOpaque: isOpaque).pngData() }
    func flattened(isOpaque: Bool = true) -> UIImage {
        if imageOrientation == .up { return self }
        let format = imageRendererFormat
        format.opaque = isOpaque
        return UIGraphicsImageRenderer(size: size, format: format).image { _ in draw(at: .zero) }
    }
}

//usage
@ImageStorage("imageKey")
private var image: UIImage? = nil
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
0

You can store image data using default and get back image from the image data.

Save here

 Image(uiImage: image ?? UIImage())
                .resizable()
                .frame(width: 300, height: 300)
                .onChange(of: image, perform: { newImage in // <-- Here
                    UserDefaults.standard.setValue(newImage?.pngData(), forKey: "image")
                })

Get here

struct ContentView: View {
    @State private var image: UIImage?
    
    init() {// <-- Here
        if let imageData = UserDefaults.standard.data(forKey: "image") {
            self._image = State(wrappedValue: UIImage(data: imageData))
        }
    }
    
Raja Kishan
  • 16,767
  • 2
  • 26
  • 52