Alan Hou的个人博客

大师学SwiftUI第18章 – 媒体资源

其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记

图片

如今,个人设备主要用于处理图片、视频和声音,苹果的设备也不例外。SwiftUI可以通过Image视图显示图片,但需要其它框架的支持来处理图片、在屏幕上展示视频或是播放声音。本章中我们将展示Apple所提供的这类工具。

图片选择器

SwiftUI内置了一个PhotosPicker结构体用于生成一个视图,允许用户从图片库中选择一张或多张照片。以下为该视图的初始化方法。

因获取资源会耗费时间,选择器并不直接返回图片和视频,而是一个稍后可供我们提取的资源指针。框架为此定义了PhotosPickerItem结构体。该结构体包含如下访问媒体资源的属性和方法。

要访问ç结构体,我们必须导入PhotosUI框架。此外,视图需要一个@State属性用于存储所选资源。要启用多选,该属性必须存储PhotosPickerItem结构体的数组,而对于单选,该属性只需要存储一个可选的PhotosPickerItem值。如下所示。

示例18-1:创建一个图片选择器

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视图创建一个在应用顶部打开视图的按钮,但我们也可以使用如下修饰符将视图嵌套到界面中。

presentation样式以弹窗展示视图,上例正是如此。如果希望将视图嵌套到界面中,可以使用compactinline样式。这两个样式很相似,但inline样式提供了更多的选项并且易于访问内容,如下例所示。

示例18-2:在界面中嵌套图片选择器

使用compactinline样式展示的图片选择器大小由可用空间决定。也就是说图片选择器在界面大小发生改变时会对新的空间进行适配。但我们可以使用frame()修饰符来设置固定大小,本例就是这么做的。结果如下所示。

图18-2:内联图片选择器

frame()修饰符之外,我们也可变使用框架所提供的如下修饰符来配置视图。

这些修饰符让我们可以选择希望包含或隐藏的控件。下例中我们删除了顶部的导航按钮。

示例18-3:隐藏控件

图18-3:自定义控件的图片选择器

在上例中,用户一次仅能选择一张图片。通过将@State属性定义为PhotosPickerItem结构体数组,可以让用户选择多张图片。虽然我们启用多图选择只要这么做,但必须考虑在用户取消选择时如何从列表中删除图片。我们可以清空数组重新载入每张图片,但有些图片的加载可能要花上一些时间。另一个选择是将图片存在单独的数组中,比较它们值,这样只删除取消选择的,而保留其它的。下例中我们采用的正是这种方法。为此,我们需要一个带结构体的模型来存储图片及其ID。

示例18-4:定义用于多选的模型

以下模型包含两个observable属性,一个用于存储ItemsData结构体数组,将当前选中的图片发送给视图,另一个PhotosPickerItem结构体数组用于为PhotosPicker视图存储选中图片的指针。

模型中还有两个方法:removeDeselectedItems()addSelectedItems()。两者都在用户修改选项时执行(即每当selected属性值发生改变时)。removeDeselectedItems()方法迭代listPictures数组中的各项,检查哪些是用户选中的图片,所以用户取消选择的图片就不再位于列表中。而addSelectedItems()方法将用户选中的图片添加到listPictures数组中。现在视图可以使用listPictures数组来显示在屏幕上选择的图片,在每次选项发生更改时调用这两个方法。

示例18-5:允许用户执行多图选择

本例中,我们配置了最多允许选择4张图片,但这么做没什么必要。如果不设置上限,用户可以选择希望添加的所有图片。注意因为图片选择器内嵌在了界面中,并不需要使用选择按钮,选择行为设置为了continous,这样选取的图片会实时更新(用户无需按添加按钮)。

图18-4:图片多选

✍️跟我一起做:使用示例18-4中的代码创建一个ApplicationData.swift文件。再用示例18-5中的代码更新ContentView视图。不要忘记把ApplicationData注入到应用的环境和预览中(参见第7章示例7-4)。选择多张图片,会看到选中的图片实时更新,如图18-4所示。

相机

移动设备最常见的用途之一是拍摄、存储照片,因此现在设备都有带摄像头。因为应用访问相机和管理图片都是很常规的操作。UIKit内置了控制器为用户提供所有拍摄照片和视频所需的工具。用于创建这一控制器的类是UIImagePickerController。以下是用于配置该类的一些属性。

UIImagePickerController类还提供了如下类型方法用于检测可用数据源以及其可管理的媒体类型。

UIImagePickerController类创建一个用户可拍照或录制视频的视图。在创建完成图片或视频创建后,必须要释放该视图以及处理媒体资料。代码访问媒体资料以及知晓何时释放视图是借助于一个遵循UIImagePickerControllerDelegate协议的代理。

该协议包含如下方法。

图片选择器放在弹窗中,但如果我们希望视图点满整个屏幕,可将其嵌套在NavigationStack视图中,通过NavigationLink进行打开。这正是我们在下面示例中采取的方法。界面中包含一个打开图片选择器的按钮以及一个显示用户所拍照片的Image视图。

图18-5:使用相机的界面

注意:访问相机必须要获得用户的授权。这个过程是自动的,但需要在应用配置的Info面板中添加Privacy - Camera Usage Description选项,设置向用户展示的信息(第5章图5-34)。

图片选择控制器是一个UIKit视图控制器,因此通过representable视图控制器在SwiftUI界面中显示。为处理相机所捕获的图片,我们需要添加一个coordinator并实现代理方法。这个coordinator必须遵循两个协议:UINavigationControllerDelegateUIImagePickerControllerDelegate,如下例所示。

示例18-6:创建图片选择控制器拍摄照片

这一representable视图控制器创建一个UIImagePickerController类的实例,并将ImagePickerCoordinator赋为其代理 。接着检查相机是否就绪,成功后配置控制器或是失败在控制台打印消息。将camera赋给sourceType属性来告诉控制器通过相机获取图像,对mediaTypes属性赋一个public.image数组,指定获取的图片,allowEditting设置为false阻止用户编辑图片,cameraCaptureMode赋了值photo来允许用户仅捕获图像。

相机界面包含控制相机和捕获图像的按钮。用户拍完照后,会出现一组新的按钮,允许用户选择图像或再拍一张。如果用户决定使用当前图片,控制器会对代理调用imagePickerController(didFinishPickingMediaWithInfo:)方法。该方法接收一个参数info,可读取它来获取控制器返回的媒体资源并进行处理(保存到文件、数据库或在屏幕上显示)。本例中,我们读取originalImage键的值来获取用户拍摄图片的UIImage对象,将对象赋给@State属性使其在视图中可用。注意我们还在coordinator中实现了imagePickerControllerDidCancel()方法来在用户点击Cancel按钮时释放控制器。

视图中必须包含一下打开图片选择控制器的按钮以及一个展示用户拍摄照片的Image视图。

示例18-7:定义拍照的界面

这个视图创建了一个ImagePicker结构体的实例,将其声明为NavigationLink按钮的目的地。点击按钮时打开视图。如果用户拍好照并决定使用它,该图片通过代理方法赋值给picture属性,屏幕上显示的Image视图也进行了更新。

✍️跟我一起做:创建一个多平台项目。使用示例18-6中的代码创建ImagePicker.swift文件。使用示例18-7中的代码更新ContentView.swift文件。下载nopicture.png文件放到资源目录中。在应用配置的Info面板中使用希望对用户显示的文字添加Privacy - Camera Usage Description选项。在设备上运行应用,点击按钮。拍照并按下按钮使用这张照片。会在屏幕中看到这张照片。

存储图片

在前面的示例中,我们在屏幕上展示了图片,但也可以将其存储到文件或数据库中。另外有时使用相机将照片存储到设备的相册薄里会很有用,这样可供其它应用访问。UIKit框架提供了如下两个保存图片和视频的函数。

注意:将照片或视频存储到设备中,必须要有用户的授权。没错,通过应用配置的Info面板可以实现。对于本例,必须添加Privacy - Camera Usage Description选项,设置请求授权时向用户展示的信息。

这些是在Objective-C中定义的老方法,因此用到了一些在SwiftUI应用中不常见的参数。但如果只是要保存图片,我们可以只指定第一个参数,将其它的定义为nil。例如,可以在前面的应用界面的上方添加一个按钮,打开带两个按钮的警告视图,一个按钮用于取消操作,另一个用于将当前图片保存到相册。点击按钮保存图片时,我们可以调用UIImageWriteToSavedPhotosAlbum,传入picture属性的指针,图片就会被保存。

示例18-8:在保存图片时显示警告视图

流程和之前相同。图片选择控制器让用户可以拍照,然后调用代理方法来处理。图片赋值给picture属性来在屏幕上进行显示,但此时我们多了一个按钮可以将图片保存到照片库中。

✍️跟我一起做:使用示例18-8中的代码更新ContentView.swift文件。在应用的info面板中添加Privacy – Photo Library Additions Usage Description选项来获取照片库的访问权限。(别忘了还需要和之前一样配置Privacy – Camera Usage Description来获取相机的权限。)在设备上运行应用、拍照。应该会在屏幕上看到照版。点击Share Picture按钮,会弹出一个警告框要求获取权限。点击YES。这时相片会保存到照片库中。

分享链接

另一种与其它应用分享信息的方式是分享弹窗。这个弹窗由系统提供,通过图标可打开希望共享内容的应用,同时带有拷贝和打印信息的选项。SwiftUI提供了如下打开弹窗的视图。

如果希望共享图片,必须提供预览。为此SwiftUI内置了SharePreview结构体。

分享链接经常用于分享文本,但也可以分享其它内容,只要内容符合Transferable协议即可。例如,我们可以分享拍摄的照片。

示例18-9:对其它应用分享图像

ShareLink视图使用左侧带有SF图标的预定义标签创建按钮。本例中,我们将其放在左上角,但仅在有图片可共享时才显示(如果用户已使用相机拍摄照片)。按下按钮后,系统会打开一个小弹窗,其中包含可分享信息的应用图标,在弹窗中向上滚动时,会看到拷贝和打印数据等其它操作选项。例如,假设我们安装了Facebook,就可以像下图这样通过图片发帖。

图18-6:分享弹窗

✍️跟我一起做:使用示例18-9中的代码更新ContentView视图。在设备上运行应用。点击Get Picture按钮拍照。然后点击Share Picture按钮。会在屏幕底部弹出分享弹窗。选择分享图片的应用。

自定义相机

UIImagePickerController控制器通过AV Foundation框架中定义的类构建。该框架提供了处理媒体资源和控制输入设备所需的代码。因此可以使用框架中的类直接构建自己的控制器以及自定义处理流程和界面。

创建访问相机从输入设备获取信息的自定义控制器,要求多系统的协同,我们需要配置相机和麦克风的输入、处理通过这些输入接收到数据、对用户提供预览并生成图片、实时图片、视频或音频形式的输出。图18-7所有相关的元素。

图18-7 捕获媒体资源的系统

构建之初我们需要确定输入设备。AV Foundation框架为此定义了AVCaptureDevice类。该类的实例可表示任意输入设备,包括相机和麦克风。下面是该类中包含的访问和管理设备的一方法。

AVCaptureDevice类的实例表示一个捕获的设备,如相机或麦克风。该类包含用于配置和管理设备的属性和方法。以下为其中最常用的以及我们在例子中要用到的。

要将捕获的设备定义为输入设备,我们必须创建控制商品和连接对象。框架为此定义了AVCaptureDeviceInput类。类中包含如下创建设备输入对象的初始化方法。

除输入外,我们还需要输出来捕获和处理从其它设备接收到设备。框架定义了基类AVCaptureOutput的子类 来描述输出。有多个可用的子类 ,比如处理视频帧的AVCaptureVideoDataOutput和获取音频数据的AVCaptureAudioDataOutput,但最有用的还是AVCapturePhotoOutput类,用于捕获单个视频帧(拍照)。这个类包含很多配置输出的属性和方法。下面是设置最大图片尺寸的属性和捕获照片的方法。

AVCapturePhotoOutput类与符合AVCapturePhotoCaptureDelegate协议的委托一起返回一个静止图片,其中定义有如下方法。

为控制输入到输出的数据流,框架定义了AVCaptureSession类。通过该类的实例,我们可以通过调用如下方法控制输入、输出并决定处理何时开始和结束。

框架还定义了AVCaptureVideoPreviewLayer类向用户展示预览。该类创建一个图层展示输入设备捕获的视频。类中包含如下创建和管理预览图层的初始化方法和属性。

输入、输出和预览图层通过AVCaptureConnection类的对象与捕捉会话进行连接。该类管理连接信息,有端口、数据和朝向。以下是用于设置预览层朝向的属性和方法。

旋转角度由旋转coordinator决定。框架在AVCaptureDevice类中定义了RotationCoordinator来进行创建。该类中包含如下初始化方法。

RotationCoordinator类中包含如下两个属性,可读取获取当前旋转角度。

本例所创建的界面与前面的相近。需要一个按钮打开视图允许用户用相机拍照,以及一个在屏幕上显示照片的Image视图。

图18-8:自定义相机界面

启动相机以及获取用户所拍相片的处理与界面相独立,但如果希望用户看到来自相机的图片,我们需要创建一个预览层。图层是视图在屏幕上展示图像的方式。视图定义区域并提供功能,但图像由CALayer类创建的图层进行展示。UIView类创建的每个包含可用于展示视频的图层,但图层必须转换为AVCaptureVideoPreviewLayer。为此我们需要创建一个UIView的子类 ,重载类型属性layerClass,将该视频的图层转换为预览图层,然后创建一个UIViewRepresentable结构体来展示SwiftUI界面中的视图。

示例18-10:定义一个UIView的子类展示相机的预览视频

layerClass属性是系统读取决定图层数据类型的类型属性。本例中,我们重载了该属性返回AVCaptureVideoPreviewLayer类的指针,这样系统知道我们使用这一视图层来显示视频。representable视图的其它代码和之前一样。本例我们会在model中管理所有相机的逻辑。以下是配置系统所需的基本元素。

示例18-11:定义管理相机的属性

以上代码是这模型的第一部分,我们还要添加一些方法来启用和控制相机,但它提供了需要存储系统各个元素指针的属性。因这些属性在多个方法中用到,我们将其声明到了单独的类ViewData。在初始化模型时,我们创建了此类的实例和表征视图(CustomPreview),将它们存在于非观测属性中供其它代码访问。

下一步是定义方法获取访问相机的权限。如果使用UIImagePickerController控制器这会自动实现,但在自定义控制器中我们需要使用AVCaptureDevice类所提供的方法自己实现。以下是在模型中添加的对应方法。

示例18-12:请求使用相机的权限

requestAccess()方法是异步的,它等待用户响应,返回Bool类型的值报告结果。如果用户授权访问,我们执行prepareCamera()方法。这里我们开始构建图18-7中介绍的对象网络。该方法必须获取当前视频拾取设备的指针,创建我们抓取静止图像(拍照)的输入和输出。

示例18-13:初始化相机

该方法一开始新建会话并请求对相机的访问。如果default()方法返回值,如就将true赋值给isSubjectAreaChangeMonitoringEnabled属性,开启对设备朝向变化的监控。

有了会话和设备访问权限后,我们就可以定义所需的输入和输出。并没有特别的顺序要求,但因为AVCapturePhotoOutput()初始化方法会抛错误,我们先使用了它。这个初始化方法创建一个管理捕获设备输入的对象。如果成功,使用addInput()将其添加到捕获会话,再创建输出。

本例中,我们希望使用会话捕获静止图像。因此,我们使用AVCapturePhotoOutput类创建输出,将其添加到会话,然后配置返回允许的最大尺寸的图像。注意最大尺寸由maxPhotoDimensions属性决定,但不能对其赋自定义值。我们需要获取相机可生成的可用尺寸列表并使用最大的那个。实现这一任务,我们读取activeFormat属性获取相机当前使用格式的Format对象,并读取其supportedMaxPhotoDimensions属性。这个属性返回一个CMVideoDimensions结构体数组,包含设备所支持的尺寸,我们获取最后一个赋值给输出,得到尽可能大尺寸的图像。

在读取输入、输出获取捕获会话后,prepareCamera()方法还执行showCamera()方法定义预览层并在屏幕上显示来自相机的视频。

示例18-14:在屏幕上显示来自相机的视频

前面已经提到,包含UIView类创建视图的图层由CALayer类型对象定义。这是显示图像和执行动画的基类。但要显示来自相机的视频,我们需要将其转换为AVCaptureVideoPreviewLayer对象。在将图层转化为预览层后,我们可以创建旋转协调器来设置视频的朝向。协调器检测设备和预览层,并将当前旋转角度存储到videoRotationAngleForHorizonLevelPreview属性中,因此我们将该属性的值赋给AVCaptureConnection对象的videoRotationAngle属性来设置当前朝向。为保持该值实时更新,我们对属性videoRotationAngleForHorizonLevelPreview添加了观察者,每当该属性值发生改变时设置视频的朝向(参见第14章的键/值观察)。准备好预览层和旋转协调器后,捕获会话通过startRunning()方法初始化。(系统要求该方法在后台线程中执行。)

此时,视频在屏幕上播放,系统可以捕捉图像了。捕捉图像的过程由AVCapturePhotoOutput对象提供的capturePhoto()方法初始化,输出的图片类型由AVCapturePhotoSettings对象决定。该类包含多个初始化方法。以下是最常用的那个。

以下是该类中用于配置图像和预览的一些属性。

配置图像,我们要通过AVCapturePhotoSettings对象定义设置,调用AVCapturePhotoOutput对象的capturePhoto()方法,并定义接收图像的委托方法。以下是需要向模型添加的拍照方法。

示例18-15:拍照

在用户点击按钮拍照时,执行takePicture()方法并调用capturePhoto()方法请求捕捉图像的输出对象。捕捉图像后,此对象将结果发送给委托对象(见示例18-11),因此我们可以在模型内实现委托方法。参见下面我们对该方法的实现。

示例18-16:处理图像

photoOutput(AVCapturePhotoOutput, didFinishProcessingPhoto:)方法接收相机所生成的图片。方法接收的值是AVCapturePhoto类型的对象,这是一个带有图片信息的容器。类中包含两个方便获取表示图像的数据的方法。

在本例中,我们实现了cgImageRepresentation()方法,因为UIImage类定义了一个便捷的初始化方法,可通过包含缩放比例和朝向的CGImage创建图像。通过旋转协调器的videoRotationAngleForHorizonLevelCapture属性获取朝向。该属性返回带有旋转角度的CGFloat值,我们可将其转换为Orientation值来设置图像的朝向(参见第10章中的UIImage)。要设置缩放比例,我们需要访问屏幕。屏幕由UIScreen类的对象进行管理,自动按设备创建并赋值给Scene属性。因此,要访问屏幕和缩放比例,我们需要读取UIWindowScene对象,它通过UIApplication对象的connectedScenes属性控制当前场景。我们在第14章中介绍过这个对象。它由系统创建用于控制应用。该对象由shared类提供的类型属性返回。要访问应用所打开的场景,我们读取connectedScenes属性。本例我们为移动设备开发应用,因此只需要访问第一个场景。UIWindowScene对象包含screen属性,返回表示屏幕的UIScreen对象指针,而UIScreen对象包含有返回当前比例的scale属性,以及屏幕大小的bounds属性。通过这些值,我们创建了UIImage对象,并将其赋值给picture属性更新视图及显示图像,如下所示。

示例18-17:显示图像

示例18-17中并没有太多新内容,只是不再打开包含标准界面的UIImagePickerController,我们打开了一个需添加供用户拍照的按钮和自定义控件的视图。以下是对该视图的实现。

示例18-18:拍照

图18-8(右图)所示,该视频包含有UIView,显示 来自相机的视频,以及底部的另一个视图,包含两个按钮,一个用于取消处理和释放视频,另一个用于拍照。该视图出现在屏幕上时,我们调用getAuthorization()方法来启动处理。如果用户点击Take Picture按钮,我们调用takePicture()方法捕捉图像。处理好图像后,委托方法释放该视图并在屏幕上显示图像。注意我们应用了onDisappear()修饰符来删除观察者。这样可以确保在不需要时不再有活跃的观察者。

✍️跟我一起做:创建一个多平台项目。下载nopicture.png图片,将其添加到资源目录。使用示例18-10的代码创建CustomPreview.swift,用示例18-11的代码创建ApplicationData.swift。在模型中添加示例18-1218-1318-1418-1518-16中的方法。用示例18-17中的代码更新ContentView视图。创建一个SwiftUI文件CustomCameraView.swift,代码见示例18-18。别忘了在应用设置的Info面板中添加rivacy - Camera Usage Description选项,并将ApplicationData对象注入到应用和预览中(参见第7章示例7-4)。在设备中运行应用并拍照测试。

视频

录制和播放视频对用户来说和拍照、显示图片一样重要。和图片一样,Apple框架中内置了播放视频和创建自定义播放器的工具。

视频播放器

SwiftUI定义了VideoPlayer视图用于播放视频。该视图提供了所有用于播放、停止、前进和后退的控件。视图包含如下初始化方法。

VideoPlayer视图展示用户控制视频的界面,但视频由AVPlayer类的对象播放。该类包含如下初始化方法。

AVPlayer类还提供通过程序控制视频的属性和方法。

VideoPlayer视图需要有AVPlayer对象来播放视频,该对象对过URL加载视频。如果希望播放线上的视频,只需要URL,但如果视频由应用提供,则需要通过包来获取(参见第10章中的Bundle)。在以下的模型中,我们在项目中添加了一个视频videotrees.mp4,通过Bundle对象获取指向该文件的URL,并用该值创建一个AVPlayer对象。

示例18-19:准备待播放的视频

VideoPlayer视图和AVPlayer类来自AVKit框架。导入该框架后,我们获取到videotrees.mp4视频的URL,创建AVPlayer对象并将其存储到可观测属性中,以供视图使用。在视图中,我们需要检测该属性并在视频准备就绪后显示VideoPlayer视图。

示例18-20:播放视频

图18-9:标准视频播放器

✍️跟我一起做:创建一个多平台项目。下载videotrees.mp4并添加至项目中(别忘了在弹窗中选择target)。使用示例18-19中的代码创建一个Swift模型文件ApplicationData.swift。使用示例18-20中的代码更新ContentView视图。还要将ApplicationData对象注入应用和预览的环境中(参见第7章示例7-4)。运行应用。点击播放按钮播放视频。

上例中,视频需要由用户点击播放按钮才开始播放。但我们可以实现AVPlayer属性和方法来通过程序控制视频。例如,以下示例在视图加载完后就开始播放视频。

示例18-21:自动播放视频

VideoPlayer视图初始化方法还可以包含一个参数,接收闭包来在视频上添加浮层。下例中,实现的初始化方法在视频的顶部添加标题。

示例18-22:在视频上展示视图

闭包返回中的视频位于视频之上和控件之下,因此无法接收用户的输入,但可以使用它来提供额外的信息,就像本例中这样。结果如下所示。

图18-10:浮层视图

自定义视频播放器

除了让VideoPlayer视图正常工作的代码外,AVFoundation框架还提供了创建播放媒体独立组件的功能。有一个负责资源(视频或音频)的类,一个负责将媒体资源发送给播放器的类,一个播放媒体的类以及一个负责在屏幕上显示媒体的类。图18-11描述了这一结构。

图18-11:播放媒体的系统

待播放的媒体以资源形式提供。资源以一个或多个媒体轨道组成,包括视频、音频和字幕等。AVFoundation框架定义了一个AVAsset类来加载资源。该类包含如下初始化方法。

资源包含有静态信息,在播放后无法管理自身的状态。框架定义了AVPlayerItem类来控制资源。通过此类我们可以引用资源并管理其时间轴。该类中包含多个初始化方法。以下是最常用的一个。

AVPlayerItem类还包含一些控制资源状态的属性和方法。以下是最常用的那些。

AVPlayerItem对象管理播放所需的信息,但不会播放媒体,这是由AVPlayer类的实例来处理的。它是稍早我们在VideoPlayer视图中用于加载视频相同的类。该类包含如下通过AVPlayerItem对象创建播放器的初始化方法。

系统所需的最后一个对象负责展示媒体资源。它是CALayer的子类AVPlayerLayer,提供了在屏幕上绘制视频帧所需要的代码。该类包含如下创建和配置播放层的初始化方法和属性。

这些类一起定义了用于播放媒体的系统,但还要有方法来控制时间。因浮点数的精度不适合于播放媒体资源,框架还通过旧框架的Core Media实现了CMTime结构体。这一结构体包含了很多以分数表示时间的值。最重要的两个是valuetimescale,分别表示分子和分母。例如,想要创建表示0.5秒的CMTime结构体时,可以指定分子为1、分母为2(1除以2得0.5)。该类包含一些创建这些值的初始化方法和类型属性。以下是最常使用的。

CMTime结构体还包含一些设置和获取值的属性。最常用的如下。

要自定义视频播放器,我们必须加载资源(AVURLAsset),创建一个管理资源的子项(AVPlayerItem),将子项添加至播放器(AVPlayer),将播放器关联至屏幕上媒体的显示层(AVPlayerLayer)。

就像前面用于显示来自相机的视频的预览层,我们需要将UIView对象提供的显示层转化为预览层(本例中CALayer需要转换为AVPlayerLayer对象)。以下是本例需要实现的表现视图。

示例18-23:构建自定义播放器

有了表现视图,下一步就是构建视频播放器,然后在就绪后调用player()方法播放视频。

示例18-24:构建自定义视频播放器

视频并非立马可见,需要进行加载和做好播放准备,因而不能马上播放,需等待其就绪。媒体的状态由AVPlayerItemstatus属性进行上报。因此需要监测该属性的值,公在其值等于readyToPlay时开始播放。这就要使用到观察过生日和。因此,在定义三个属性后,我们需要存储播放项、播放器和播放层,我们定义了一个存储观察者的属性,调用AVPlayerItem对象的observer()方法来跟踪status属性。在当前状态为readyToPlay时播放视频。

为配置视频播放器,我们从bundle中加载视频、创建播放器结构体、将UIView层转换为AVPlayerLayer,将其赋值给player。因所有内容都在模型中进行了准备,界面只需要在展示视图中进行显示。视频填满屏幕、适配屏幕的朝向并在加载视图后进行播放。

示例18-25:显示视频

✍️跟我一起做:创建一个多平台项目。下载videotrees.mp4,添加至项目中(记住要勾选Add to Target选项)。使用示例18-23中的代码创建CustomPlayerView.swift文件,使用示例18-24中的创建模型文件ApplicationData.swift。再用示例18-25中的代码更新ContentView视图。运行应用,视频应该会在应用启动后立即播放。

上例中进行了视频的播放,但没为用户提供任何控件工具。AVPlayer类包含有播放、暂停和检查媒体状态的方法,但需要我们来创建界面。下例中我们会创建一个带有按钮和进度条的界面,这样用户可以播放、暂停并查看视频的进度。

图18-12:自定义视频播放器的控件

如何控制流程以及对界面进行响应取决于应用的要求。例如,我们决定定义两个状态,一个表示视频是否在播放,另一个表示进度条的位置。以下是对模型所做的修改,让用户可以播放、暂停视频以及拖动进度条。

示例18-26:准备视频播放器

本例中,我们添加了一个playVideo()方法,在用户点击Play按钮时执行。该方法检测是否可以播放媒体,然后根据playing属性的值执行操作。如果视频在播放就暂停,如果在暂停就播放。playing属性的值会进行更新来反映新的状态。

要计算进度条的长度,必须要实现一个观察者。但不是像之前所实现的KVO观察者。常规的观察者不够快,所以AVFoundation框架自带了一个addPeriodicTimeObserver()方法创建提供更精准响应的观察者。该方法需要一个CMTime值来指定执行任务的频率、一个主队列指针以及一个带每次触发观察者执行代码的闭包。本例中,我们创建一个表示0.5秒时长的CMTime值,然后使用它调用addPeriodicTimeObserver()方法来注册观察者。之后,传递给观察者的闭包在播放期间每0.5秒执行一次。在闭包中,我们获取到了当前时间以及视频时长秒数,通过将秒数转换成0.0到1.0之间的值来计算进度,稍后可转化成点数在屏幕上显示进度条。

注意:addPeriodicTimeObserver()方法无法用于Swift的并发。而是需要将线程定义在DispatchQueue对象中。这是由Dispatch框架定义的老类,用于创建异步任务。该类包含一个类型属性main,定义一个主队列任务(Main Actor),这正是确保赋给这一方法的闭包在主线程中运行的方式。

播放器已就绪,是时修改定义界面了。在这个场景中,我们需要在ZStack中展示表现视图,这样可以在上层显示工具栏(参见图18-12)。

示例18-27:播放及暂停视频

工具栏包含一个按钮和一个表示进度条的Rectangle视图。按钮的标签取决于playing属性的值。在视频播放时显示Pause,暂停时显示Play。为计算表示进度条的Rectangle视图的长度,我们嵌入了GeometryReader视图,然后用其宽度乘上progress属性。因该属性包含一个0.0到1.0之间的值,这一运算返回一个设置进度条宽度的值,在屏幕上显示出进度。

✍️跟我一起做:使用示例18-26中的代码更新模型,用示例18-27中的代码更新ContentView视图。运行程序,就会看到图18-12中所示的视频播放器。

通过addPeriodicTimeObserver()方法添加的观察者不是获取播放器的信息的唯一方式。AVPlayerItem类还定义了一些通知用于报告媒体播放期间发生的事件。例如,我们可以监听AVPlayerItemDidPlayToEndTime通知来了解视频何时停止播放。为此,我们需要在模型中定义一个方法监听并响应该通知,并添加一个任务在展示视图创建时调用该方法。以下是我们需要在ApplicationData类的初始化方法添加的任务。

示例18-28:执行异步方法监测视频结束

rewindVideo()方法中,我们必须监听AVPlayerItemDidPlayToEndTime通知,并准备再次播放视频。为此,AVPlayerItem类提供了seek()方法。该方法将播放进度移到参数所指定的时间,并在处理完成后执行一个闭包。本例中我们将使用值为0的CMTime将播放器移到视频开头,然后重置playingprogress属性允许用户重新播放视频。

示例18-29:重新播放视频

✍️跟我一起做:将示例18-28中的任务添加到ApplicationData初始化方法的最后。将示例18-29中的方法添加到ApplicationData类的最后。运行程序。点击播放,等待视频结束。播放器应该会重置,可以再次播放视频。

如果希望按顺序播放多个视频,我们可以使用AVPlayerItemDidPlayToEndTime通知将新资源赋值给AVPlayer对象,但框架提供了AVPlayer类的子类AVQueuePlayer,专门上用于管理视频列表。该类通过AVPlayerItem对象数组创建一个播放列表。以下为初始化方法和其中的一些方法。

AVQueuePlayer对象替换用于展示媒体资源的AVPlayer对象。播放视频序列我闪只需要每个视频创建一个AVPlayerItem对象,以及将我们一直使用的AVPlayer对象替换为AVQueuePlayer对象,如下例所示。

示例18-30:播放视频列表

本例中,我们使用的是示例18-25中的ContentView视图。代码中加载了两个视频,videotrees.mp4videobeaches.mp4,然后创建两个AVURLAsset对象以及两个用于展示它们的AVPlayerItem对象。接着定义AVQueuePlayer对象来按顺序播放这两个视频。注意因为在本例中我们使用的界面没带播放按钮,我们对第一个视频添加了一个观察者,在准备就绪后调用play()方法。

✍️跟我一起做:使用示例18-30中的代码更新ApplicationData.swift文件。结合示例18-25中的ContentView视图使用。下载videotrees.mp4videobeaches.mp4视频添加至项目中(记得勾选Add to Target)。运行应用,视频应该会逐一播放。

颜色拾取器

SwiftUI自带了ColorPicker视图来允许用户选取颜色。该视图创建一个按钮,打开预定义界面,自带有选取和配置颜色的工具。以下是该视图的初始化方法。

颜色拾取器的实现非常简单。我们用Color视图定义一个@State属性,然后使用它初始化ColorPicker视图,这样每次用户选择颜色时,就会存储到该属性中,我们可以使用它来修改其它视图。在下例中,我们使用该属性的值来修改界面的背景色。

示例18-31:显示颜色拾取器

ColorPicker视图展示一个按键,打开用户选择颜色的界面。用户选取颜色后,颜色会自动赋给@State属性。这意味着用户可以按意愿多次修改选择,但只有最后一次所选的颜色保存到该属性中。

图18-13:颜色拾取器

✍️跟我一起做:创建一个多平台项目。使用示例18-31中的代码更新ContentView视图。运行应用、点击颜色拾取器按钮。选择颜色,会看到界面颜色的变化,如图18-13所示。

代码请见:GitHub仓库

退出移动版