炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

Swift2 - カメラ、写真のアクセス許可の確認をプロトコルを利用して使いまわす


Swift2.2, Xcode7

カメラ( Camera )や写真(フォトライブラリー)のアクセス許可の設定状況の確認は、複数の ViewController で行いたいケースが多いので、プロトコルで確認するメソッドを定義して、プロトコルを採用した ViewController であれば簡単にアクセス許可の設定状況の確認を行えるように実装しました。

  1. アクセス許可の各種ステータスを知っておく
  2. プライバシーの変更はアプリが強制的に終了されることを知っておく
  3. アクセス許可の状況を確認するプロトコルを実装する
  4. ViewController でプロトコルのメソッドを呼び出す
  5. Alert表示用の Utility を作成する

1. アクセス許可の各種ステータスを知っておく

カメラや写真のアクセス許可のステータスは 4 つあります。

  1. Authorized -> 許可済み。カメラや写真が利用できる。
  2. Denied -> 拒否した。カメラや写真が利用できない
  3. Restricted -> 機能が制限されている?このステータスは利用したことありません。
  4. NotDetermined -> 許可も拒否もしていない状態。この状態の時にカメラや写真を利用しようとすると Apple が準備してくれているアクセス許可のアラートが自動で表示されます。

アプリをインストールして初めて写真やカメラを利用した時のステータスは NotDetermined です。NotDetermined の時に自動で表示されるアラートで、許可した場合は Authorized になります。 NotDetermined の時に自動で表示されるアラートで、許可しなかった場合は Denied となります。注意する点としては NotDetermined の時に表示されるアラートでユーザーが許可を行わなかった場合、ステータスは Denied となり、以後は自前で「許可してね」のアラートを表示してあげないと、カメラは起動するけど撮影ができなかったり、フォトライブラリーの modal は開くけど、写真にアクセスできないといった状況になります。

2. プライバシーの変更はアプリが強制的に終了されることを知っておく

もう一つ、写真やカメラのプライバシーに関する変更を促す際の注意すべき点です。ステータスが Denied であることを確認して、「許可してね」のアラートを出して設定画面に飛ばしてあげることは簡単に行えますが、ユーザーが写真やカメラの利用の ON / OFF を切り替えた瞬間に、バックグラウンドで起動しているアプリは強制終了させられます。これはクラッシュとは別で、SIGKILL の警告が出力されてアプリが終了する形になるのですが、この背景としては、プライバシーに関する変更を行った場合は、変更を確実にアプリに反映させる為の意図があるんだと思います。この件に関しては以下の記事に詳しいことが書かれていますので、ここではプライバシーに関する項目を変更した場合は、アプリが強制終了させられるので、それを把握した上でアプリの設計を行う必要があると覚えておけば大丈夫だと思います。

http://stackoverflow.com/questions/12522574/toggling-privacy-settings-will-kill-the-app

3. アクセス許可の状況を確認するプロトコルを実装する

複数の ViewController で使いまわしできるように protocol での実装をします。今回はカメラ用の AVCaptureDeviceAuthorizationProtocol と、写真(フォトライブラリー)用の ALAssetsLibraryAuthorizationProtocol を作成するようにしました。

  • AVCaptureDeviceAuthorizationProtocol.swift
import AVFoundation

protocol AVCaptureDeviceAuthorizationProtocol {
    // カメラを起動する
    func launchCamere(successHandler:(Void -> Void), viewController: UIViewController)
}

extension AVCaptureDeviceAuthorizationProtocol {
    func launchCamere(successHandler:(Void -> Void), viewController: UIViewController) {
        let status = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo)
        switch status {
        case .Authorized:
            successHandler()
        case .Denied:
            self.showCamereAlert(viewController)
        case .Restricted:
            break
        case .NotDetermined:
            AVCaptureDevice.requestAccessForMediaType(AVMediaTypeVideo) { (isGranted: Bool) -> () in
                if isGranted {
                    successHandler()
                }
            }
        }
    }
    
    private func showCamereAlert(viewController: UIViewController) {
        let okButtonHandler = { (action: UIAlertAction) -> () in
            if let url = NSURL(string: UIApplicationOpenSettingsURLString) {
                UIApplication.sharedApplication().openURL(url)
            }
        }
        // AlertUtility は独自に作成したアラート表示用の便利クラスです。クラスの内容については後述しています。
        AlertUtility.showNoticeAlert("アクセス許可設定", message: "カメラへのアクセスを許可してください", okButtonTitle: "設定する", okButtonHandler: okButtonHandler, cancelButtonTitle: "キャンセル", cancelButtonHandler: nil, viewController: viewController)
    }
}
  • ALAssetsLibraryAuthorizationProtocol.swift
import AssetsLibrary

protocol ALAssetsLibraryAuthorizationProtocol {
    // フォトライブラリーを起動する
    func launchPhotoLibrary(successHandler:(Void -> Void), viewController: UIViewController)
}

extension ALAssetsLibraryAuthorizationProtocol {
    func launchPhotoLibrary(successHandler:(Void -> Void), viewController: UIViewController) {
        let status = ALAssetsLibrary.authorizationStatus()
        switch status {
        case .Authorized:
            successHandler()
        case .Denied:
            self.showPhotoLibraryAlert(viewController)
        case .Restricted:
            break
        case .NotDetermined:
            ALAssetsLibrary().enumerateGroupsWithTypes(ALAssetsGroupAll, usingBlock: { (_, _) -> () in
                successHandler()
            }, failureBlock: { (error: NSError?) -> () in
                // ※1
                AlertUtility.showErrorAlert(error, viewController: viewController)
            })
        }
    }
    
    private func showPhotoLibraryAlert(viewController: UIViewController) {
        let okButtonHandler = { (action: UIAlertAction) -> () in
            if let url = NSURL(string: UIApplicationOpenSettingsURLString) {
                UIApplication.sharedApplication().openURL(url)
            }
        }
        AlertUtility.showNoticeAlert("アクセス許可設定", message: "写真へのアクセスを許可してください", okButtonTitle: "設定する", okButtonHandler: okButtonHandler, cancelButtonTitle: "キャンセル", cancelButtonHandler: nil, viewController: viewController)
    }
}

4. ViewController でプロトコルのメソッドを呼び出す

「3」で作成したプロトコルを ViewController で採用して、カメラと写真の起動を試してみます。ViewController 側のコードは許可されていた場合の処理を successHandler に定義しておいて、引数で渡してあげるだけなので、コードはかなりスッキリするかと思います。

  • ViewController.swift
class ViewController: UIViewController, AVCaptureDeviceAuthorizationProtocol, ALAssetsLibraryAuthorizationProtocol {
    
    ... 省略

    func cameraButtonTapped() {
            let successHandler = {
                let imagePicker = UIImagePickerController()
                imagePicker.delegate = self
                imagePicker.sourceType = .Camera
                self.presentViewController(imagePicker, animated: true, completion: nil)
            }
            self.launchCamere(successHandler, viewController: self)
    }

    func photoLibraryTapped() {
            let successHandler = {
                let imagePicker = UIImagePickerController()
                imagePicker.delegate = self
                imagePicker.sourceType = .PhotoLibrary
                self.presentViewController(imagePicker, animated: true, completion: nil)
            }
            self.launchPhotoLibrary(successHandler, viewController: self)
    }
}

5. Alert表示用の Utility を作成する

写真やカメラの対応とはほとんど関係ないですが、アラート表示用の Utility も紹介しておきます。

  • AlertUtility.swift
struct AlertUtility
{
    /**
     アラートを表示する
     - parameter title タイトル
     - parameter message メッセージ
     - parameter okButtonTitle OKボタンの表示タイトル
     - parameter okHandler OKボタン押下後の処理ハンドラ
     - parameter cancelButtonTitle キャンセルボタンの表示タイトル
     - parameter cancelHandler キャンセルボタン押下後の処理ハンドラ
     - parameter viewController:アラートを表示するViewController
     */
    static private func showAlert(
        title: String,
        message: String?,
        okButtonTitle: String,
        okButtonHandler: ((UIAlertAction) -> Void)?,
        cancelButtonTitle: String? = nil,
        cancelButtonHandler: ((UIAlertAction) -> Void)? = nil,
        viewController: UIViewController) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.Alert)
        let okButtonAction = UIAlertAction(title: okButtonTitle, style: .Default, handler: okButtonHandler)
        alert.addAction(okButtonAction)
        if let buttonTitle = cancelButtonTitle {
            let cancelButtonAction = UIAlertAction(title: buttonTitle, style: .Cancel, handler: cancelButtonHandler)
            alert.addAction(cancelButtonAction)
        }
        viewController.presentViewController(alert, animated: true, completion: nil)
    }

    /**
     エラーアラートを表示する
     - parameter message:エラーメッセージ
     - parameter okButtonTitle:OKボタンの表示タイトル
     - parameter okHandler:OKボタン押下後の処理ハンドラ
     - parameter viewController:アラートを表示するViewController
     */
    static func showErrorAlert(
        message: String,
        okButtonTitle: String = NSLocalizedString("okButtonTitle", comment: "okButtonTitle"),
        okButtonHandler: ((UIAlertAction) -> Void)? = nil,
        viewController: UIViewController) {
        self.showAlert(
            "エラー",
            message: message,
            okButtonTitle: okButtonTitle,
            okButtonHandler: okButtonHandler,
            viewController: viewController)
    }

    static func showErrorAlert(error: NSError?, viewController: UIViewController){
        guard let error = error else {
            return
        }
        let title: String = error.localizedDescription
        let message: String = error.localizedRecoverySuggestion ?? "接続環境の良いところで再度お試しください"
        self.showErrorAlert(title, message: message, viewController: viewController)
    }
}