大师学SwiftUI第18章Part1 - 图片选择器和相机
如今,个人设备主要用于处理图片、视频和声音,苹果的设备也不例外。SwiftUI可以通过Image
视图显示图片,但需要其它框架的支持来处理图片、在屏幕上展示视频或是播放声音。本章中我们将展示Apple所提供的这类工具。
图片选择器
SwiftUI内置了一个PhotosPicker
结构体用于生成一个视图,允许用户从图片库中选择一张或多张照片。以下为该视图的初始化方法。
- PhotosPicker(selection: Binding, maxSelectionCount: Int?, selectionBehavior: PhotosPickerSelectionBehavior, matching: PHPickerFilter?, preferredItemEncoding: EncodingDisambiguationPolicy, photoLibrary: PHPhotoLibrary, label: Closure):这一初始化方法通过由参数所指定的配置创建一个
PhotosPicker
视图。selection
参数是一个存储所选项指针的绑定属性。maxSelectionCount
参数是我们希望用户选取的最大图片数。selectionBehavior
参数指定如何进行选取。该结构体具有类型属性default
(复选框选取)、ordered
(数字选取)、continous
(实时选取)和continousAndOrdered
(实时数字选择)。matching
参数指定视图所包含的资源类型。这个结构体的类型属性有bursts
、cinematicVideos
、depthEffectPhotos
、images
、livePhotos
、panoramas
、screenRecordings
、screenshots
、slomoVideos
、timelapseVideos
和videos
。preferredItemEncoding
参数指定用于处理资源的编码。这个结构体包含类型属性automatic
(默认值)、current
和compatible
。photoLibrary
参数提供对图片库的访问。该结构体带有类型方法shared()
。label
参数是一个闭包,提供视图所生成按钮的标签。
因获取资源会耗费时间,选择器并不直接返回图片和视频,而是一个稍后可供我们提取的资源指针。框架为此定义了PhotosPickerItem
结构体。该结构体包含如下访问媒体资源的属性和方法。
- itemIdentifier:该属性返回资源标识符的字符串。
- loadTransferable(type: Type):这一异步方法加载资源并将其赋值给由
type
参数指定数据类型的实例。这个参数的数据类型必须遵循Transferable
协议。
要访问ç
结构体,我们必须导入PhotosUI框架。此外,视图需要一个@State
属性用于存储所选资源。要启用多选,该属性必须存储PhotosPickerItem
结构体的数组,而对于单选,该属性只需要存储一个可选的PhotosPickerItem
值。如下所示。
示例18-1:创建一个图片选择器
import SwiftUI
import PhotosUI
struct ContentView: View {
@State private var selected: PhotosPickerItem?
@State private var picture: UIImage?
var body: some View {
NavigationStack {
VStack {
Image(uiImage: picture ?? UIImage(named: "nopicture")!)
.resizable()
.scaledToFit()
Spacer()
PhotosPicker(selection: $selected, matching: .images, photoLibrary: .shared()) {
Text("Select a photo")
.padding()
.buttonStyle(.borderedProminent)
}
.onChange(of: selected, initial: false) { old, item in
Task(priority: .background) {
if let data = try? await item?.loadTransferable(type: Data.self) {
picture = UIImage(data: data)
}
}
}
}
}
}
}
PhotosPicker
初始化方法中的大部分参数都是可选的。本例中,我们只需要告诉选择在哪里存储所选资源的指针,需要对用户显示哪种资源(图片),以及从哪里获取(共享库)。
PhotosPicker
结构体创建了一个打开选取资源视图的按钮。在视图中选中资源后,指针会存储到@State
属性中。这意味着我们可以通过onChange()
修饰符监控属性的变化。在选中新图片后,我们开启一个异步任务对所选资源调用loadTransferable()
方法。该方法加载图片,将其转换成一个Data
结构体返回。如果成功,我们使用这个数据初始化一个UIImage
对象,并将其赋值给picture
属性显示到屏幕上。
图18-1:图片库界面(中间图)
✍️跟我一起做:创建一个多平台项目。使用示例18-1中的代码更新ContentView
视图。下载nopicture.png
并将其添加到Assets中。点击Select a photo按钮。点击选中图片,图片会被赋给Image
视图并显示到屏幕上,如图18-1所示(右图)。
注意:本例中,我们使用了
Data
结构体通过loadTransferable()
方法传输值。我们大可以使用Image
视图,但它只能接收PNG图片。更多有关Transferable
协议的信息,请阅读第12章拖放手势一节。
默认PhotosPicker
视图创建一个在应用顶部打开视图的按钮,但我们也可以使用如下修饰符将视图嵌套到界面中。
- photosPickerStyle(PhotosPickerStyle):这一修饰符指定视图的展现样式。参数是一个具有
compact
、inline
和presentation
(默认值)属性的结构体。
presentation
样式以弹窗展示视图,上例正是如此。如果希望将视图嵌套到界面中,可以使用compact
和inline
样式。这两个样式很相似,但inline
样式提供了更多的选项并且易于访问内容,如下例所示。
示例18-2:在界面中嵌套图片选择器
PhotosPicker(selection: $selected, matching: .images, photoLibrary: .shared()) {
Text("Select a photo")
.buttonStyle(.borderedProminent)
.photosPickerStyle(.inline)
.frame(height: 300)
}
使用compact
和inline
样式展示的图片选择器大小由可用空间决定。也就是说图片选择器在界面大小发生改变时会对新的空间进行适配。但我们可以使用frame()
修饰符来设置固定大小,本例就是这么做的。结果如下所示。
图18-2:内联图片选择器
除frame()
修饰符之外,我们也可变使用框架所提供的如下修饰符来配置视图。
- photosPickerDisabledCapabilities(PHPickerCapabilities):这一修饰符指定对视图排除哪些能力。参数是用于表示能力的一个(或一组)结构体。该结构体包含的属性有
collectionNavigation
、selectionActions
、search
、sensitivityAnalysisIntervention
和stagingArea
。如果希望包含所有能力可以删除这一修饰符或是指定一个空集合。 - photosPickerAccessoryVisibility(Visibility, edges: Edge):该修饰符指定是否显示控件。第一个参数指定可见性。它是一个值为的
automatic
、visible
和hidden
的枚举。edges
参数是一组Edge
值,用于指定应删除图片选择器哪一边的控件。Edge
枚举的值有top
、bottom
、leading
和trailing
。
这些修饰符让我们可以选择希望包含或隐藏的控件。下例中我们删除了顶部的导航按钮。
示例18-3:隐藏控件
PhotosPicker(selection: $selected, matching: .images, photoLibrary: .shared()) {
Text("Select a photo")}
.buttonStyle(.borderedProminent)
.photosPickerStyle(.inline)
.frame(height: 300)
.photosPickerDisabledCapabilities([.collectionNavigation])
图18-3:自定义控件的图片选择器
在上例中,用户一次仅能选择一张图片。通过将@State
属性定义为PhotosPickerItem
结构体数组,可以让用户选择多张图片。虽然我们启用多图选择只要这么做,但必须考虑在用户取消选择时如何从列表中删除图片。我们可以清空数组重新载入每张图片,但有些图片的加载可能要花上一些时间。另一个选择是将图片存在单独的数组中,比较它们值,这样只删除取消选择的,而保留其它的。下例中我们采用的正是这种方法。为此,我们需要一个带结构体的模型来存储图片及其ID。
示例18-4:定义用于多选的模型
import SwiftUI
import Observation
import PhotosUI
struct ItemsData: Identifiable {
var id: String
var image: UIImage
}
@Observable class ApplicationData {
var listPictures: [ItemsData] = []
var selected: [PhotosPickerItem] = []
func removeDeselectedItems() {
listPictures = listPictures.filter{ value in
if selected.contains(where: { $0.itemIdentifier == value.id }) {
return true
} else {
return false
}
}
}
func addSelectedItems() {
for item in selected {
Task(priority: .background) {
if let data = try? await item.loadTransferable(type: Data.self) {
if let id = item.itemIdentifier, let image = UIImage(data: data) {
if !listPictures.contains(where: { $0.id == id}) {
let newPicture = ItemsData(id: id, image: image)
await MainActor.run {
listPictures.append(newPicture)
}
}
}
}
}
}
}
}
以下模型包含两个observable属性,一个用于存储ItemsData
结构体数组,将当前选中的图片发送给视图,另一个PhotosPickerItem
结构体数组用于为PhotosPicker
视图存储选中图片的指针。
模型中还有两个方法:removeDeselectedItems()
和addSelectedItems()
。两者都在用户修改选项时执行(即每当selected
属性值发生改变时)。removeDeselectedItems()
方法迭代listPictures
数组中的各项,检查哪些是用户选中的图片,所以用户取消选择的图片就不再位于列表中。而addSelectedItems()
方法将用户选中的图片添加到listPictures
数组中。现在视图可以使用listPictures
数组来显示在屏幕上选择的图片,在每次选项发生更改时调用这两个方法。
示例18-5:允许用户执行多图选择
struct ContentView: View {
@Environment(ApplicationData.self) private var appData
let guides = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: guides) {
ForEach(appData.listPictures) { item in
Image(uiImage: item.image)
.resizable()
.scaledToFit()
}
}
}
.padding()
Spacer()
PhotosPicker(selection: Bindable(appData).selected, maxSelectionCount: 4, selectionBehavior: .continuous, matching: .images, photoLibrary: .shared()) {
Text("Select Photos")
}
.photosPickerStyle(.inline)
.photosPickerDisabledCapabilities(.selectionActions)
}
.onChange(of: appData.selected, initial: false) { old, items in
appData.removeDeselectedItems()
appData.addSelectedItems()
}
}
}
本例中,我们配置了最多允许选择4张图片,但这么做没什么必要。如果不设置上限,用户可以选择希望添加的所有图片。注意因为图片选择器内嵌在了界面中,并不需要使用选择按钮,选择行为设置为了continous
,这样选取的图片会实时更新(用户无需按添加按钮)。
图18-4:图片多选
✍️跟我一起做:使用示例18-4中的代码创建一个ApplicationData.swift
文件。再用示例18-5中的代码更新ContentView
视图。不要忘记把ApplicationData
注入到应用的环境和预览中(参见第7章示例7-4)。选择多张图片,会看到选中的图片实时更新,如图18-4所示。
相机
移动设备最常见的用途之一是拍摄、存储照片,因此现在设备都有带摄像头。因为应用访问相机和管理图片都是很常规的操作。UIKit内置了控制器为用户提供所有拍摄照片和视频所需的工具。用于创建这一控制器的类是UIImagePickerController
。以下是用于配置该类的一些属性。
- sourceType:该属性设置或返回希望用于获取图片出处的类型。它是
UIImagePickerController
类一个SourceType
枚举。当前,可用值仅有camera
。 - mediaTypes:该属性设置或返回我们希望处理的媒体类型。它接收一个字符串数组,值表示希望使用的所有媒体。最常见的为用于图片的
public.image
和用于视频的public.movie
。(这些值可通过常量kUTTypeImage
和kUTTypeMovie
进行表示。) - cameraCaptureMode:该属性设置或返回相机使用的捕获模式。它是
UIImagePickerController
类中的CameraCaptureMode
枚举。可用值有photo
和video
。 - cameraFlashMode:该属性设置或返回相机的闪光灯模式。它是
UIImagePickerController
类中的CameraFlashMode
枚举。可用的值有on
、off
和auto
。 - allowsEditting:该属性设置或返回是否允许用户编辑图片的布尔值。
- videoQuality:该属性设置或返回录制视频品质的值。这是
UIImagePickerController
类中的QualityType
枚举。可用的值有typeHigh
、typeMedium
、typeLow
、type640x480
、typeIFrame960x540
以及typeIFrame1280x720
。
UIImagePickerController
类还提供了如下类型方法用于检测可用数据源以及其可管理的媒体类型。
- isSourceTypeAvailable(SourceType):该类型方法返回一个表明设备是否支持所指定数据源的布尔值。其中的参数是
UIImagePickerController
类中的SourceType
枚举。当前可用值仅有camera
。 - availableMediaTypes(for: SourceType):该类型方法返回参数指定数据源所支持媒体类型的字符串数组。其中的参数是
UIImagePickerController
类中的SourceType
枚举。当前可用值仅有camera
。 - isCameraDeviceAvailable(CameraDevice):该类型方法返回一个表明参数所指定摄像头是否可用的布尔值。其中的参数是
UIImagePickerController
类中的CameraDevice
枚举。可用值有rear
和front
。
UIImagePickerController
类创建一个用户可拍照或录制视频的视图。在创建完成图片或视频创建后,必须要释放该视图以及处理媒体资料。代码访问媒体资料以及知晓何时释放视图是借助于一个遵循UIImagePickerControllerDelegate
协议的代理。
该协议包含如下方法。
- imagePickerController(UIImagePickerController, didFinishPickingMediaWithInfo: Dictionary):该方法在用户完成拍照或录制视频后由代理调用。第二个参数包含一个有关媒体信息的字典。字典中的值通过
UIImagePickerController
类中的InfoKey
结构体的属性进行标识。可用的属性有cropRect
、editImage
、imageURL
、livePhoto
、mediaMetadata
、mediaType
、mediaURL
和originalImage
。 - imagePickerControllerDidCancel(UIImagePickerController):该方法在用户取消处理后由代理调用。
图片选择器放在弹窗中,但如果我们希望视图点满整个屏幕,可将其嵌套在NavigationStack
视图中,通过NavigationLink
进行打开。这正是我们在下面示例中采取的方法。界面中包含一个打开图片选择器的按钮以及一个显示用户所拍照片的Image
视图。
图18-5:使用相机的界面
注意:访问相机必须要获得用户的授权。这个过程是自动的,但需要在应用配置的Info面板中添加
Privacy - Camera Usage Description
选项,设置向用户展示的信息(第5章,图5-34)。
图片选择控制器是一个UIKit视图控制器,因此通过representable视图控制器在SwiftUI界面中显示。为处理相机所捕获的图片,我们需要添加一个coordinator
并实现代理方法。这个coordinator
必须遵循两个协议:UINavigationControllerDelegate
和UIImagePickerControllerDelegate
,如下例所示。
示例18-6:创建图片选择控制器拍摄照片
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
@Binding var path: NavigationPath
@Binding var picture: UIImage?
func makeUIViewController(context: Context) -> UIImagePickerController {
let mediaPicker = UIImagePickerController()
mediaPicker.delegate = context.coordinator
if UIImagePickerController.isSourceTypeAvailable(.camera) {
mediaPicker.sourceType = .camera
mediaPicker.mediaTypes = ["public.image"]
mediaPicker.allowsEditing = false
mediaPicker.cameraCaptureMode = .photo
} else {
print("The media is not available")
}
return mediaPicker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
func makeCoordinator() -> ImagePickerCoordinator {
ImagePickerCoordinator(path: $path, picture: $picture)
}
}
class ImagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
@Binding var path: NavigationPath
@Binding var picture: UIImage?
init(path: Binding<NavigationPath>, picture: Binding<UIImage?>) {
self._path = path
self._picture = picture
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let newpicture = info[.originalImage] as? UIImage {
picture = newpicture
}
path = NavigationPath()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
path = NavigationPath()
}
}
这一representable视图控制器创建一个UIImagePickerController
类的实例,并将ImagePickerCoordinator
赋为其代理 。接着检查相机是否就绪,成功后配置控制器或是失败在控制台打印消息。将camera
赋给sourceType
属性来告诉控制器通过相机获取图像,对mediaTypes
属性赋一个public.image
数组,指定获取的图片,allowEditting
设置为false
阻止用户编辑图片,cameraCaptureMode
赋了值photo
来允许用户仅捕获图像。
相机界面包含控制相机和捕获图像的按钮。用户拍完照后,会出现一组新的按钮,允许用户选择图像或再拍一张。如果用户决定使用当前图片,控制器会对代理调用imagePickerController(didFinishPickingMediaWithInfo:)
方法。该方法接收一个参数info
,可读取它来获取控制器返回的媒体资源并进行处理(保存到文件、数据库或在屏幕上显示)。本例中,我们读取originalImage
键的值来获取用户拍摄图片的UIImage
对象,将对象赋给@State
属性使其在视图中可用。注意我们还在coordinator中实现了imagePickerControllerDidCancel()
方法来在用户点击Cancel按钮时释放控制器。
视图中必须包含一下打开图片选择控制器的按钮以及一个展示用户拍摄照片的Image
视图。
示例18-7:定义拍照的界面
struct ContentView: View {
@State private var path = NavigationPath()
@State private var picture: UIImage?
var body: some View {
NavigationStack(path: $path) {
VStack {
HStack {
Spacer()
NavigationLink("Get Picture", value: "Open Picker")
}.navigationDestination(for: String.self, destination: { _ in
ImagePicker(path: $path, picture: $picture)
})
Image(uiImage: picture ?? UIImage(named: "nopicture")!)
.resizable()
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.clipped()
Spacer()
}.padding()
}.statusBarHidden()
}
}
这个视图创建了一个ImagePicker
结构体的实例,将其声明为NavigationLink
按钮的目的地。点击按钮时打开视图。如果用户拍好照并决定使用它,该图片通过代理方法赋值给picture
属性,屏幕上显示的Image
视图也进行了更新。
✍️跟我一起做:创建一个多平台项目。使用示例18-6中的代码创建ImagePicker.swift
文件。使用示例18-7中的代码更新ContentView.swift
文件。下载nopicture.png
文件放到资源目录中。在应用配置的Info面板中使用希望对用户显示的文字添加Privacy - Camera Usage Description
选项。在设备上运行应用,点击按钮。拍照并按下按钮使用这张照片。会在屏幕中看到这张照片。
其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记