https://www.kodeco.com/books/swiftui-cookbook/v1.0/chapters/10-use-the-swiftui-photospicker


PhotosPicker

PhotosPicker 提供了一种从用户照片库导入一张或多张照片的简单方法。为了避免造成任何性能问题,数据以 PhotosPickerItem 的特殊类型提供给我们,然后我们可以异步加载该类型以将数据转换为 SwiftUI 图像。

使用 PhotosUI 框架

//使用 PhotosPicker,首先要导入 PhotosUI 框架:
import PhotosUI

PhotosPickerItem 类型

TBD。


选择单个图像

1. 创建状态属性储存项目

// 创建两个属性:这两个区别很重要,因为在实际要求加载图片之前,所选项目只是对照片库中图片的引用
// 一个用于存储所选图像项目
// 一个用于将该所选图像项目,存储为 SwiftUI 图像

@State private var pickerItem: PhotosPickerItem?
@State private var selectedImage: Image?

2. 添加 PhotosPicker 视图

// 在视图结构中的某个位置添加 PhotosPicker 视图。这里包含3个参数
// - 向用户展示的标题
// - 绑定前面定义的状态属性(PhotosPickerItem 类型的),用于存储所选图像
// - matching 代表允许访问相册的数据类型,例如 .images 代表所有图像, .screenshot 代表截图
VStack {
    PhotosPicker(
	    "Select a picture", 
	    selection: $pickerItem, 
	    matching: .images
	  )
}

3. onChange 监控所选项目并解码

// 这一步是观察 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)
    }
}

4. 添加 PhotosPicker 视图

// 最后一步是在某处显示加载的 SwiftUI 图像。将其添加到 VStack 中,位于 PhotosPicker 之前或之后:
VStack {
    PhotosPicker("Select a picture", selection: $pickerItem, matching: .images)
		selectedImage?
		    .resizable()
		    .scaledToFit()
}


选择多个图像

当想要支持选择多个图像时, 可以将选择器绑定到一个 PhotosPickerItem 的数组,并让用户选择多个项目。

1. 改用数组作为状态属性

//1. 这需要创建一个选择器项目数组作为状态属性
@State private var pickerItems = [PhotosPickerItem]()

//2. 像前面一样绑定该数组
PhotosPicker("Select images", selection: $pickerItems, matching: .images)

//3. 因为无法再观看单个照片选择器项目,所以应该创建一个数组来存储加载的图像:
@State private var selectedImages = [Image]()

2. 展示所选项目

//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()
    }
}

自定义图像选择器

1. 自定义 PhotosPicker 视图

与许多 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 来禁用它。

2. 限制可选项目的数量

//如果要允许用户选择多张照片,建议通过添加 maxSelectionCount 参数来限制实际上一次可以选择的图片数量,如下所示:
PhotosPicker("Select images", selection: $pickerItems, maxSelectionCount: 3, matching: .images)

3. 限制可选项目的类型

自定义 matching 属性,限制可以导入的图片种类: 前面使用了 .images 代表将获得常规照片、屏幕截图、全景图等

//例如,传入以下数组可以匹配除屏幕截图之外的所有图像:
PhotosPicker(selection: $pickerItems, maxSelectionCount: 3, matching: .any(of: [.images, .not(.screenshots)])) 

还可以使用 .any().all().not() 等更高级的过滤器,甚至向它们传递一个数组


用 PhotosPicker 导入视频

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