https://www.kodeco.com/books/swiftui-cookbook/v1.0/chapters/10-use-the-swiftui-photospicker
PhotosPicker
提供了一种从用户照片库导入一张或多张照片的简单方法。为了避免造成任何性能问题,数据以 PhotosPickerItem
的特殊类型提供给我们,然后我们可以异步加载该类型以将数据转换为 SwiftUI 图像。
//使用 PhotosPicker,首先要导入 PhotosUI 框架:
import PhotosUI
TBD。
// 创建两个属性:这两个区别很重要,因为在实际要求加载图片之前,所选项目只是对照片库中图片的引用
// 一个用于存储所选图像项目
// 一个用于将该所选图像项目,存储为 SwiftUI 图像
@State private var pickerItem: PhotosPickerItem?
@State private var selectedImage: Image?
// 在视图结构中的某个位置添加 PhotosPicker 视图。这里包含3个参数
// - 向用户展示的标题
// - 绑定前面定义的状态属性(PhotosPickerItem 类型的),用于存储所选图像
// - matching 代表允许访问相册的数据类型,例如 .images 代表所有图像, .screenshot 代表截图
VStack {
PhotosPicker(
"Select a picture",
selection: $pickerItem,
matching: .images
)
}
// 这一步是观察 pickerItem 的变化,因为当它变化时,意味着用户已经选择了一张图片供我们加载
// 完成后可以在 pickerItem 上调用 loadTransferable(type:) 方法,将实际的底层数据从 pickerItem 加载到 SwiftUI 图像中
// 如果成功,可以将结果值分配给之前定义的状态属性 selectedImage
// 调用 loadTransferable(type:) 可能需要几秒钟,特别是对于全景图等大图片,因此要放到 Task 中,使用 try await
VStack {
PhotosPicker("Select a picture", selection: $pickerItem, matching: .images)
}
.onChange(of: pickerItem) {
Task {
selectedImage = try await pickerItem?.loadTransferable(type: Image.self)
}
}
// 最后一步是在某处显示加载的 SwiftUI 图像。将其添加到 VStack 中,位于 PhotosPicker 之前或之后:
VStack {
PhotosPicker("Select a picture", selection: $pickerItem, matching: .images)
selectedImage?
.resizable()
.scaledToFit()
}
当想要支持选择多个图像时, 可以将选择器绑定到一个 PhotosPickerItem
的数组,并让用户选择多个项目。
//1. 这需要创建一个选择器项目数组作为状态属性
@State private var pickerItems = [PhotosPickerItem]()
//2. 像前面一样绑定该数组
PhotosPicker("Select images", selection: $pickerItems, matching: .images)
//3. 因为无法再观看单个照片选择器项目,所以应该创建一个数组来存储加载的图像:
@State private var selectedImages = [Image]()
//4. 最后更新 onChange() ,以便在选择新项目时清除该数组,然后单独加载新集:
.onChange(of: pickerItems) {
Task {
//先清除原来的数据,不用一个个替换
selectedImages.removeAll()
//然后遍历数组中的项目,一个个赋予前面的储存所选图像的状态属性
for item in pickerItems {
if let loadedImage = try await item.loadTransferable(type: Image.self) {
selectedImages.append(loadedImage)
}
}
}
}
//5. 使用 ForEach 或类似的方式显示它们
ScrollView {
ForEach(0..<selectedImages.count, id: \\.self) {
i in
selectedImages[i]
.resizable()
.scaledToFit()
}
}
与许多 SwiftUI 视图一样,您可以提供完全自定义的标签,这可能是 Label 视图或完全自定义的内容
// 例子1: 简单的Label
PhotosPicker(selection: $pickerItems, maxSelectionCount: 3, matching: .images) {
Label("Select a picture", systemImage: "photo")
}
// 例子2: 把更复杂的视图当作 label,点击全部均可触发
PhotosPicker(selection: $selectedItem, matching: .images) {
if let processedImage {
processedImage
.resizable()
.scaledToFit()
} else {
ContentUnavailableView("No Picture", systemImage: "photo.badge.plus", description: Text("Import"))
}
}
// 这有点像网页的 a 链接里包含别的标签的写法,这样点击整体都可以触发。并且和网页一样,这种做法会让 ContentUnavailableView 变成蓝色以表示可交互式。如果不喜欢,可以通过将 .buttonStyle(.plain) 添加到 PhotosPicker 来禁用它。
//如果要允许用户选择多张照片,建议通过添加 maxSelectionCount 参数来限制实际上一次可以选择的图片数量,如下所示:
PhotosPicker("Select images", selection: $pickerItems, maxSelectionCount: 3, matching: .images)
自定义 matching
属性,限制可以导入的图片种类: 前面使用了 .images
代表将获得常规照片、屏幕截图、全景图等
//例如,传入以下数组可以匹配除屏幕截图之外的所有图像:
PhotosPicker(selection: $pickerItems, maxSelectionCount: 3, matching: .any(of: [.images, .not(.screenshots)]))
还可以使用 .any()
、 .all()
和 .not()
等更高级的过滤器,甚至向它们传递一个数组
matching: .screenshots
matching: .any(of: [.panoramas, .screenshots])
matching: .not(.videos)
matching: .any(of: [.images, .not(.screenshots)]))
PhotosPicker
允许用户选择视频并将其带入应用程序中,但根据经验,需要以相当精确的方式使用它以避免出现问题。
// 例如:一个完整的代码
// 首先,需要导入 AVKit 才能访问 VideoPlayer 视图,并且需要 PhotosUI 框架才能访问 PhotosPicker
import AVKit
import PhotosUI
import SwiftUI
// 其次,自定义 Movie 结构是告诉 SwiftUI 导入电影数据的方式。
// 它通过将其 URL 转换为 SentTransferredFile 使用 Transferable 发送数据,这意味着可以将 Movie 实例拖出应用程序。它还可以使用 importing 闭包接收数据:它将电影 URL 作为“movie.mp4”复制到我们的文档目录,并删除任何现有文件。
struct Movie: Transferable {
let url: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .movie) {
movie in
SentTransferredFile(movie.url)
} importing: {
received in
let copy = URL.documentsDirectory.appending(path: "movie.mp4")
if FileManager.default.fileExists(atPath: copy.path()) {
try FileManager.default.removeItem(at: copy)
}
try FileManager.default.copyItem(at: received.file, to: copy)
return Self.init(url: copy)
}
}
}
struct ContentView: View {
// 导入电影可能需要一些时间,因此需要确保用户在应用程序运行时了解我们的导入状态。
// 这是通过具有四种情况的枚举来处理的: unknown 当应用程序启动时, loading 显示进度旋转器, loaded 当我们完成 Movie 可以使用, failed 当导入由于某种原因失败时。
enum LoadState {
case unknown, loading, loaded(Movie), failed
}
@State private var selectedItem: PhotosPickerItem?
@State private var loadState = LoadState.unknown
var body: some View {
VStack {
PhotosPicker("Select movie", selection: $selectedItem, matching: .videos)
switch loadState {
case .unknown:
EmptyView()
case .loading:
ProgressView()
case .loaded(let movie):
//加载完后展示播放器
VideoPlayer(player: AVPlayer(url: movie.url))
.scaledToFit()
.frame(width: 300, height: 300)
case .failed:
Text("Import failed")
}
}
//最后,在 onChange() 修饰符中,要求系统为我们提供一个 Movie 实例,以便接受 URL 并将其移动到我们的应用程序使用的正确位置。这还负责设置 loadState 属性,以便我们的 UI 保持同步。
.onChange(of: selectedItem) { _ in
Task {
do {
loadState = .loading
if let movie = try await selectedItem?.loadTransferable(type: Movie.self) {
loadState = .loaded(movie)
} else {
loadState = .failed
}
} catch {
loadState = .failed
}
}
}
}
}