大师学SwiftUI第18章Part2 - 存储图片和自定义相机

存储图片

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

  • UIImageWriteToSavedPhotosAlbum(UIImage, Any?, Selector?, UnsafeMutableRawPointer?):该函数将第一个参数所指定的图像添加到相册。第二个参数是在处理结束后包含所要执行方法的对象的指针,第三个参数是表示该方法的选择器,最后一个参数是传递给该方法数据的指针。
  • UISaveVideoAtPathToSavedPhotosAlbum(String, Any?, Selector?, UnsafeMutableRawPointer?):该函数将第一个参数所指定路径的视频添加到相册。第二个参数是在处理结束后包含所要执行方法的对象的指针,第三个参数是表示该方法的选择器,最后一个参数是传递给该方法数据的指针。

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

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

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

less 复制代码
struct ContentView: View {
    @State private var path = NavigationPath()
    @State private var picture: UIImage?
    @State private var showAlert: Bool = false
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                HStack {
                    Button("Share Picture") {
                        showAlert = true
                    }.disabled(picture == nil ? true : false)
                    Spacer()
                    NavigationLink("Get Picture", value: "Open Picker")
                }.navigationDestination(for: String.self, destination: { _ in
                    ImagePicker(path: $path, picture: $picture)
                })
                .alert("Save Picture", isPresented: $showAlert, actions: {
                    Button("Cancel", role: .cancel, action: {
                        showAlert = false
                    })
                    Button("YES", role: .none, action: {
                        if let picture {
                            UIImageWriteToSavedPhotosAlbum(picture, nil, nil, nil)
                        }
                    })
                }, message: { Text("Do you want to store the picture in the Photo Library?") })
                Image(uiImage: picture ?? UIImage(named: "nopicture")!)
                    .resizable()
                    .scaledToFit()
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                    .clipped()
                Spacer()
            }.padding()
        }.statusBarHidden()
    }
}

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

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

分享链接

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

  • ShareLink (String, item : Item, subject : Text?, message : Text?, preview : SharePreview):这一初始化方法创建一个按钮,可打开弹窗选择希望共享数据的应用。第一个参数是按钮的标签。item参数是希望共享的值(必须符合Transferable协议)。subject参数是内容的标题。message参数是内容的描述。preview参数是提供内容展示的结构体。

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

  • SharePreview (String, image : Image):这一初始化方法创建一个分享内容的展示。第一个参数是内容的描述,image参数是在视觉上表现内容的Image视图。

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

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

less 复制代码
struct ContentView: View {
    @State private var path = NavigationPath()
    @State private var picture: UIImage?
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                HStack {
                    if let picture = picture {
                        let photo = Image(uiImage: picture)
                        ShareLink("Share Picture", item: photo, preview: SharePreview("Photo", image: photo))
                    }
                    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()
                    .scaledToFill()
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                    .clipped()
                Spacer()
            }.padding()
        }.statusBarHidden()
    }
}

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

图18-6:分享弹窗

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

自定义相机

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

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

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

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

  • default (for : AVMediaType):这一类型方法返回一个表示参数指定的默认捕获媒体资源设备的AVCaptureDevice对象。for参数是一个AVMediaType类型的结构体,包含定义媒体类型的属性。用于操作相机和麦克风的属性为videoaudio
  • requestAccess (for : AVMediaType):向用户请求访问设备权限的异步类型方法。for参数是一个AVMediaType类型的结构体,包含定义媒体类型的属性。用于操作相机和麦克风的属性为videoaudio
  • authorizationStatus (for : AVMediaType):该类型方法返回决定使用设备权限状态的值。for参数是一个AVMediaType类型的结构体,包含定义媒体类型的属性。用于操作相机和麦克风的属性为videoaudio。该方法返回AVAuthorizationStatus类型的枚举,值有notDeterminedrestricteddeniedauthorized

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

  • isSubjectAreaChangeMonitoringEnabled:该属性设置或返回一个布尔值,决定设备是否监测修改的区域,如光照和朝向。
  • formats :该属性返回一个Format对象的数组,表示设备支持的格式。
  • activeFormat:该属性返回一个Format对象,表示设备当前使用的格式。
  • lockForConfiguration():该方法请求对配置设备的独占访问。
  • unlockForConfiguration():该方法释放所配置的设备。

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

  • AVCaptureDeviceInput (device : AVCaptureDevice):该初始化方法创建由device参数指定的设备输入。

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

  • maxPhotoDimensions :此属性设置或返回待捕获图片的大小。这一个CMVideoDimensions类型的结构体,包含属性widthheight
  • capturePhoto (with : AVCapturePhotoSettings, delegate : AVCapturePhotoCaptureDelegate):该方法通过with参数指定的设置初始化照片抓取。delegate参数是一个对象指针,对象实现了AVCapturePhotoCaptureDelegate协议中接收输出生成数据的方法。

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

  • photoOutput (AVCapturePhotoOutput, didFinishProcessingPhoto : AVCapturePhoto, error : Error?):该方法在捕获图片后对委托进行调用。didFinishProcessingPhoto参数是一个容器,包含有关图片的信息,error参数用于报告错误。

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

  • addInput(AVCaptureInput):此方法向捕获会话添加输入。参数表示希望添加的输入设备。
  • addOutput(AVCaptureOutput):此方法向捕获会话添加输出。参数表示我们希望从捕获会话生成的输出。
  • startRunning():此方法开启捕获会话。
  • stopRunning():此方法停止捕获会话。

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

  • AVCaptureVideoPreviewLayer (session : AVCaptureSession):此初始化方法创建一个连接由session参数定义的捕获会话的AVCaptureVideoPreviewLayer对象.
  • connection :此属性返回AVCaptureConnection类型的对象,定义捕获会话与预览图层间的连接。

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

  • videoRotationAngle :该属性返回表示连接应用于预览的旋转角度的CGFloat值(角度为0.0, 90.0, 180.0, 270.0)。
  • isVideoRotationAngleSupported(CGFloat):该方法返回一个布尔值,表示是否支持由参数所指定的旋转角度。

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

  • AVCaptureDevice .RotationCoordinator (device : AVCaptureDevice, previewLayer: CALayer?):该谢谢学姐为设备创建一个旋转coordinator以及由参数指定的预览层。

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

  • videoRotationAngleForHorizonLevelPreview:该属性返回需应用于预览层的旋转角度,与设备朝向进行匹配。
  • videoRotationAngleForHorizonLevelCapture:该属性返回需应用于相机抓取图像的旋转角度,与设备朝向进行匹配。

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

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

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

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

swift 复制代码
import SwiftUI
import AVFoundation

class CustomPreviewView: UIView {
    override class var layerClass: AnyClass {
        return AVCaptureVideoPreviewLayer.self
    }
}
struct CustomPreview: UIViewRepresentable {
    let view = CustomPreviewView()
    
    func makeUIView(context: Context) -> UIView {
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {}
}

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

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

swift 复制代码
import SwiftUI
import Observation
import AVFoundation

class ViewData {
    var captureDevice: AVCaptureDevice?
    var captureSession: AVCaptureSession?
    var stillImage: AVCapturePhotoOutput?
    var rotationCoordinator: AVCaptureDevice.RotationCoordinator?
    var previewObservation: NSKeyValueObservation?
}
@Observable class ApplicationData: NSObject, AVCapturePhotoCaptureDelegate {
    var path = NavigationPath()
    var picture: UIImage?
    @ObservationIgnored var cameraView: CustomPreview!
    @ObservationIgnored var viewData: ViewData!
    
    override init() {
        cameraView = ç()
        viewData = ViewData()
    }
}

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

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

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

swift 复制代码
    func getAuthorization() async {
        let granted = await AVCaptureDevice.requestAccess(for: .video)
        await MainActor.run {
            if granted {
                self.prepareCamera()
            } else {
                print("Not Authorized")
            }
        }
    }

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

示例18-13:初始化相机

swift 复制代码
    func prepareCamera() {
        viewData.captureSession = AVCaptureSession()
        viewData.captureDevice = AVCaptureDevice.default(for: AVMediaType.video)
        
        if let _ = try? viewData.captureDevice?.lockForConfiguration() {
            viewData.captureDevice?.isSubjectAreaChangeMonitoringEnabled = true
            viewData.captureDevice?.unlockForConfiguration()
        }
        if let device = viewData.captureDevice {
            if let input = try? AVCaptureDeviceInput(device: device) {
                viewData.captureSession?.addInput(input)
                
                viewData.stillImage = AVCapturePhotoOutput()
                if viewData.stillImage != nil {
                    viewData.captureSession?.addOutput(viewData.stillImage!)
                    if let max = viewData.captureDevice?.activeFormat.supportedMaxPhotoDimensions.last {
                        viewData.stillImage?.maxPhotoDimensions = max
                    }
                }
                showCamera()
            } else {
                print("Not Authorized")
            }
        } else {
            print("Not Authorized")
        }
    }

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

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

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

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

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

ini 复制代码
    func showCamera() {
        let previewLayer = cameraView.view.layer as? AVCaptureVideoPreviewLayer
        previewLayer?.session = viewData.captureSession
        
        if let device = viewData.captureDevice, let preview = previewLayer {
            viewData.rotationCoordinator = AVCaptureDevice.RotationCoordinator(device: device, previewLayer: preview)
            preview.connection?.videoRotationAngle = viewData.rotationCoordinator!.videoRotationAngleForHorizonLevelCapture
            
            viewData.previewObservation = viewData.rotationCoordinator!.observe(.videoRotationAngleForHorizonLevelPreview, changeHandler: { old, value in
                preview.connection?.videoRotationAngle = self.viewData.rotationCoordinator!.videoRotationAngleForHorizonLevelPreview
            })
        }
        Task(priority: .background) {
            viewData.captureSession?.startRunning()
        }
    }

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

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

  • AVCapturePhotoSettings():此初始化方法使用默认格式创建一个AVCapturePhotoSettings对象。

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

  • maxPhotoDimensions :该属性设置或返回所捕捉图片的尺寸。这是一个CMVideoDimensions类型的结构体,包含属性widthheight
  • previewPhotoFormat :该属性设置或返回一个字典,包含的键和值决定预览图片的特征。键包括kCVPixelBufferPixelFormatTypeKey (未压缩格式), kCVPixelBufferWidthKey (宽) and kCVPixelBufferHeightKey (高)。
  • flashMode :该属性设置或返回捕捉图像时使用的闪光灯模式。这是一个FlashMode类型的枚举,值有onoffauto

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

示例18-15:拍照

csharp 复制代码
    func takePicture() {
        let settings = AVCapturePhotoSettings()
        if let max = viewData.captureDevice?.activeFormat.supportedMaxPhotoDimensions.last {
            settings.maxPhotoDimensions = max
        }
        viewData.stillImage?.capturePhoto(with: settings, delegate: self)
    }

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

示例18-16:处理图像

swift 复制代码
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        let scale = scene?.screen.scale ?? 1
        let orientationAngle = viewData.rotationCoordinator!.videoRotationAngleForHorizonLevelCapture
        var imageOrientation: UIImage.Orientation!
        switch orientationAngle {
        case 90.0:
            imageOrientation = .right
        case 270.0:
            imageOrientation = .left
        case 0.0:
            imageOrientation = .up
        case 180.0:
            imageOrientation = .down
        default:
            imageOrientation = .right
        }
        if let imageData = photo.cgImageRepresentation() {
            picture = UIImage(cgImage: imageData, scale: scale, orientation: imageOrientation)
            path = NavigationPath()
        }
    }

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

  • fileDataRepresentation ():该方法返回可用于创建UIImage对象的图像数据形式。
  • cgImageRepresentation ():该方法以UIImage对象(Core Graphic)返回图像。

在本例中,我们实现了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:显示图像

scss 复制代码
struct ContentView: View {
    @Environment(ApplicationData.self) private var appData
    
    var body: some View {
        NavigationStack(path: Bindable(appData).path) {
            VStack {
                HStack {
                    Spacer()
                    NavigationLink("Take Picture", value: "Open Camera")
                }.navigationDestination(for: String.self, destination: { _ in
                    CustomCameraView()
                })
                Image(uiImage: appData.picture ?? UIImage(named: "nopicture")!)
                    .resizable()
                    .scaledToFill()
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                    .clipped()
                Spacer()
            }.padding()
                .navigationBarHidden(true)
        }.statusBarHidden()
    }
}

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

示例18-18:拍照

scss 复制代码
import SwiftUI

struct CustomCameraView: View {
    @Environment(ApplicationData.self) private var appData
    
    var body: some View {
        ZStack {
            appData.cameraView
            VStack {
                Spacer()
                HStack {
                    Button("Cancel") {
                        appData.path = NavigationPath()
                    }
                    Spacer()
                    Button("Take Picture") {
                        appData.takePicture()
                    }
                }.padding()
                    .frame(height: 80)
                    .background(Color(red: 0.9, green: 0.9, blue: 0.9, opacity: 0.8))
            }
        }.edgesIgnoringSafeArea(.all)
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
            .navigationBarHidden(true)
            .task {
                await appData.getAuthorization()
            }
            .onDisappear {
                appData.viewData.previewObservation = nil
            }
    }
}

图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)。在设备中运行应用并拍照测试。

代码请见:GitHub仓库

本文首发地址:AlanHou的个人博客,整理自2023年10月版《SwiftUI for Masterminds》

相关推荐
茂茂在长安2 分钟前
Linux 命令大全完整版(11)
java·linux·运维·服务器·前端·centos
知识分享小能手35 分钟前
Html5学习教程,从入门到精通,HTML5 简介语法知识点及案例代码(1)
开发语言·前端·javascript·学习·前端框架·html·html5
IT、木易38 分钟前
大白话React第二章深入理解阶段
前端·javascript·react.js
晚安72043 分钟前
Ajax相关
前端·javascript·ajax
图书馆钉子户1 小时前
怎么使用ajax实现局部刷新
前端·ajax·okhttp
bin91531 小时前
DeepSeek 助力 Vue 开发:打造丝滑的单选按钮(Radio Button)
前端·javascript·vue.js·ecmascript·deepseek
qianmoQ1 小时前
第五章:工程化实践 - 第五节 - Tailwind CSS 常见问题解决方案
前端·css
那就可爱多一点点1 小时前
超高清大图渲染性能优化实战:从页面卡死到流畅加载
前端·javascript·性能优化
不能只会打代码2 小时前
六十天前端强化训练之第一天HTML5语义化标签深度解析与博客搭建实战
前端·html·html5
OpenTiny社区3 小时前
Node.js技术原理分析系列——Node.js的perf_hooks模块作用和用法
前端·node.js