大師學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 = falsevar 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: { _ inImagePicker(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: { _ inImagePicker(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 AVFoundationclass 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 AVFoundationclass 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 = trueviewData.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? AVCaptureVideoPreviewLayerpreviewLayer?.session = viewData.captureSessionif let device = viewData.captureDevice, let preview = previewLayer {viewData.rotationCoordinator = AVCaptureDevice.RotationCoordinator(device: device, previewLayer: preview)preview.connection?.videoRotationAngle = viewData.rotationCoordinator!.videoRotationAngleForHorizonLevelCaptureviewData.previewObservation = viewData.rotationCoordinator!.observe(\.videoRotationAngleForHorizonLevelPreview, changeHandler: { old, value inpreview.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? UIWindowScenelet scale = scene?.screen.scale ?? 1let orientationAngle = viewData.rotationCoordinator!.videoRotationAngleForHorizonLevelCapturevar imageOrientation: UIImage.Orientation!switch orientationAngle {case 90.0:imageOrientation = .rightcase 270.0:imageOrientation = .leftcase 0.0:imageOrientation = .upcase 180.0:imageOrientation = .downdefault: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 appDatavar body: some View {NavigationStack(path: Bindable(appData).path) {VStack {HStack {Spacer()NavigationLink("Take Picture", value: "Open Camera")}.navigationDestination(for: String.self, destination: { _ inCustomCameraView()})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 SwiftUIstruct CustomCameraView: View {@Environment(ApplicationData.self) private var appDatavar body: some View {ZStack {appData.cameraViewVStack {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)。在設備中運行應用并拍照測試。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/212202.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/212202.shtml
英文地址,請注明出處:http://en.pswp.cn/news/212202.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

JAVA后端自學技能實操合集

JAVA后端自學技能實操 內容將會持續更新中,有需要添加什么內容可以再評論區留言,大家一起學習FastDFS使用docker安裝FastDFS(linux)集成到springboot項目中 內容將會持續更新中,有需要添加什么內容可以再評論區留言,大家一起學習 FastDFS 組名:文件上傳后所在的 st…

leetcode 100.相同的樹

涉及到遞歸,最好多畫圖理解,希望對你們有幫助 100.相同的樹 題目 給你兩棵二叉樹的根節點 p 和 q ,編寫一個函數來檢驗這兩棵樹是否相同。 如果兩個樹在結構上相同,并且節點具有相同的值,則認為它們是相同的。 題目鏈接…

GPIO的使用--滴答定時器--pir人體紅外傳感器

目錄 一、滴答定時器的使用與原理 1、定義 2、原理 (1)向上計數?編輯 (2)向下計數 (3) 代碼流程 a、配置滴答時鐘喚醒頻率 b、滴答時鐘中斷函數 (4)結果 3、優化-->寄存…

Proxy Hook Trace JSON

Proxy var window {key: "qww",age: 22 } window new Proxy(window, {get(target, p, receiver) {console.log("target: ", target);console.log("p: ", p);// return window[username];/// 這里如果這樣寫. 有遞歸風險的...// return Reflec…

【線性代數與矩陣論】Jordan型矩陣

Jordan型矩陣 2023年11月3日 #algebra 文章目錄 Jordan型矩陣1. 代數重數與幾何重數2. Jordan塊與Jordan標準型2.1 最小多項式與Jordan標準型2.2 兩類重要矩陣 3. 矩陣的Jordan分解3.1 Jordan分解的應用 下鏈 1. 代數重數與幾何重數 在對向量做線性變換時,向量空間…

讀書筆記-《數據結構與算法》-摘要4[插入排序]

插入排序 核心:通過構建有序序列,對于未排序序列,在已排序序列中從后向前掃描(對于單向鏈表則只能從前往后遍歷),找到相應位置并插入。實現上通常使用in-place排序(需用到O(1)的額外空間) 從第一個元素開始,該元素可…

如何主持一場知識競賽搶答賽

知識競賽主持說難不難,說簡單也不簡單,我就從易到難介紹一下。 入門級,題主不用練習太多其他花哨的技巧,只要注意一點,熟悉比賽流程。知識競賽需要給所有選手一個公平流暢的答題環境,所以題主自身必須非常…

干貨!接口中的大事務,該如何進行優化?

作為后端開發的程序員,我們常常會的一些相對比較復雜的邏輯,比如我們需要給前端寫一個調用的接口,這個接口需要進行相對比較復雜的業務邏輯操作,比如會進行,查詢、遠程接口或本地接口調用、更新、插入、計算等一些邏輯…

掌握iText:輕松處理PDF文檔-進階篇

簡體中文寫入 iText本身對簡體中文的支持有限,但可以通過引入額外的字體包來增強其對簡體中文的支持。例如,可以使用iTextAsian.jar這個亞洲字體包,它包含了幾種簡單的亞洲字體,其中包括簡體中文字體。只需要將iTextAsian.jar放到…

springboot 啟動之后報錯:Unsatisfied dependency through field ‘bbbClient’

springboot 啟動之后報錯:UnsatisfiedDepencyException:Error creating bean with name ‘aaaServiceImpl’: Unsatisfied dependency through field ‘bbbClient’。 這兩天一直在進行著日常 debugger 查看代碼。可是發生了一個挺“靈異”的事件。那就是我看的項目…

46. 全排列

46. 全排列 原題鏈接:完成情況:解題思路:參考代碼:_46全排列_構建數組回溯_46全排列_直接構建 錯誤經驗吸取 原題鏈接: 46. 全排列 https://leetcode.cn/problems/permutations/description/ 完成情況:…

codeforces D.In Love

思路 用兩個 m u l t i s e t multiset multiset 分別存 l , r l,r l,r 。你也可以寫平衡樹在 l l l 的 m u l t i s e t multiset multiset 里去查詢是否存在比最小的 r r r 大的 l l l 。 Think Twice, Code Once #include<bits/stdc.h> #define il inline #d…

小模型學習(1)-人臉識別

【寫作背景】因為最近一直在研究大模型&#xff0c;在與客戶進行交流時&#xff0c;如果要將大模型的變革性能力講清楚&#xff0c;就一定要能將AI小模型的一些原理和效果講清楚&#xff0c;進而形成對比。當然這不是一件簡單的事情&#xff0c;一方面大模型分析問題的的本質原…

Mybatis分頁插件PageHelper

PageHelper是什么&#xff1f; 是MyBatis提供的分頁插件&#xff0c;可以支持MySQL、Oracle等六種數據庫。 集成方式如下&#xff1a; 1 引入依賴 <!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper --> <dependency><groupId>co…

反射加載SDK完成統一調用

文章目錄 1、需求背景2、接口抽象類具體實現類3、疑問4、存在的問題5、通過反射加載SDK并完成調用5、補充&#xff1a;關于業務網關7、補充&#xff1a;關于SDK的開發 關鍵點&#xff1a; 接口抽象類&#xff08;半抽象半實現&#xff09;具體實現類業務網關反射加載SDK&#…

JAVA如何調用python

以下代碼想通過測試&#xff0c;必須有一個前提&#xff1a;電腦上安裝了Python環境。不太習慣說廢話&#xff0c;直接上代碼了。 以下是用于測試的python代碼&#xff08;mytest.py&#xff09;&#xff1a; # 因為用戶到了參數處理&#xff0c;所以需要引用 import argpars…

Java學習手冊——第五篇數據類型

數據類型&#xff1a;是數據化的基石&#xff0c;如果沒有數據類型怎么表示呢&#xff1f;比如年齡可以用整數&#xff1a;18歲。如果有更好的表示方式大家可以留言喲~ 在舉個例子就是姓名&#xff0c;我們需要用字符串的形式來表示。這就是數據類型的魅力&#xff0c;而又有同…

TS基礎語法

前言&#xff1a; 因為在寫前端的時候&#xff0c;發現很多UI組件的語法都已經開始使用TS語法&#xff0c;不學習TS根本看不到懂&#xff0c;所以簡單的學一下TS語法。為了看UI組件的簡單代碼&#xff0c;不至于一臉懵。 一、安裝node 對于windows來講&#xff0c;node版本高…

電腦出現這些現象,說明你的固態硬盤要壞了

與傳統機械硬盤&#xff08;HDD&#xff09;相比&#xff0c;固態硬盤&#xff08;SSD&#xff09;速度更快、更穩定、功耗更低。但固態硬盤并不是完美無瑕的&#xff0c;由于顆粒寫入機制&#xff0c;可能會在七到十年的預期壽命之前出現故障。所以用戶最好為最終故障做好準備…

網頁設計中增強現實的興起

目錄 了解增強現實 增強現實的歷史背景 AR 和網頁設計的交叉點 AR 在網頁設計中的優勢 增強參與度和互動性 個性化的用戶體驗 競爭優勢和品牌差異化 AR 在網頁設計中的用例 結論 近年來&#xff0c;增強現實已成為一股變革力量&#xff0c;重塑了我們與數字領域互動的方式。它被…