skip to Main Content

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

Answers


  1. 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))
            }
        }
        
    
    Login or Signup to reply.
  2. 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…

    Login or Signup to reply.
  3. 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
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search