在swiftui中保存照片?

Posted

技术标签:

【中文标题】在swiftui中保存照片?【英文标题】:Saving photos on view in swiftui? 【发布时间】:2021-08-13 14:49:25 【问题描述】:

我在视图中添加了一张照片,但是在关闭应用程序后,照片被删除了,我该如何保存照片? 是否有某种属性包装器? 提前谢谢!

ContentView(这是存储用户选择的照片的地方):

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(相机和图库的功能在这里详细说明):

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
        
    
    

【问题讨论】:

【参考方案1】:

您可以将文件存储在缓存和文档目录中。

首先你必须检索他们的网址(这里是cachesDirectory):

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

你不能写一个 UIImage,你必须先检索它的数据。

let data = uiImage.pngData()

然后构建一个路径,将写入此数据。 并创建文件。

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

阅读,原理是一样的。 我们得到路径和try 来读取数据。 Data 构造函数可能会引发错误。 这里我不处理错误。 那么你所要做的就是使用这些数据来构建一个 UIImage(UIImage 构造函数不会抛出错误,但如果失败则返回 nil)。

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

让我们把所有这些都放在一个class 中,这将帮助我们处理这些读/写。

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)
    

警告:如果图像很大,或者您需要同时访问很多图像,异步启动这些读/写操作会很有用。但这是另一个问题......

【讨论】:

【参考方案2】:

您可以使用@AppStorage("key") 而不是@State 来获得更简单的东西,例如StringInt,它会将其保存到UserDefaults

但是UserDefaults 不能直接与UIImage 一起工作(所以@AppStorage 也不能)

遗憾的是AppStorage 无法成功扩展以添加对UIImage 的支持

    您可以使用AppStorageCompat,它是AppStorageios 13 的端口,并且由于它具有开源代码,因此可以通过转换为Data 轻松扩展以存储UIImage
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
    如果您正在寻找更稳定的解决方案,将图像存储在没有UserDefaults 的磁盘上会给您带来更好的性能。您可以创建自己的@propertyWrapper,如下所示:
@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://***.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

【讨论】:

【参考方案3】:

您可以使用默认存储图像数据并从图像数据中获取图像。

保存在这里

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

到这里

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))
        
    
    

【讨论】:

UserDefaults 用于存储图像虽然...? UserDefault 适合单张图片。 头像需要一张照片。

以上是关于在swiftui中保存照片?的主要内容,如果未能解决你的问题,请参考以下文章

通过共享扩展从图库中保存照片时 NSData WriteToFile 失败

在Flutter中保存照片(尤其是相机胶卷)

如何使用 SwiftUI 在 iOS 上获取和显示联系人照片?

SwiftUI 如何在新视图中链接 ForEach 索引和照片索引?

从照片 SwiftUI 中获取图像时内存不足

现代图片选择器(PHPicker)在 SwiftUI 应用