大师学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:在保存图片时显示警告视图

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:对其它应用分享图像

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-6:分享弹窗

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

自定义相机

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

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

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

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

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

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

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

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

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

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

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

  • maxPhotoDimensions:此属性设置或返回待捕获图片的大小。这一个​​CMVideoDimensions​​​类型的结构体,包含属性​​width​​​和​​height​​。
  • 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:自定义相机界面

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

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

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

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:定义管理相机的属性

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:请求使用相机的权限

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:初始化相机

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:在屏幕上显示来自相机的视频

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​​​类型的结构体,包含属性​​width​​​和​​height​​。
  • previewPhotoFormat:该属性设置或返回一个字典,包含的键和值决定预览图片的特征。键包括kCVPixelBufferPixelFormatTypeKey (未压缩格式), kCVPixelBufferWidthKey (宽) and kCVPixelBufferHeightKey (高)。
  • flashMode:该属性设置或返回捕捉图像时使用的闪光灯模式。这是一个​​FlashMode​​​类型的枚举,值有​​on​​​、​​off​​​和​​auto​​。

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

示例18-15:拍照

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:处理图像

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:显示图像

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:拍照

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/234831.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ffmpeg6.0之ffprobe.c源码分析二-核心功能源码分析

本篇我们继续分析: 1、ffprobe -show_packets 参数的处理流程;2、ffprobe -show_frames 参数的处理流程;3、ffprobe -show_streams 参数的处理流程;4、ffprobe -show_format 参数的处理流程; 因为前面的文章已经回顾了这些命令的使用,以及作用。本文就不在赘述,以免篇幅…

电子学会C/C++编程等级考试2022年03月(五级)真题解析

C/C++等级考试(1~8级)全部真题・点这里 第1题:数字变换 给定一个包含 5 个数字(0-9)的字符串, 例如 “02943”, 请将“12345”变换到它。 你可以采取 3 种操作进行变换 1. 交换相邻的两个数字 2. 将一个数字加 1。 如果加 1 后大于 9, 则变为 0 3. 将一个数字加倍。 如果…

查找两个总和为特定值的索引(蓝桥杯)

#include <stdio.h> int main(){int n;scanf("%d",&n);int s[n];for(int i 0 ; i < n ; i)scanf("%d",&s[i]);int k;scanf("%d",&k);int sum 0;int t0,h;int st[101]; for(int i 0 ; i < n ; i)st[i] 0; //标记数…

加载离线镜像包:在线镜像离线为tar包、tar离线镜像包加载并根据imageId打tag

第一步&#xff1a;在线环境压缩离线镜像&#xff1a; 需要两个文件&#xff0c;第一个是脚本文件image_offline_load.sh脚本&#xff0c;第二个是image_list.txt 按行 存放需要离线的镜像名称 ./image_offline_load.sh save image_list.txt output.tar第二步&#xff1a;在离…

【参天引擎】华为参天引擎内核架构专栏开始更新了,多主分布式数据库的特点,类oracle RAC国产数据开始出现了

cantian引擎的介绍 ​专栏内容&#xff1a; 参天引擎内核架构 本专栏一起来聊聊参天引擎内核架构&#xff0c;以及如何实现多机的数据库节点的多读多写&#xff0c;与传统主备&#xff0c;MPP的区别&#xff0c;技术难点的分析&#xff0c;数据元数据同步&#xff0c;多主节点的…

【Linux】进程间通信之共享内存/消息队列/信号量

文章目录 一、共享内存的概念及原理二、共享内存相关接口说明1.shmget函数2.ftok函数3.shmat函数4.shmdt函数5.shmctl函数 三、用共享内存实现server&client通信1.shm_server.cc2.shm_client.cc3.comm.hpp4.查看ipc资源及其特征5.共享内存的优缺点6.共享内存的数据结构 四、…

Spring JDBC和事务管理

Spring JDBC是Spring框架用来处理关系型数据库的模块&#xff0c;对JDBC的API进行了封装。 Spring JDBC的核心类为JdbcTemplate&#xff0c;提供数据CRUD方法 Spring JDBC使用步骤 Maven工程引入依赖spring-jdbc <dependency><groupId>org.springframework<…

案例026:基于微信小程序的原创音乐系统的设计与实现

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

建立个人学习观|地铁上的自习室

作者&#xff1a;向知 如果大家有机会来北京&#xff0c;可以来看看工作日早上八九点钟&#xff0c;15 号线从那座叫“顺义”的城市通向“望京”的地铁&#xff0c;你在那上面&#xff0c;能看到明明白白的&#xff0c;人们奔向梦想的模样。 一、地铁上的自习室 我在来北京之前…

基于JavaWeb+SSM+Vue助农扶贫微信小程序系统的设计和实现

基于JavaWebSSMVue助农扶贫微信小程序系统的设计和实现 源码获取入口Lun文目录前言主要技术系统设计功能截图 源码获取入口 Lun文目录 目 录 第一章 绪论 1 1.1 研究背景 1 1.2 研究意义 1 1.3 研究内容 2 第二章 开发环境与技术 3 2.1 JSP技术 3 2.2 MySQL数据库 3 2.3 Java…

DIP——边缘提取与分割

1.使用canny算法进行边缘提取 本实验比较简单&#xff0c;基本思路是对原图像进行一个高斯模糊处理&#xff0c;用于去噪&#xff0c;之后转换为灰度图&#xff0c;直接调用cv库中的canny记性边缘提取。若想直接得到彩色边缘&#xff0c;则通过按位与操作&#xff0c;将原始彩色…

TailwindCSS 如何处理RTL布局模式

背景 TikTok作为目前全世界最受欢迎的APP&#xff0c;需要考虑兼容全世界各个地区的本地化语言和阅读习惯。其中对于阿拉伯语、波斯语等语言的阅读书写习惯是从右向左的&#xff0c;在前端有一个专有名字RTL模式&#xff0c;即Right-to-Left。 其中以阿拉伯语作为第一语言的人…

《算法与数据结构》答疑

答疑 问题一问题二问题三问题四 问题一 在匹配成功时&#xff0c;在返回子串位置那里&#xff0c;为什么不是i-t的长度啊&#xff0c;为什么还要加一 问题二 问题三 问题四 问&#xff1a;如果题目让我们构造一个哈夫曼树&#xff0c;像我发的这个例题的话&#xff0c;我画成我…

Kafka Streams:深度探索实时流处理应用程序

Apache Kafka Streams 是一款强大的实时流处理库&#xff0c;为构建实时数据处理应用提供了灵活且高性能的解决方案。本文将深入探讨 Kafka Streams 的核心概念、详细原理&#xff0c;并提供更加丰富的示例代码&#xff0c;以帮助读者深入理解和应用这一流处理框架。 1. Kafka…

uniapp自定义的日历(纯手写)

效果图&#xff1a; html&#xff1a; <!-- 年月 --><view class"box"><view class"box_time"><view class"time"><image click"lefts" :src"url/uploads/20231206/9d1fb520b12383960dca3c214d84fa0…

三. LiDAR和Camera融合的BEV感知算法-融合算法的基本介绍

目录 前言0. 简述1. 融合背景2. 融合思路3. 融合性能优劣总结下载链接参考 前言 自动驾驶之心推出的《国内首个BVE感知全栈系列学习教程》&#xff0c;链接。记录下个人学习笔记&#xff0c;仅供自己参考 本次课程我们来学习下课程第三章——LiDAR和Camera融合的BEV感知算法&am…

数据分析基础之《numpy(2)—ndarray属性》

一、ndarray的属性 1、属性方法 属性名字属性解释ndarray.shape数组维度的元组&#xff08;形状&#xff09;ndarray.ndim数组维数ndarray.size数组中的元素数量ndarray.itemsize一个数组元素的长度&#xff08;字节&#xff09;ndarray.dtype数组元素的类型使用方法 数组名.…

List 接口

1 List 接口 java.util 中的集合类包含 Java 中某些最常用的类。最常用的集合类是 List 和 Map。 List是一种常用的集合类型&#xff0c;它可以存储任意类型的对象&#xff0c;也可以结合泛型来存储具体的类型对象&#xff0c;本质上就是一个容器。 1.1 List 类型介绍 有序性…

JVM进程缓存

引言 缓存在日常开发中启动至关重要的作用&#xff0c;由于是存储在内存中&#xff0c;数据的读取速度是非常快的&#xff0c;能大量减少对数据库的访问&#xff0c;减少数据库的压力。我们把缓存分为两类&#xff1a; 分布式缓存&#xff0c;例如Redis&#xff1a; 优点&…

Jmeter 请求签名api接口-BeanShell

Jmeter 请求签名api接口-BeanShell 项目签名说明编译扩展jar包jmeter 使用 BeanShell 调用jar包中的签名方法 项目签名说明 有签名算法的api接口本地不好测试&#xff0c;使用BeanShell 扩展jar 包对参数进行签名&#xff0c;接口签名算法使用 sha512Hex 算法。签名的说明如下…