カメラロールへのアクセス許諾を実装

## はじめに
はじめまして。iOSエンジニアであれば一度は通るカメラロールへのアクセス許諾。プロフィール画像の設定など目的別に様々な場面で実装されることが多いです。とはいえ、僕は毎回実装のたびにググっているような気がしているので、今回は自戒も含めて備忘録的に残しておこうと思います。

## 開発環境
- XCode10.1
- Swift4.2
- macOS Mojave 10.14.3

## iOSのカメラロールにおける特性
カメラロールにアクセスする場合は、必ずユーザからアクセス許可を受け取ることがOSの仕様になっています。そのため、カメラロールにアクセスする場合には必ずユーザの許可を得る必要があり(２回目)、一度でもユーザに 「拒否」されてしまった場合は、ユーザに再度カメラロールへのアクセス許可を依頼し、「設定アプリ」から許可してもらう必要があります。


## PHPhotoLibrary
さて、では本題に入っていきましょう。
カメラロールの許諾を行う場合、 `Photos` を importし、  `PHPhotoLibrary` にアクセスしてあげる必要があります。
実際に `PHPhotoLibrary` を開いてみると、下記のように書かれてあります。

```swift

/*!
 @class        PHPhotoLibrary
 @abstract     A PHPhotoLibrary provides access to the metadata and image data for the photos, videos and related content in the user's photo library, including content from the Camera Roll, iCloud Shared, Photo Stream, imported, and synced from iTunes.
 @discussion   ...
 */
@available(iOS 8.0, *)
open class PHPhotoLibrary : NSObject {
    open class func shared() -> PHPhotoLibrary
    open class func authorizationStatus() -> PHAuthorizationStatus
    open class func requestAuthorization(_ handler: @escaping (PHAuthorizationStatus) -> Void)

    // handlers are invoked on an arbitrary serial queue
    // Nesting change requests will throw an exception
    open func performChanges(_ changeBlock: @escaping () -> Void, completionHandler: ((Bool, Error?) -> Void)? = nil)
    open func performChangesAndWait(_ changeBlock: @escaping () -> Void) throws
    open func register(_ observer: PHPhotoLibraryChangeObserver)
    open func unregisterChangeObserver(_ observer: PHPhotoLibraryChangeObserver)
}

```


この中の `authorizationStatus` というクラスメソッドを呼ぶと、ユーザのカメラロールへのアクセス状況のステータスを返してくれます。
そして、最終的にはステータス毎にユーザに対して正しい挙動を返してあげる必要があります。


僕の場合は以下のようにRxSwiftとRxCocoaを使っているので、アクセス許諾するViewControllerに対して1:1でViewModelを実装し、ボタンなどのタップをRxCocoaのイベントとして検知して、その中でカメラロールへのアクセス許諾のメソッドを呼ぶようにしています。

#### ViewController

```swift
class ViewController: UIViewController {
    
    // このボタンをタップした時にカメラロールへのアクセスの関数を呼ぶ
    @IBOutlet weak private var accessToPhotoButton: UIButton!
    
    // viewModelの初期化
    private let viewModel: ViewModel = ViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bind()
        viewModel
        .photoAuthorizedResult
        .subscribe(onNext: {[weak self] photoAuthorizedResult in
            switch photoAuthorizedResult {
            case .success:
                // カメラロールへのアクセス許可が通った場合の処理
            case .error(_):
                // カメラロールへのアクセスが拒否された場合
                self?.callToSettingApp()
            }
        }).disposed(by: disposeBag)
    }
    
    private func callToSettingApp() {
        if let url = URL(string: UIApplication.openSettingsURLString) {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        }
    }
    
    private func bind() {
        accessToPhotoButton
           .rx
           .tap
           .asDriver()
           .drive(onNext: {[weak self] _ in
            self?.viewModel.acccessToPhoto()
        }).disposed(by: disposeBag)
    }
}

```
ちなみに僕は最初の時に結構混乱したのですが、successで、何かをしなければならない訳ではありません。
successに入った時点ですでにアクセス許可は出ているので、アクセス許諾専用の画面を表示していれば、閉じるなどの処理を書いています。


次にエラー時です。エラーのステータスは2つ存在するので、より細かくハンドリングしたい場合には下記のような形でもいいかもしれません。
勿論switchがネストするのは可読性の観点から良くないので、 `PhotoAuthorizedErrorType` の引数を持つ関数を別で作った方が良いと思います。 
基本的にはsuccessとerrorの2つがあれば問題ないと思います。

```swift
  case .error(let errorType):
  // カメラロールへのアクセスが拒否された場合 
       switch errorType {
       case .restricted: ...
       case .denied: ...
       }
  }
```

#### ViewModel
```swift

import Foundation
import RxSwift
import RxCocoa
import Photos

enum PhotoAuthorizedResult {
    case success
    case error(PhotoAuthorizedErrorType)
}

enum PhotoAuthorizedErrorType {
    case restricted
    case denied
}

class ViewModel {
    private(set) var photoAuthorizedResult = PublishRelay<PhotoAuthorizedResult>()
    
    private func acccessToPhoto() {
        switch .authorizationStatus() {
        // 未設定(初回表示)
        case .notDetermined:
            PHPhotoLibrary.requestAuthorization() { [weak self] status in
                if status == PHAuthorizationStatus.authorized {
                    self?.photoAuthorizedResult.accept(.success)
                    return
                }
                self?.photoAuthorizedResult.accept(.error(.denied))
            }
        // ParentalControlなどで制限されていて、ユーザーがアプリのアクセス許可を変更できない
        case .restricted:
            self.photoAuthorizedResult.accept(.error(.restricted))
        // カメラへのアクセスが拒否されている
        case .denied:
            self.photoAuthorizedResult.accept(.error(.denied))
        // カメラへのアクセスが許可されている
        case .authorized:
            self.photoAuthorizedResult.accept(.success)
        }
    }
}
```

ViewModelが持つインスタンス変数である `photoAuthorizedResult` に対してacceptすることで、ViewControllerはこのインスタンス変数をsubscribeして、ステータスに見合ったAlertControllerを表示するなどの処理が可能になります。


#### 全体の流れと注意点
- ViewControllerでボタンタップなどUIの処理を取得
- ViewControllerの状態を持つのはViewModelの役割なので、ステータス周りのロジックはViewModelにかく
- 端末のステータスは常に一種類しか存在しないので、ViewModelのインスタンス変数は一つで良い
- あくまでViewContollerはViewModelのインスタンス変数を観測し、その変数の持つステータスに見合った画面を表示する事を徹底する


## 終わりに
ご視聴ありがとうございました