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

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

前回の続きです。今回はカメラの撮影ボタンの設定から撮影したデータの保存までを行います。
前回の記事はこちらから。
[Swift4でカメラアプリを作成する(1)](https://qiita.com/t_okkan/items/f2ba9b7009b49fc2e30a)

# カメラアプリのフロー

カメラアプリの処理フローの確認です。

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

今回は5~8までを説明します。今回はカメラなどへのアクセスの許可やUIの設定、画像の撮影と保存を実装します。

# カメラと写真の利用許可

一旦コードから離れて、プロジェクトの設定に移ります。カメラと写真のライブラリーを使用するため、アクセスの許可をする必要があります。プロジェクトのinfo.plistを開いて、アプリのアクセス許可をもらう処理を追加します。

<img width="1440" alt="img4.png" src="https://qiita-image-store.s3.amazonaws.com/0/239179/f3238aa4-4515-e4d5-3b45-aef5252595a8.png">

一番上のInformationPropertyListの右側の＋を押して
- NSCameraUsageDescription(カメラを使用することの許可を求める)
- NSPhotoLibraryAddUsageDescription(写真ライブラリへのアクセスの許可を求める)

の2つを設定します。右側のValueに許可を求めるダイアログに表示する文言を設定できます。

# UIの設定

次に、カメラアプリのUIを作成します。今回は画面にカメラの表示用レイヤーと、シャッター用のボタンを配置します。
まずはボタンからです。ボタンはMain.storyboardで作成します。Main.storyboradに移動し右ペインの下側にあるFilterに「Button」と入力します。検索結果で出てきた「Button」をドラックし、画面に配置します。ボタンのテキストを消し、横と縦の大きさをそれぞれ60に設定します。

<img width="1440" alt="img1.png" src="https://qiita-image-store.s3.amazonaws.com/0/239179/48593249-2c7e-ef28-fc7b-a0717dc1c5b7.png">

次に、画面のViewControllerを選択し、Xcodeのツールバーの「Assistant Editor」をクリックします。すると画面に紐づいたViewControllerのコードが表示されます。Main.storyboardでcontrolキーを押しながらButtonを選択し、コードにドラッグ&ドロップします。
「connection」はOutletを選択し、「Name」にcameraButtonを指定して、Connectを押します。さらにボタンの背景色を白色にします。これでボタンの設定は終わりです。

<img width="1440" alt="img2.png" src="https://qiita-image-store.s3.amazonaws.com/0/239179/fcf35a3f-9abe-f0c2-0891-978fa4ce160d.png">

今回は、ボタンのデザインを作成するために、以下の関数を用意しました。他の関数と同様に、ライフサイクルメソッドviewDidLoadで呼び出します。

```swift
// ボタンのスタイルを設定
func styleCaptureButton() {
    cameraButton.layer.borderColor = UIColor.white.cgColor
    cameraButton.layer.borderWidth = 5
    cameraButton.clipsToBounds = true
    cameraButton.layer.cornerRadius = min(cameraButton.frame.width, cameraButton.frame.height) / 2
}
```

# 撮影ボタン押下時のアクションの設定

カメラのシャッターボタンが押された時のイベントを実装します。まずはstoryboardに移動し、作成したシャッター用のボタンをcontrolキーを押しながらドラッグします。
ConnectionをActionにし、Nameを「cameraButton_TouchUpInside」とします。

<img width="1440" alt="img3.png" src="https://qiita-image-store.s3.amazonaws.com/0/239179/4bffadcf-3aff-4be1-a16f-e8f40993e410.png">

この関数の中では[AVCapturePhotoSettings](https://developer.apple.com/documentation/avfoundation/avcapturephotosettings)クラスで、撮影する際のフラッシュや手ぶれ補正などをおこなうかなどの設定をします。最後にAVCapturePhotoOutputクラスの[capturePhoto](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/1648765-capturephoto)メソッドで指定した設定で写真の撮影を開始します。

```swift
// シャッターボタンが押された時のアクション
@IBAction func cameraButton_TouchUpInside(_ sender: Any) {
    let settings = AVCapturePhotoSettings()
    // フラッシュの設定
    settings.flashMode = .auto
    // カメラの手ぶれ補正
    settings.isAutoStillImageStabilizationEnabled = true
    // 撮影された画像をdelegateメソッドで処理
    self.photoOutput?.capturePhoto(with: settings, delegate: self as! AVCapturePhotoCaptureDelegate)
}
```

# 撮影した画像の保存

AVCapturePhotoOutputクラスの[capturePhoto](https://developer.apple.com/documentation/avfoundation/avcapturephotooutput/1648765-capturephoto)メソッドで撮影した画像データを受け取るために、[AVCapturePhotoCaptureDelegate](https://developer.apple.com/documentation/avfoundation/avcapturephotocapturedelegate)の[photoOutput](https://developer.apple.com/documentation/avfoundation/avcapturephotocapturedelegate/2873949-photooutput)メソッドのなかで撮影した画像データを取得します。
撮影された画像データは、パラメータのphotoにピクセルデータとメタデータなどの関連データと共に格納されています。[fileDataRepresentation](https://developer.apple.com/documentation/avfoundation/avcapturephoto/2873919-filedatarepresentation)メソッドでデータを生成し、UIImageオブジェクトに変換します。
最後に[UIImageWriteToSavedPhotosAlbum](https://developer.apple.com/documentation/uikit/1619125-uiimagewritetosavedphotosalbum)で画像を写真ライブラリに保存して、カメラアプリの完成です。

```swift
 extension ViewController: AVCapturePhotoCaptureDelegate{
    // 撮影した画像データが生成されたときに呼び出されるデリゲートメソッド
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let imageData = photo.fileDataRepresentation() {
            // Data型をUIImageオブジェクトに変換
            let uiImage = UIImage(data: imageData)
            // 写真ライブラリに画像を保存
            UIImageWriteToSavedPhotosAlbum(uiImage!, nil,nil,nil)
        }
    }
}
```

# 動作確認

作成したアプリを、ビルドし実機にインストールして起動します。起動すると、カメラと写真ライブラリへのアクセス許可を求められた後に、写真の撮影ができます。
カメラボタンを押すと、シャッター音が鳴り画像を撮影できます。撮影後にカメラロールに移動すると、画像を撮影できていることが確認できます。

# コード

作成したアプリのコードは以下のようになります。

```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?
    // シャッターボタン
    @IBOutlet weak var cameraButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupCaptureSession()
        setupDevice()
        setupInputOutput()
        setupPreviewLayer()
        captureSession.startRunning()
        styleCaptureButton()
    }

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

    // シャッターボタンが押された時のアクション
    @IBAction func cameraButton_TouchUpInside(_ sender: Any) {
        let settings = AVCapturePhotoSettings()
        // フラッシュの設定
        settings.flashMode = .auto
        // カメラの手ぶれ補正
        settings.isAutoStillImageStabilizationEnabled = true
        // 撮影された画像をdelegateメソッドで処理
        self.photoOutput?.capturePhoto(with: settings, delegate: self as! AVCapturePhotoCaptureDelegate)
    }
    
}

//MARK: AVCapturePhotoCaptureDelegateデリゲートメソッド
extension ViewController: AVCapturePhotoCaptureDelegate{
    // 撮影した画像データが生成されたときに呼び出されるデリゲートメソッド
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let imageData = photo.fileDataRepresentation() {
            // Data型をUIImageオブジェクトに変換
            let uiImage = UIImage(data: imageData)
            // 写真ライブラリに画像を保存
            UIImageWriteToSavedPhotosAlbum(uiImage!, nil,nil,nil)
        }
    }
}

//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)
    }
    
    // ボタンのスタイルを設定
    func styleCaptureButton() {
        cameraButton.layer.borderColor = UIColor.white.cgColor
        cameraButton.layer.borderWidth = 5
        cameraButton.clipsToBounds = true
        cameraButton.layer.cornerRadius = min(cameraButton.frame.width, cameraButton.frame.height) / 2
    }
}
```