Swiftでカメラアプリを作成する(1)

# Swift4 でカメラアプリを作成する(1)

Swift4とiOS11でカメラアプリを作成します。
AVCaptureSessionクラス周りの設定がややこしかったので、メソッドやパラメータを一つずつ説明しながら進めていきます。

# 開発環境

- Swift4
- iOS11
- Xcode9

# カメラアプリの機能

作成するカメラアプリは、iSightカメラ(背面のカメラ)で静止画の撮影ができるアプリです。FaceTimeカメラは使用しませんが、今後のために設定だけ行います。

# AVFoundationの概要

まずはカメラ処理の大まかな流れを説明します。カメラアプリを作成するには「AVFoundation」というフレームワークを使用します。音声や画像、動画の作成や再生の細かい作業を行うためのAPIです。
カメラやマイクの入力情報と、画像や動画、オーディオなどの出力データの管理を行うAVCaptureSessionクラスによってデータの流れを管理しています。

![ipone.png](https://qiita-image-store.s3.amazonaws.com/0/239179/866aed24-42ce-ad1a-ecd6-faec6e98576d.png)

カメラなどのデバイスを表すAVCaptureDeviceクラスで取得した入力テータを、AVCaptureDeviceInputクラスを用いてAVCaptureSessionに渡します。AVCaptureSessionクラスから出力されるデータは、出力形式を管理するAVCaptureOutputクラスによって、画像、動画、フレームデータ、音声データ、メターデータなど様々な形式で出力されます。

# カメラアプリのフロー

今回作成するカメラアプリの処理フローは、以下の通りです。

1. AVCaptureSessionの設定
2. AVCaptureDeviceクラスを用いたデバイスの設定
3. 入力・出力データの設定
4. カメラの取得している映像の表示
5. UIの設定
6. カメラと写真の利用許可
7. 撮影ボタン押下時のアクション設定
8. 撮影した画像の保存

今回は1~4までを説明します。各セクションごとに関数を作成して、カメラ機能を実装していきます。作成した関数を、ライフサイクルメソッドviewDidLoadで順番に呼び出します。

# AVCaptureSessionの設定

まずは、AVFoundationをインポートします。

```swift
import UIKit
import AVFoundation

class ViewController: UIViewController {
    ・
    ・
    ・
}
```

はじめに、デバイスからの入力と出力を管理する[AVCaptureSession](https://developer.apple.com/documentation/avfoundation/avcapturesession)クラスを使用し、カメラの画質を決定するsessionPresetの値を設定します。

```swift
// デバイスからの入力と出力を管理するオブジェクトの作成
var captureSession = AVCaptureSession()

// カメラの画質の設定
func setupCaptureSession() {
    captureSession.sessionPreset = AVCaptureSession.Preset.photo
}
```

sessionPreset値は[公式リファレンス](https://developer.apple.com/documentation/avfoundation/avcapturesession.preset)に列挙されています。今回は、高解像度の画像出力ができるphotoを設定します。

# AVCaptureDeviceクラスを用いたデバイスの設定

次にカメラデバイスの管理を行う[AVCaptureDevice](https://developer.apple.com/documentation/avfoundation/avcapturedevice)クラスを用いて、カメラやマイクなどのデバイス本体の設定を行います。
[AVCaptureDevice.DiscoverySession](https://developer.apple.com/documentation/avfoundation/avcapturedevice.discoverysession)クラスでカメラデバイスの種類([deviceTypes](https://developer.apple.com/documentation/avfoundation/avcapturedevice.devicetype))、取得するメディアの種類([mediaType](https://developer.apple.com/documentation/avfoundation/avmediatype))、FaceTimeカメラとiSightカメラ([position](https://developer.apple.com/documentation/avfoundation/avcapturedevice.position))の選択をし、デバイスのプロパティを設定しセッションを取得します。今回は、カメラの種類に広角カメラ(builtInWideAngleCamera)、メディアの種類に描画(video)、FaceTimeカメラとiSightカメラのどちらも(unspecified)を設定します。
取得したデバイスのセッションから利用可能なデバイスをdevicesメソッドで取得し、FaceTimeカメラとiSightカメラそれぞれの管理オブジェクトに代入します。その後、アプリ起動時に使用するカメラを設定します。今回は、iSightカメラ（背面のカメラ）を使用するので、Positionがbackのカメラを設定します。

```swift
// カメラデバイスそのものを管理するオブジェクトの作成
// メインカメラの管理オブジェクトの作成
var mainCamera: AVCaptureDevice?
// インカメの管理オブジェクトの作成
var innerCamera: AVCaptureDevice?
// 現在使用しているカメラデバイスの管理オブジェクトの作成
var currentDevice: AVCaptureDevice?

// デバイスの設定
func setupDevice() {
    // カメラデバイスのプロパティ設定
    let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position: AVCaptureDevice.Position.unspecified)
    // プロパティの条件を満たしたカメラデバイスの取得
    let devices = deviceDiscoverySession.devices

    for device in devices {
        if device.position == AVCaptureDevice.Position.back {
            mainCamera = device
        } else if device.position == AVCaptureDevice.Position.front {
            innerCamera = device
        }
    }
    // 起動時のカメラを設定
    currentDevice = mainCamera
}
```

# 入力・出力データの設定

デバイスの設定が終わった後にデバイスのカメラの入力を初期化します。設定したデバイスの情報を[AVCaptureDeviceInput](https://developer.apple.com/documentation/avfoundation/avcapturedeviceinput)クラスを用いて、AVCaptureSessionに追加します。
次にキャプチャーの出力データである[AVCapturePhotoOutput](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput)オブジェクトを作成します。オブジェクトを作成後、[setPreparedPhotoSettingsArray](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/2305505-setpreparedphotosettingsarray)メソッドを用いて、出力ファイルのフォーマットをjpegで指定します。
最後にAVCapturePhotoOutputオブジェクトをAVCaptureSessionに追加し、入力・出力データの設定が完了です。

```swift
// キャプチャーの出力データを受け付けるオブジェクト
var photoOutput : AVCapturePhotoOutput?

// 入出力データの設定
func setupInputOutput() {
    do {
        // 指定したデバイスを使用するために入力を初期化
        let captureDeviceInput = try AVCaptureDeviceInput(device: currentDevice!)
        // 指定した入力をセッションに追加
        captureSession.addInput(captureDeviceInput)
        // 出力データを受け取るオブジェクトの作成
        photoOutput = AVCapturePhotoOutput()
        // 出力ファイルのフォーマットを指定
        photoOutput!.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey : AVVideoCodecType.jpeg])], completionHandler: nil)
        captureSession.addOutput(photoOutput!)
    } catch {
        print(error)
    }
}
```

# カメラの取得している映像の表示

次に、カメラの表示用のレイヤーの設定を行います。LayerとViewの違いを簡単に説明すると、Viewは実際の描画や画面のイベント処理を行うオブジェクトで、LayerはViewに描画する内容を管理するオブジェクトです。
カメラの取得している映像を画面に表示するには、[AVCaptureVideoPreviewLayer](https://developer.apple.com/documentation/avfoundation/avcapturevideopreviewlayer)クラスを用います。今回は、コードから設定します。AVCaptureVideoPreviewLayerクラスは、描画コンテンツを管理しているCALayerクラスのサブクラスです。
作成するレイヤーの設定を行います。まず、指定するAVCaptureSessionオブジェクトでAVCaptureVideoPreviewLayerオブジェクトを初期化します。次にレイヤーの構成を行います。videoGravityプロパティでプレビューレイヤが、カメラからの映像をどのように表示するかを設定します。
縦横比を維持したまま表示するため、[AVLayerVideoGravity](https://developer.apple.com/documentation/avfoundation/avlayervideogravity)のresizeAspectFillを設定します。
次に、connectionプロパティのvideoOrientationプロパティで、表示するプレビューレイヤの向きを指定します。ここではカメラのキャプチャーをそのままの向きで表示するため、定数[AVCaptureVideoOrientation.portrait](https://developer.apple.com/documentation/avfoundation/avcapturevideoorientation)を指定します。
最後にレイヤのフレームにビューのフレームを設定します。
これでレイヤの設定は終了です。最後にこのAVCaptureVideoPreviewLayerオブジェクトをビューのレイヤに追加することで、カメラのキャプチャが画面に表示するように設定します。

```swift
// プレビュー表示用のレイヤ
var cameraPreviewLayer : AVCaptureVideoPreviewLayer?

// カメラのプレビューを表示するレイヤの設定
func setupPreviewLayer() {
    // 指定したAVCaptureSessionでプレビューレイヤを初期化
    self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
    // プレビューレイヤが、カメラのキャプチャーを縦横比を維持した状態で、表示するように設定
    self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
    // プレビューレイヤの表示の向きを設定
    self.cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait

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

フロー4までをライフサイクルメソッド[viewDidLoad](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621495-viewdidload)に記述し、AVCaptureSessionクラスの[startRunnnig](https://developer.apple.com/documentation/avfoundation/avcapturesession/1388185-startrunning)メソッドを起動することで、セッションの入力から出力へのデータの流れが開始され、画面にカメラのキャプチャーを表示することができます。

```swift
override func viewDidLoad() {
    super.viewDidLoad()
    setupCaptureSession()
    setupDevice()
    setupInputOutput()
    setupPreviewLayer()
    captureSession.startRunning()
}
```

# まとめ

今回の実装はここまでです。[次回](https://qiita.com/t_okkan/items/b2dd11426eab107c5d15)はシャッターボタンの作成やUIの設定、画像の撮影と保存などを行います。
[Swift4でカメラアプリを作成する(2)](https://qiita.com/t_okkan/items/b2dd11426eab107c5d15)

## 今回のソースコード

```swift
import UIKit
import AVFoundation

class ViewController: UIViewController {
    
    // デバイスからの入力と出力を管理するオブジェクトの作成
    var captureSession = AVCaptureSession()
    // カメラデバイスそのものを管理するオブジェクトの作成
    // メインカメラの管理オブジェクトの作成
    var mainCamera: AVCaptureDevice?
    // インカメの管理オブジェクトの作成
    var innerCamera: AVCaptureDevice?
    // 現在使用しているカメラデバイスの管理オブジェクトの作成
    var currentDevice: AVCaptureDevice?
    // キャプチャーの出力データを受け付けるオブジェクト
    var photoOutput : AVCapturePhotoOutput?
    // プレビュー表示用のレイヤ
    var cameraPreviewLayer : AVCaptureVideoPreviewLayer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCaptureSession()
        setupDevice()
        setupInputOutput()
        setupPreviewLayer()
        captureSession.startRunning()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

//MARK: カメラ設定メソッド
extension ViewController{
    // カメラの画質の設定
    func setupCaptureSession() {
        captureSession.sessionPreset = AVCaptureSession.Preset.photo
    }
    
    // デバイスの設定
    func setupDevice() {
        // カメラデバイスのプロパティ設定
        let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position: AVCaptureDevice.Position.unspecified)
        // プロパティの条件を満たしたカメラデバイスの取得
        let devices = deviceDiscoverySession.devices
        
        for device in devices {
            if device.position == AVCaptureDevice.Position.back {
                mainCamera = device
            } else if device.position == AVCaptureDevice.Position.front {
                innerCamera = device
            }
        }
        // 起動時のカメラを設定
        currentDevice = mainCamera
    }
    
    // 入出力データの設定
    func setupInputOutput() {
        do {
            // 指定したデバイスを使用するために入力を初期化
            let captureDeviceInput = try AVCaptureDeviceInput(device: currentDevice!)
            // 指定した入力をセッションに追加
            captureSession.addInput(captureDeviceInput)
            // 出力データを受け取るオブジェクトの作成
            photoOutput = AVCapturePhotoOutput()
            // 出力ファイルのフォーマットを指定
            photoOutput!.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey : AVVideoCodecType.jpeg])], completionHandler: nil)
            captureSession.addOutput(photoOutput!)
        } catch {
            print(error)
        }
    }
    
    // カメラのプレビューを表示するレイヤの設定
    func setupPreviewLayer() {
        // 指定したAVCaptureSessionでプレビューレイヤを初期化
        self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        // プレビューレイヤが、カメラのキャプチャーを縦横比を維持した状態で、表示するように設定
        self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
        // プレビューレイヤの表示の向きを設定
        self.cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait
        
        self.cameraPreviewLayer?.frame = view.frame
        self.view.layer.insertSublayer(self.cameraPreviewLayer!, at: 0)
    }
}
```