在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
来获得更简单的东西,例如String
或Int
,它会将其保存到UserDefaults
但是UserDefaults
不能直接与UIImage
一起工作(所以@AppStorage
也不能)
遗憾的是AppStorage
无法成功扩展以添加对UIImage
的支持
-
您可以使用AppStorageCompat,它是
AppStorage
到ios 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 失败
如何使用 SwiftUI 在 iOS 上获取和显示联系人照片?