SwiftUI使用PhotoUI
在wwdc2022上,苹果让SwiftUI支持了PhotoUI。PhotoUI是属于PhotoKit的一个组件,使用PhotoUI,可以很方便的实现选择图片的功能。
使用PhotoUI选择文件需要处理三个步骤:
步骤一: 使用PhotoPicker打开图片选择视窗。
@State var photoPickerItem: PhotosPickerItem? = nil
var body: some View {
PhotoPicker(selection: $photoPickerItem) {
Text("tap to pick")
}
}
PhotoPicker有许多的构造器,可以设置单选还是多选,可以设置允许选择的图片数量,可以设置图片(或视频)类型。
具体的设置可以查看官方文档。
PhotoPicker实质上是一个按钮,所以可以使用buttonStyle对PhotoPicker进行样式设置。
Text部分则是按钮的Label。
步骤二: 创建一个Transferable对象用于处理图片转换。
因为PhotoPicker获取到的仅是图片的索引(官方称之为Placeholder),实际获取到图片还需要将图片加载到应用中,有时候由于图片尺寸过大或者图片位于iCloud上需要等待下载,或者是请求到视频文件(需要做类型适配)等等原因,所以需要一个转换器对索引进行转换。
struct ImageTransfer: Transferable {
let image: Image
enum TransferError: Error {
case importFailed
}
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(importedContentType: .image) { data in
#if canImport(AppKit)
guard let nsImage = NSImage(data: data) else {
throw TransferError.importFailed
}
let image = Image(nsImage: nsImage)
return TransferImage(image: image)
#elseif canImport(UIKit)
guard let uiImage = UIImage(data: data) else {
throw TransferError.importFailed
}
let image = Image(uiImage: uiImage)
return TransferImage(image: image)
#else
throw TransferError.importFailed
#endif
}
}
}
这里使用了宏编程的方法处理各个平台的图片
步骤三: 在PhotoPicker获取到图片索引后解析图片
enum ImageState {
case empty
case loading(Progress)
case success(Image)
case failure(Error)
}
@State
var imageState: ImageState = .empty
首先创建一个imageState来存放解析过程,后面会通过这个imageState的不同阶段显示不同的视图。
private func loadTransferable(from photoPickerItem: PhotoPickerItem) -> Progress {
return photoPickerItem.loadTransferable(type: ImageTransfer.self) { res in
DispatchQueue.main.async {
guard let photoPickerItem = self.photoPickerItem else {
print("Failed to get the selected item.")
return
}
switch res {
case .success(let transferImage?):
self.imageState = .success(transferImage.image)
case .success(nil):
self.imageState = .empty
case .failure(let error):
self.imageState = .failure(error)
}
}
}
}
然后创建一个转换函数用于处理转换。这里使用了异步方法来处理转换,以免阻塞视图渲染。
PhotoPicker(selection: $photoPickerItem) {
Text("tap to pick")
}
.onChange(of: photoPickerItem) {
if let photoPickerItem = $0 {
let progress = loadTransferable(from: photoPickerItem)
imageState = .loading(progress)
} else {
imageState = .empty
}
}
最后在选择图片后调用转换函数处理图片。
可以看到,我们是通过photoPickerItem的属性方法来触发转换。然后将imageState置为loading,等待转换处理完成后,根据处理的结果再将imageState切换为对应的状态。
最后一步: 根据imageState显示对应的视图
VStack {
PhotoPicker(selection: $photoPickerItem) {
Text("tap to pick")
}
.onChange(of: photoPickerItem) {
if let photoPickerItem = $0 {
let progress = loadTransferable(from: photoPickerItem)
imageState = .loading(progress)
} else {
imageState = .empty
}
}
switch imageState {
case .success(let image):
image.resizable()
.aspectRatio(contentMode: .fit)
case .loading:
ProgressView()
case .empth:
Image(systemName: "photo.fill")
.font(.system(size: 40)
.foregroundColor(.gray)
case .failure:
Image(systemName: "rectangle.slash.fill")
.font(.system(size: 40)
.foregroundColor(.gray)
}
}
这部分就比较简单了,就不展开了。
完整代码:
struct ContentView: View {
@State
var photoPickerItem: PhotosPickerItem? = nil
@State
var imageState: ImageState = .empty {
didSet {
print("imageState: ", imageState)
}
}
var body: some View {
VStack {
PhotosPicker(selection: $photoPickerItem) {
Text("tap to pick")
}
.onChange(of: photoPickerItem) { newValue in
if let photoPickerItem {
let progress = loadTransferable(from: photoPickerItem)
imageState = .loading(progress)
} else {
imageState = .empty
}
}
switch imageState {
case .success(let image):
image.resizable()
.aspectRatio(contentMode: .fit)
case .loading:
ProgressView()
case .empty:
Image(systemName: "photo.fill")
.font(.system(size: 40))
.foregroundColor(.blue)
case .failure:
Image(systemName: "rectangle.slash.fill")
.font(.system(size: 40))
.foregroundColor(.blue)
}
}
}
enum ImageState {
case empty
case loading(Progress)
case success(Image)
case failure(Error)
}
struct ImageTransfer: Transferable {
let image: Image
enum TransferError: Error {
case importFailed
}
static var transferRepresentation: some TransferRepresentation {
DataRepresentation(importedContentType: .image) { data in
#if canImport(AppKit)
guard let nsImage = NSImage(data: data) else {
throw TransferError.importFailed
}
let image = Image(nsImage: nsImage)
return TransferImage(image: image)
#elseif canImport(UIKit)
guard let uiImage = UIImage(data: data) else {
throw TransferError.importFailed
}
let image = Image(uiImage: uiImage)
return ImageTransfer(image: image)
#else
throw TransferError.importFailed
#endif
}
}
}
private func loadTransferable(from photoPickerItem: PhotosPickerItem) -> Progress {
print("loadTransferable")
return photoPickerItem.loadTransferable(type: ImageTransfer.self) { res in
DispatchQueue.main.async {
guard let photoPickerItem = self.photoPickerItem else {
print("Failed to get the selected item.")
return
}
switch res {
case .success(let transferImage?):
self.imageState = .success(transferImage.image)
case .success(nil):
self.imageState = .empty
case .failure(let error):
self.imageState = .failure(error)
}
}
}
}
}