iOS App 程式開發

應用 AVFoundation 建立一個全螢幕相機 App

應用 AVFoundation 建立一個全螢幕相機 App
應用 AVFoundation 建立一個全螢幕相機 App
In: iOS App 程式開發

今天,我們將學習如何使用 AV Foundation ,它是一個 Apple 系統框架,適用於 macOS、iOS、watchOS 和 tvOS 操作系統上。本教學的目標就是幫助你開發一個有完整功能的 iOS App,能夠使用裝置的相機來拍照與錄影。我們亦會以物件導向程式基礎,設計一個實用的類別,讓它可重覆使用與延伸至你所有的專案中。

注意:本篇教學需要用到實體 iOS 裝置,單靠模擬器無法完成本篇的示範 App。且本篇教學需要讀者對 UIkit 基礎有深入了解,包括如 Action (動作)、Interface Builder (介面建構器)、與 Storyboard (故事板),並對 Swift 語法有一定程度的了解。

提示:你需要使用 Xcode 9(或以上版本)執行此範例程式,所有程式碼已更新至 Swift 4。

甚麼是 AV Foundation?

根據 Apple,AV Foundation 是一個在 iOS、macOS、watchOS 與 tvOS 系統上以時間為主的影音系統框架,你可以輕易地使用 AV Foudation 來撥放、建立與編輯 QuickTime 影片和 MEPG-4 檔案格式、撥放 HLS 串流檔,或是在你的 App 內建立強大影音功能。

明白了吧? AV Foundation 是一個可以在 Apple 裝置上擷取、處理、編輯聲音與影像的系統框架。在本篇教學中,我們將會特別介紹如何使用它,配合前後相機、閃光功能與聲音,來擷取照片與影片。

我需要 AV Foundation 嗎?

在開始這段教學旅程前,你要知道 AV Foundation 是一個複雜又詳細的工具。在很多的案例中,其實應用 Apple 預設的 APIs 如UIImagePickerController就已足夠。所以在開始前,請確認你是否真的需要應用 AV Foundation。

Sessions, Devices, Inputs 和 Outputs

利用 AV Foundation 來擷取相片或影片的核心就是 Capture Sessions。根據 Apple,Capture Session 是用以「管理擷取活動、並協調來自 Input Devices 到擷取 Outputs 的數據流」。在 AV Foundation 內,Capture Sessions 是由AVCaptureSession來管理的。

此外,所謂的 Capture Device 是指在 iOS 裝置上,可實際得到真實聲音與影像的擷取設備。為了使用 AV Foundation,你要利用 Capture Devices 建立 Capture Inputs,並提供予 Capture Session,然後將結果存入 Capture Outputs。以下的架構圖可清楚描述它們的關係:

Flowchart

專案範例

一如往昔,你需要親自探索如何使用這個框架。我們將會提供一個專案範例讓你學習,但我們要先專注在 AV Foundation 的框架上,所以本篇教學會以一個簡易的專案開始。在開始前,請你先在此下載專案,並簡單看一下專案的內容。

這個專案範例內容比較簡單,包括:

  • 一個Assets.xcassets檔,裡面有所有這個專輯範例所需的 icon 圖檔。圖檔是 Google 的 Material Design 團隊設計的,你也可以在這裡material.io/icons 免費下載其他的相關圖檔。
  • 一個 Storyboard 檔和一個 View Controller,我們的 App 將會用這個 View Controller 來處理所有相片與影像擷取,包括:
    • 一個擷取按鈕,用來啟動相片或影像的擷取
    • 一個擷取預覽畫面,可實時看到相機鏡頭要拍攝的畫面
    • 可切換前後相機與閃光燈的功能
  • 一個Viewcontroller.swift檔來管理上面提到的 View Controller,包括:
    • 所有將上述 UI control 與 Code 連結的 Outlet
    • 一個計算屬性 (Computed Property) 來隱藏狀態欄
    • 一個設定功能來適當地設計擷取按鈕的樣式

執行這個專案,你應該會看見這個畫面:

AV Foundation Sample Project

好,我們現在就開始吧!

應用 AVFoundation 實作

在這個教學內,我們將設計一個類別CameraController,這個類別在相片與影像的擷取方面十分重要,我們的 View Controller 將會使用CameraController,並把它結合到使用者介面。

開始前,請先在專案內建立一個新的 Swift 檔,命名為CameraController.swift。然後,在程式碼介面上 import AVFoundation,並宣告一個空類別,像下列所示:

import AVFoundation
 
class CameraController { }

相片擷取

首先,我們將實作利用後相機來擷取相片。這是本教學的基本功能,我們會以此為基礎,加入切換前後相機、開關閃光燈、和錄製影片的功能。由於設定與開始一個 Capture Session 程序較為緊湊,我們將會分成幾個步驟。我們init時,先建立一個叫prepare的函式,準備好 Capture Session 予使用,完成後呼叫 Completion Handler。請把這段prepare函式加到CameraController類別內:

func prepare(completionHandler: @escaping (Error?) -> Void) { }

這個函式將負責建立與設定一個新的 Capture Session。記住,設定 Capture Session 分為四個步驟:

  1. 建立一個 Capture Session
  2. 取得與設定必要的 Capture Devices
  3. 在 Capture Device 上建立 Inputs
  4. 設定一個 Photo Output 物件來處理擷取到的影像

我們將使用 Swift 的巢狀 (nest) 函式來封裝我們的程式碼。先在prepare內宣告四個空函式,然後呼叫它們:

func prepare(completionHandler: @escaping (Error?) -> Void) {
    func createCaptureSession() { }
    func configureCaptureDevices() throws { }
    func configureDeviceInputs() throws { }
    func configurePhotoOutput() throws { }
    
    DispatchQueue(label: "prepare").async {
        do {
            createCaptureSession()
            try configureCaptureDevices()
            try configureDeviceInputs()
            try configurePhotoOutput()
        }
            
        catch {
            DispatchQueue.main.async {
                completionHandler(error)
            }
            
            return
        }
        
        DispatchQueue.main.async {
            completionHandler(nil)
        }
    }
}

在上述的程式碼中,我們已經建立了一套樣板函式,執行準備AVCaptureSession來擷取相片的四個步驟。我們也設計了一個非同步計算處理模式來呼叫這四個函式,必要時會捕獲任何錯誤,完成時就會呼叫 Completion Handler。現在就只差實作這四個函式了!我們從createCaptureSession開始吧。

建立 Capture Session

在設定一個特定的AVCaptureSession之前,我們要先建立它。將下列屬性變數加到CameraController.swift程式碼:

var captureSession: AVCaptureSession?

接下來,在prepare內的巢狀函式createCaptureSession加入下列程式碼:

self.captureSession = AVCaptureSession()

這個程式碼很簡單,就這樣建立一個新的AVCaptureSession,並將它儲存在captureSession的屬性裡。

設定 Capture Devices

建立了一個AVCaptureSession後,我們要建立AVCaptureDevice物件來代表實際的 iOS 裝置相機,跟著我來加入這個屬性變數到CameraController類別內吧!現在我們將加入frontCamerarearCamera屬性變數,讓之後可以設定多鏡頭擷取功能,並執行切換相機的功能。

var frontCamera: AVCaptureDevice?
var rearCamera: AVCaptureDevice?

然後,在CameraController.swift宣告一個嵌入類型,我們將使用這個嵌入類型來管理建立 Capture Session 時可能遇到的各種錯誤:

   enum CameraControllerError: Swift.Error {
        case captureSessionAlreadyRunning
        case captureSessionIsMissing
        case inputsAreInvalid
        case invalidOperation
        case noCamerasAvailable
        case unknown
    }

你會發現在這 enum 內有不同類型的錯誤碼。先將它們加到你的程式碼內,我們待會就會用到它們。

現在,來了最有趣的部份了!來找出裝置上可用的相機吧!我們可以利用AVCaptureDeviceDiscoverySession來找,將下列程式碼加入到configureCaptureDevices

//1
let session = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera], mediaType: AVMediaType.video, position: .unspecified)
let cameras = session.devices.compactMap { $0 }

guard !cameras.isEmpty else { throw CameraControllerError.noCamerasAvailable }

//2
for camera in cameras {
    if camera.position == .front {
        self.frontCamera = camera
    }

    if camera.position == .back {
        self.rearCamera = camera

        try camera.lockForConfiguration()
        camera.focusMode = .continuousAutoFocus
        camera.unlockForConfiguration()
    }
}

讓我解釋一下上面的程式碼吧:

  1. 這三行程式碼使用了`AVCaptureDeviceDiscoverySession`找出裝置上所有可用的內置相機 (`.builtInDualCamera`)。若找不到任何實體相機,就會出現錯誤訊息。
  2. 這個循環會查看從第一段程式碼找到的可用相機,從而分辦前後相機。然後,將後相機設定為自動對焦,過程中如遇上任何錯誤,也會出現錯誤訊息。

好了,我們使用了AVCaptureDeviceDiscoverySession來找出裝置上可用的相機,並設定了所需要的功能。下一步,我們要將它們連結到 Capture Session。

設定 Device Inputs

現在我們可以建立 Capture Device Inputs,這步驟將會用到 Capture Device,並把它連結到 Capture Session。開始之前,先將下列屬性變數加至CameraController,以確保我們可以儲存 Inputs:

var currentCameraPosition: CameraPosition?
var frontCameraInput: AVCaptureDeviceInput?
var rearCameraInput: AVCaptureDeviceInput?

你會發現程式碼在這個狀態下不會編譯,那是因為CameraPosition沒有被定義。要來定義它,請在CameraController加入另一個嵌入類型:

public enum CameraPosition {
    case front
    case rear
}

很好,有了儲存和管理 Capture Device Inputs 所有需要的屬性,就可以實作configureDeviceInputs

func configureDeviceInputs() throws {
    //1
    guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }

    //2
    if let rearCamera = self.rearCamera {
        self.rearCameraInput = try AVCaptureDeviceInput(device: rearCamera)

        if captureSession.canAddInput(self.rearCameraInput!) { captureSession.addInput(self.rearCameraInput!) }

        self.currentCameraPosition = .rear
    }

    else if let frontCamera = self.frontCamera {
        self.frontCameraInput = try AVCaptureDeviceInput(device: frontCamera)

        if captureSession.canAddInput(self.frontCameraInput!) { captureSession.addInput(self.frontCameraInput!) }
        else { throw CameraControllerError.inputsAreInvalid }

        self.currentCameraPosition = .front
    }

    else { throw CameraControllerError.noCamerasAvailable }
}

讓我解釋一下這些程式碼的意思吧:

  1. 這行先簡單地確認`captureSession`是否存在,若不存在就會出現錯誤訊息。
  2. 這些`if`流程主要是要建立所需的 Capture Device Input 來支援相片擷取。`AVFoundation`每一次 Capture Session 僅能允許一台相機的輸入。由於裝置的初始設定通常是後相機,所以我們會先嘗試用後相機建立 Input,再加到 Capture Session;如出現錯誤,就會轉成前相機;若還是有問題,就會出現錯誤訊息。

設定 Photo Outputs

到目前為止,我們已經加了所有必要的 Inputs 到captureSession,現在我們只需一種方式從 Capture Session 中獲取必要的資料。我們可以使用AVCapturePhotoOutput,請多加一個屬性變數至CameraController

var photoOutput: AVCapturePhotoOutput?

現在我們來實作configurePhotoOutput

func configurePhotoOutput() throws {
    guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }

    self.photoOutput = AVCapturePhotoOutput()
    self.photoOutput!.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey : AVVideoCodecJPEG])], completionHandler: nil)

    if captureSession.canAddOutput(self.photoOutput!) { captureSession.addOutput(self.photoOutput!) }

    captureSession.startRunning()
}

這是一個簡單的實作方式,我們設定了photoOutput,讓它使用 JPEG 檔案格式作為影片編碼格式。然後,它會將photoOutput加入到captureSession。最後,就開始進行captureSession

快完成了!你的CameraController.swift檔應該是這樣的:

import AVFoundation
 
class CameraController {
    var captureSession: AVCaptureSession?
 
    var currentCameraPosition: CameraPosition?
 
    var frontCamera: AVCaptureDevice?
    var frontCameraInput: AVCaptureDeviceInput?
 
    var photoOutput: AVCapturePhotoOutput?
 
    var rearCamera: AVCaptureDevice?
    var rearCameraInput: AVCaptureDeviceInput?
}
 
extension CameraController {
    func prepare(completionHandler: @escaping (Error?) -> Void) {
        func createCaptureSession() {
            self.captureSession = AVCaptureSession()
        }
 
        func configureCaptureDevices() throws {
            let session = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera], mediaType: AVMediaType.video, position: .unspecified)
                        let cameras = session.devices.compactMap { $0 }
            guard !cameras.isEmpty else { throw CameraControllerError.noCamerasAvailable }
 
            for camera in cameras {
                if camera.position == .front {
                    self.frontCamera = camera
                }
 
                if camera.position == .back {
                    self.rearCamera = camera
 
                    try camera.lockForConfiguration()
                    camera.focusMode = .autoFocus
                    camera.unlockForConfiguration()
                }
            }
        }
 
        func configureDeviceInputs() throws {
            guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
 
            if let rearCamera = self.rearCamera {
                self.rearCameraInput = try AVCaptureDeviceInput(device: rearCamera)
 
                if captureSession.canAddInput(self.rearCameraInput!) { captureSession.addInput(self.rearCameraInput!) }
 
                self.currentCameraPosition = .rear
            }
 
            else if let frontCamera = self.frontCamera {
                self.frontCameraInput = try AVCaptureDeviceInput(device: frontCamera)
 
                if captureSession.canAddInput(self.frontCameraInput!) { captureSession.addInput(self.frontCameraInput!) }
                else { throw CameraControllerError.inputsAreInvalid }
 
                self.currentCameraPosition = .front
            }
 
            else { throw CameraControllerError.noCamerasAvailable }
        }
 
        func configurePhotoOutput() throws {
            guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
 
            self.photoOutput = AVCapturePhotoOutput()
            self.photoOutput!.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey : AVVideoCodecJPEG])], completionHandler: nil)
 
            if captureSession.canAddOutput(self.photoOutput!) { captureSession.addOutput(self.photoOutput!) }
            captureSession.startRunning()
        }
 
        DispatchQueue(label: "prepare").async {
            do {
                createCaptureSession()
                try configureCaptureDevices()
                try configureDeviceInputs()
                try configurePhotoOutput()
            }
 
            catch {
                DispatchQueue.main.async {
                    completionHandler(error)
                }
 
                return
            }
 
            DispatchQueue.main.async {
                completionHandler(nil)
            }
        }
    }
    
}
 
extension CameraController {
    enum CameraControllerError: Swift.Error {
        case captureSessionAlreadyRunning
        case captureSessionIsMissing
        case inputsAreInvalid
        case invalidOperation
        case noCamerasAvailable
        case unknown
    }
 
    public enum CameraPosition {
        case front
        case rear
    }
}

我用了 extension 適當地區分程式碼,這不是必須的;但我認為這是一個好習慣,讓程式碼更容易理解和編寫。

顯示預覽畫面

現在我們已經準備好相機裝置了,是時候來看看它在螢幕上擷取的內容了。在CameraControllerprepare外加入另一個函式,命名為displayPreview。函式應有這個特徵:

func displayPreview(on view: UIView) throws { }

另外,要import UIKit到你的CameraController.swift檔,我們也將需要應用UIView

顧名思義,這個函式主要負責建立一個擷取的預覽畫面,並顯示於提供的視圖上。現在,在CameraController加入一個屬性變數到來支援這個函式:

var previewLayer: AVCaptureVideoPreviewLayer?

這個屬性變數會讓預覽層顯示captureSession的 Output。現在我們來實作這個方法:

func displayPreview(on view: UIView) throws {
    guard let captureSession = self.captureSession, captureSession.isRunning else { throw CameraControllerError.captureSessionIsMissing }

    self.previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
    self.previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
    self.previewLayer?.connection?.videoOrientation = .portrait

    view.layer.insertSublayer(self.previewLayer!, at: 0)
    self.previewLayer?.frame = view.frame
}

這個函式應用captureSession來建立一個AVCaptureVideoPreview,並設定它為一個直向預覽畫面,再加入到所提供的視圖上。

整理

好了,現在我們試著把這些連接到 View Controller。請轉到ViewController.swift。首先,在ViewController.swift加入一個屬性變數:

let cameraController = CameraController()

接著,在viewDidLoad()內加入一個巢狀函式:

func configureCameraController() {
    cameraController.prepare {(error) in
        if let error = error {
            print(error)
        }

        try? self.cameraController.displayPreview(on: self.capturePreviewView)
    }
}

configureCameraController()

這個函式就只是將 Camera Controller 設定成我們設計的那樣。

最後一步,就是 Apple 規定的安全性要求,你必須提供一個理由向使用者解釋 App 需要使用相機權限的原因。請開啟Info.plist,並如圖加入一行指令:

privacy-info-list-camera

當 App 需要取得的使用者的的允許時,這個 Key 就向使用者解釋原因。

現在你的ViewController.swift檔應該是這樣的:

import UIKit

class ViewController: UIViewController {
    let cameraController = CameraController()

    @IBOutlet fileprivate var captureButton: UIButton!

    ///Displays a preview of the video output generated by the device's cameras.
    @IBOutlet fileprivate var capturePreviewView: UIView!

    ///Allows the user to put the camera in photo mode.
    @IBOutlet fileprivate var photoModeButton: UIButton!
    @IBOutlet fileprivate var toggleCameraButton: UIButton!
    @IBOutlet fileprivate var toggleFlashButton: UIButton!

    ///Allows the user to put the camera in video mode.
    @IBOutlet fileprivate var videoModeButton: UIButton!

    override var prefersStatusBarHidden: Bool { return true }
}

extension ViewController {
    override func viewDidLoad() {
        func configureCameraController() {
            cameraController.prepare {(error) in
                if let error = error {
                    print(error)
                }

                try? self.cameraController.displayPreview(on: self.capturePreviewView)
            }
        }

        func styleCaptureButton() {
            captureButton.layer.borderColor = UIColor.black.cgColor
            captureButton.layer.borderWidth = 2

            captureButton.layer.cornerRadius = min(captureButton.frame.width, captureButton.frame.height) / 2
        }

        styleCaptureButton()
        configureCameraController()
    }
}

請建立並執行你的專案,裝置要求允許使用權限時請按接受。然後——耶!你應該已經有一個可運作的擷取預覽畫面,若沒有的話,請重新確認你的程式碼。如果需要協助,你也可以留言。

avfoundation-app-demo

開關閃光燈/切換相機功能

現在有了一個可運作的預覽畫面,我們來繼續加入其他功能吧。許多相機 App 都可以讓使用者切換相機、啟用或關閉閃光燈,我們也來添加這個功能吧。完成後,我們將會增加擷取相片並儲存到相機膠卷的功能。

要啟用開關閃光燈的功能,首先將這個屬性變數加到CameraController

var flashMode = AVCaptureDevice.FlashMode.off

然後轉到ViewController,加入一個@IBAction函式來開關閃光燈:

@IBAction func toggleFlash(_ sender: UIButton) {
    if cameraController.flashMode == .on {
        cameraController.flashMode = .off
        toggleFlashButton.setImage(#imageLiteral(resourceName: "Flash Off Icon"), for: .normal)
    }

    else {
        cameraController.flashMode = .on
        toggleFlashButton.setImage(#imageLiteral(resourceName: "Flash On Icon"), for: .normal)
    }
}

完成了!現在,當我想要擷取一張相片時,我們的CameraController類別就會控制閃光燈的開關。接著,讓我們啟用切換前後相機功能。

在 AV Foundation 切換前後相機是非常簡單的,我們僅需要移除在原本(前/後)相機的 Capture Input,並增加新的 Capture Input 到想切換到的相機就可以。要啟用這個功能,先添加另一個函式到CameraController

func switchCameras() throws { }

當我們切換相機時,我們會選擇切換到前或後相機,所以要在switchCameras內宣告兩個巢狀函式:

func switchToFrontCamera() throws { }
func switchToRearCamera() throws { }

然後,將下列程式碼添加到switchCameras()內:

//5
guard let currentCameraPosition = currentCameraPosition, let captureSession = self.captureSession, captureSession.isRunning else { throw CameraControllerError.captureSessionIsMissing }

//6
captureSession.beginConfiguration()

func switchToFrontCamera() throws { }
func switchToRearCamera() throws { }

//7
switch currentCameraPosition {
case .front:
    try switchToRearCamera()

case .rear:
    try switchToFrontCamera()
}

//8
captureSession.commitConfiguration()

讓我解釋一下上列程式碼的功能:

  1. 這個`guard`語法可以確保在切換相機時,有一個有效可運作的 Capture Session,同時確認有正在使用的相機裝置。
  2. 這行告知 Capture Session 開始設定。
  3. 視乎正在使用前或後相機,使用 `switch` 語法,呼叫`switchToRearCamera`或`switchToFrontCamera`
  4. 最後,提交或儲存設定好的 Capture Session。

很好!現在我們開始實作switchToFrontCameraswitchToRearCamera的功能:

func switchToFrontCamera() throws {
    
    guard let rearCameraInput = self.rearCameraInput, captureSession.inputs.contains(rearCameraInput),
        let frontCamera = self.frontCamera else { throw CameraControllerError.invalidOperation }
    
    self.frontCameraInput = try AVCaptureDeviceInput(device: frontCamera)
    
    captureSession.removeInput(rearCameraInput)
    
    if captureSession.canAddInput(self.frontCameraInput!) {
        captureSession.addInput(self.frontCameraInput!)
        
        self.currentCameraPosition = .front
    }
        
    else {
        throw CameraControllerError.invalidOperation
    }
}

func switchToRearCamera() throws {
    
    guard let frontCameraInput = self.frontCameraInput, captureSession.inputs.contains(frontCameraInput),
        let rearCamera = self.rearCamera else { throw CameraControllerError.invalidOperation }
    
    self.rearCameraInput = try AVCaptureDeviceInput(device: rearCamera)
    
    captureSession.removeInput(frontCameraInput)
    
    if captureSession.canAddInput(self.rearCameraInput!) {
        captureSession.addInput(self.rearCameraInput!)
        
        self.currentCameraPosition = .rear
    }
        
    else { throw CameraControllerError.invalidOperation }
}

兩種函式的程式碼有著十分類似的實作方式。首先,取得 Capture Session 內所有 Inputs 的數組,並確保能切換至所要求的(前/後)相機。下一步,要建立所需的 Input Device,我們只要移除掉舊有的,再增加一個新 Device 就可以了。最後,設定currentCameraPosition,這樣CameraController才會注意到這個改變。很容易吧!回到ViewController.swift,我們就可以加入一個函式來切換相機了:

@IBAction func switchCameras(_ sender: UIButton) {
    do {
        try cameraController.switchCameras()
    }

    catch {
        print(error)
    }

    switch cameraController.currentCameraPosition {
    case .some(.front):
        toggleCameraButton.setImage(#imageLiteral(resourceName: "Front Camera Icon"), for: .normal)

    case .some(.rear):
        toggleCameraButton.setImage(#imageLiteral(resourceName: "Rear Camera Icon"), for: .normal)

    case .none:
        return
    }
}

好!現在打開你的 stroryboard,連接所需的 outlets,建立並執行你的 App。此時,你應該能夠自由地切換相機了!我們現在要實作最重要的功能——相片擷取了!

實作 Image Capture

現在我們要實作期待許久的功能:Image Capture 了。開始之前,讓我們先回顧目前已實作的功能:

  • 設計了一個實用的類別,可以輕易隱藏 AV Foundation 的複雜性。
  • 在這個類別內實作了許多功能,讓我們建立 Capture Session、開關閃光燈、切換相機、和顯示預覽畫面。
  • 將類別與`UIViewController`連接,並建立一個輕量化的相機 App。

現在我們就只差擷取相片的功能了!

打開CameraController.swift並開始作業。我們先要加入一個有這個特徵的captureImage函式:

func captureImage(completion: (UIImage?, Error?) -> Void) {

}

這個函式顧名思義,就是會擷取一張圖像,供我們設計的 Camera Controller 使用。一起來實作它吧:

func captureImage(completion: @escaping (UIImage?, Error?) -> Void) {
    guard let captureSession = captureSession, captureSession.isRunning else { completion(nil, CameraControllerError.captureSessionIsMissing); return }

    let settings = AVCapturePhotoSettings()
    settings.flashMode = self.flashMode

    self.photoOutput?.capturePhoto(with: settings, delegate: self)
    self.photoCaptureCompletionBlock = completion
}

這並不是一個複雜的實作,但你會發現程式碼還不能編譯,因為我們還沒定義photoCaptureCompletionBlockCameraController來符合AVCapturePhotoCaptureDelegate。首先,將一個屬性變數photoCaptureCompletionBlock加到CameraController

var photoCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?

然後,擴展CameraController以符合AVCapturePhotoCaptureDelegate

extension CameraController: AVCapturePhotoCaptureDelegate {
    public func photoOutput(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?,
                        resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Swift.Error?) {
        if let error = error { self.photoCaptureCompletionBlock?(nil, error) }
            
        else if let buffer = photoSampleBuffer, let data = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: buffer, previewPhotoSampleBuffer: nil),
            let image = UIImage(data: data) {
            
            self.photoCaptureCompletionBlock?(image, nil)
        }
            
        else {
            self.photoCaptureCompletionBlock?(nil, CameraControllerError.unknown)
        }
    }
}

很好,但現在,編譯又出現另一個問題:

Type 'CameraController' does not conform to protocol 'NSObjectProtocol'

要解決這個問題,我們只需要建立CameraController來繼承NSObject。將CameraController的類別宣告修正為class CameraController: NSObject就可以了。

現在回過再看看ViewController。首先,導入Photos框架,因為我們將使用內置的 APIs 來儲存相片。

import Photos

接著,插入下列的函式:

@IBAction func captureImage(_ sender: UIButton) {
    cameraController.captureImage {(image, error) in
        guard let image = image else {
            print(error ?? "Image capture error")
            return
        }
        
        try? PHPhotoLibrary.shared().performChangesAndWait {
            PHAssetChangeRequest.creationRequestForAsset(from: image)
        }
    }
}

我們呼叫了 Camera Controller 內的captureImage方法來擷取相片,然後使用PHPhotoLibary類別來儲存圖片到內置的相片資料庫。

最後,連接@IBAction func到 Storyboard 的 capture 按鈕,並到Info.plist加入一行:

privacy-info-list-photolib

這是 iOS 10 的私隱要求,你要解釋 App 需要使用相片資料庫的原因。

現在,你可以建立並執行 App 來擷取圖片,然後在相片資料庫看到剛擷取的相片。恭喜你!現在你懂得如何在你的 App 中使用 AV Foundation 了!本篇教學第二部將涵蓋擷取影像的部分,敬請期待!

若想要完整的專案內容,你可以從 Github 下載

譯者簡介:Oliver Chen-工程師,喜歡美麗的事物,所以也愛上Apple,目前在iOS程式設計上仍是新手,正研讀Swift與Sketch中。生活另一個身份是兩個孩子的爸,喜歡和孩子一起玩樂高,幻想著某天自己開發的App,可以讓孩子覺得老爸好棒!。聯絡方式:電郵[email protected]

原文Building a Full Screen Camera App Using AVFoundation

作者
Pranjal Satija
現時為高中生,喜歡在課餘時間創作App,享受把創作App的經驗與人分享。除此之外還喜歡滑雪、高爾夫球、足球,與朋友在一起。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。