- iOS
- 2017-05-11 - 更新:2018-07-24
今回はシステムのカメラUIImagePickerControllerではなくAVFoundationフレームワークを
使った方法をご紹介します。
簡単に実装するならUIImagePickerControllerが楽ですが、AVFoundationのほうが柔軟なアプリの作成が可能になります。
ネット上でも様々な情報がありますが、今回の肝になるのがデバイスの向きと
撮影した写真の向きの問題です。
例えば、iPhoneのみ対応のアプリで縦画面(portrait)に完全固定であれば
そう難しくないのですが、私が作ろうとしていたのが、iPhoneでは縦固定、iPadでは横固定で、
且つNavigationControllerを使用していたため、結構頭を悩ませました。
NavigationControllerを使っていると、カメラのシーンだけ縦に固定することができないのです。
通常はViewControllerに対してshouldAutorotateをoverrideすれば特定のシーンのみ
固定することが可能なのですが、NavigationControllerを使用している場合、
ViewControllerはNavigationControllerに包括されているのでViewControllerのshouldAutorotateをoverrideしても効果がないのです。
そしてその場合はNavigationControllerのshouldAutorotateをoverrideすることになりますが、
そうしてしまうと包括されている全てのシーンに適用されてしまいます。
また、iPhoneのみ対応で縦画面固定だったとしても、適切に処理していないと、
“最初から端末を横にして”カメラを起動すると写真が横にならないという事態になってしまいます。
これは私が参考にさせて頂いた書籍でもそこまでは対応していませんでした。
今回、意識するポイントとしては2種類、それぞれ3つあります。
まず1つ目
- デバイスの向き
- プレビューの向き
- 保存時の出力の向き
これはiPhoneでは縦、iPadでは横とする場合のポイントです。
次に2つ目
- インターフェースの向き
- プレビューの向き
- レイヤーの向き
これは画面の回転に対応する場合に気を付けるポイントです。
それでは、以上を踏まえた上で先に進みましょう。
まずはiPhoneでは縦固定、iPadでは横固定の場合で作成します。
カメラ利用の許可
まずはカメラを利用するためInfo.plistにカメラの利用許可を追加します。
また、写真を撮影するということは保存するためにフォトライブラリーにアクセスしますので、
そちらも一緒に追加します。
Privacy - Camera Usage Description Privacy - Photo Library Usage Description
Valueには「写真を撮影します。」「写真を保存します。」等と設定します。
Viewの配置
StoryBoardでViewControllerにViewとButtonを配置します。
配置したViewとButtonをそれぞれ「previewView」「shutterButton」としてOutlet接続します。
@IBOutlet weak var previewView: UIView! @IBOutlet weak var shutterButton: UIButton!
shutterButtonはtakePhotoメソッドを実行するようにAction接続もしておきます。
@IBAction func takePhoto(_ sender: Any)
previewViewは画面いっぱいになるようオートレイアウトを設定。
shutterButtonには背景に画像を設定しラベルテキストは空白にします。
ここでは大きさを120×120、上(top)と左(leading)を0に設定します。
また表示を一旦非表示(hidden)にしておきます。
※位置を左上にしたのは、横位置は中心で縦位置を下のほうに設置した際、
Position is ambiguousと警告が出るためであることと、どうせ後で位置変更するためです。
コーディング
UIの作成が出来ましたので、後はコーディングをしていきます。
ある程度ブロックに分けて書いていきます。
まずはAVFoundationのインポートを忘れずに
import AVFoundation
必要な変数等の宣言
// インスタンスの作成 var session = AVCaptureSession() var photoOutput = AVCapturePhotoOutput() //出力先 // 通知センターを作る let notification = NotificationCenter.default // プライバシーと入出力のステータス var authStatus:AuthorizedStatus = .authorized var inOutStatus:InputOutputStatus = .ready // 認証のステータス enum AuthorizedStatus { case authorized case notAuthorized case failed } // 入出力のステータス enum InputOutputStatus { case ready case notReady case failed }
ビューが表示された直後に実行される処理を記述
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // セッション実行中なら中断 if session.isRunning { return } // カメラのプライバシー認証確認 cameraAuth() // 入出力の設定 setupInputOutput() // カメラの準備ができているかどうか if (authStatus == .authorized)&&(inOutStatus == .ready) { // プレビューレイヤの設定 setPreviewLayer() // セッション開始 session.startRunning() shutterButton.isEnabled = true } else { // カメラの利用が許可されていない時(初回起動時は無効) shutterButton.isEnabled = false } // デバイスが回転したときに通知するイベントハンドラを設定する UIDevice.current.beginGeneratingDeviceOrientationNotifications() notification.addObserver(self, selector: #selector(self.changedDeviceOrientation(_:)), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil) }
シャッターボタンで実行する処理
@IBAction func takePhoto(_ sender: Any) { let captureSetting = AVCapturePhotoSettings() captureSetting.flashMode = .auto captureSetting.isAutoStillImageStabilizationEnabled = true captureSetting.isHighResolutionPhotoEnabled = false // キャプチャのイメージ処理はデリゲートに任せる photoOutput.capturePhoto(with: captureSetting, delegate: self) }
カメラのプライバシー認証画面
func cameraAuth() { let status = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo) switch status { case .notDetermined: // 初回起動時 AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo, completionHandler: { [unowned self] authorized in print("初回", authorized.description) if authorized { self.authStatus = .authorized } else { self.authStatus = .notAuthorized }}) case .restricted, .denied: authStatus = .notAuthorized case .authorized: authStatus = .authorized } }
入出力の設定
func setupInputOutput() { // 解像度の設定 session.sessionPreset = AVCaptureSessionPresetPhoto // 入出力 do { // デバイスの取得 let device = AVCaptureDevice.defaultDevice( withDeviceType: AVCaptureDeviceType.builtInWideAngleCamera, mediaType: AVMediaTypeVideo, position: .back) // 入力元 let input = try AVCaptureDeviceInput(device: device) if session.canAddInput(input) { session.addInput(input) } else { inOutStatus = .notReady print("セッションに入力を追加できませんでした。") return } } catch let error as NSError { inOutStatus = .notReady print("カメラがありません \(error)") return } // 出力先 if session.canAddOutput(photoOutput) { session.addOutput(photoOutput) } else { inOutStatus = .notReady print("セッションに出力を追加できませんでした。") return } }
プレビューレイヤの設定
func setPreviewLayer() { // プレビューレイヤを作る let previewLayer = AVCaptureVideoPreviewLayer(session: session) guard let videoLayer = previewLayer else { print("プレビューレイヤを作成できませんでした。") shutterButton.isEnabled = false return } videoLayer.frame = view.bounds videoLayer.masksToBounds = true videoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill // iPhone/iPadによってボタン配置位置を設定 switch UIDevice.current.model { case "iPhone": portraitShutterButton() case "iPad": landscapeShutterButton() default: portraitShutterButton() } // レイヤーの向きを設定 if UIDevice.current.model == "iPhone" { videoLayer.connection.videoOrientation = .portrait } else { videoLayer.connection.videoOrientation = .landscapeRight } // 出力の向きを設定 if let photoOutputConnection = self.photoOutput.connection(withMediaType: AVMediaTypeVideo) { switch UIDevice.current.orientation { case .portrait: photoOutputConnection.videoOrientation = .portrait case .portraitUpsideDown: photoOutputConnection.videoOrientation = .portraitUpsideDown case .landscapeLeft: photoOutputConnection.videoOrientation = .landscapeRight case .landscapeRight: photoOutputConnection.videoOrientation = .landscapeLeft default: break } } // previewViewに追加する previewView.layer.addSublayer(videoLayer) }
デバイスの向きが変わったときに呼び出すメソッド
func changedDeviceOrientation(_ notification :Notification) { // photoOutput.connectionの回転向きをデバイスと合わせる if let photoOutputConnection = self.photoOutput.connection(withMediaType: AVMediaTypeVideo) { switch UIDevice.current.orientation { case .portrait: photoOutputConnection.videoOrientation = .portrait case .portraitUpsideDown: photoOutputConnection.videoOrientation = .portraitUpsideDown case .landscapeLeft: photoOutputConnection.videoOrientation = .landscapeRight case .landscapeRight: photoOutputConnection.videoOrientation = .landscapeLeft default: break } } }
iPhone用のshutterButton位置設定
func portraitShutterButton() { // Constraintsを一度削除する for constraint in self.view.constraints { let firstItem: UIButton? = constraint.firstItem as? UIButton if firstItem == self.shutterButton { self.view.removeConstraint(constraint) } } // Constraintsを追加 self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.centerX, relatedBy: NSLayoutRelation.equal, toItem: shutterButton, attribute: NSLayoutAttribute.centerX, multiplier: 1.0, constant: 0.0)) self.view.addConstraint(NSLayoutConstraint(item: self.bottomLayoutGuide, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: shutterButton, attribute: NSLayoutAttribute.bottom, multiplier: 1.0, constant: 20.0)) shutterButton.isHidden = false }
iPad用のshutterButton位置設定
func landscapeShutterButton() { // Constraintsを一度削除する for constraint in self.view.constraints { let firstItem: UIButton? = constraint.firstItem as? UIButton if firstItem == self.shutterButton { self.view.removeConstraint(constraint) } } // Constraintsを追加 self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.centerY, relatedBy: NSLayoutRelation.equal, toItem: shutterButton, attribute: NSLayoutAttribute.centerY, multiplier: 1.0, constant: 0.0)) self.view.addConstraint(NSLayoutConstraint(item: self.view, attribute: NSLayoutAttribute.trailing, relatedBy: NSLayoutRelation.equal, toItem: shutterButton, attribute: NSLayoutAttribute.trailing, multiplier: 1.0, constant: 20.0)) shutterButton.isHidden = false }
後処理として不要になった通知を終了させます。
override func viewDidDisappear(_ animated: Bool) { UIDevice.current.endGeneratingDeviceOrientationNotifications() }
ステータスバーを非表示にしておきます。
override var prefersStatusBarHidden: Bool { return true }
撮影した画像を保存するキャプチャー処理部分はextensionで別ファイルに記述します。
import Photos // デリゲート部分を拡張する extension CameraViewController:AVCapturePhotoCaptureDelegate { // 映像をキャプチャする func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?, previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) { // バッファからjpegデータを取り出す let photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation( forJPEGSampleBuffer: photoSampleBuffer!, previewPhotoSampleBuffer: previewPhotoSampleBuffer) // photoDataがnilでないときUIImageに変換する if let data = photoData { if let stillImage = UIImage(data: data) { // アルバムに追加する UIImageWriteToSavedPhotosAlbum(stillImage, self, nil, nil) } } } }
画面の回転に対応する
オートレイアウトだとうまくいかないので、直接配置する方法を取っています。
シャッターボタン位置が常に同じ場所に来るよう配置しています。
プレビューレイヤの設定
func setPreviewLayer() { // プレビューレイヤを作る let previewLayer = AVCaptureVideoPreviewLayer(session: session) guard let videoLayer = previewLayer else { print("プレビューレイヤを作成できませんでした。") shutterButton.isEnabled = false return } videoLayer.frame = view.bounds videoLayer.masksToBounds = true videoLayer.videoGravity = AVLayerVideoGravityResizeAspectFill switch UIDevice.current.orientation { case .portrait: videoLayer.connection.videoOrientation = .portrait portraitShutterButton() case .portraitUpsideDown: videoLayer.connection.videoOrientation = .portraitUpsideDown portraitUpsideDownShutterButton() case .landscapeLeft: videoLayer.connection.videoOrientation = .landscapeRight landscapeLeftShutterButton() case .landscapeRight: videoLayer.connection.videoOrientation = .landscapeLeft landscapeRightShutterButton() default: print("unknown") } if let photoOutputConnection = self.photoOutput.connection(withMediaType: AVMediaTypeVideo) { switch UIDevice.current.orientation { case .portrait: photoOutputConnection.videoOrientation = .portrait case .portraitUpsideDown: photoOutputConnection.videoOrientation = .portraitUpsideDown break case .landscapeLeft: photoOutputConnection.videoOrientation = .landscapeRight case .landscapeRight: photoOutputConnection.videoOrientation = .landscapeLeft default: break } } // previewViewに追加する previewView.layer.addSublayer(videoLayer) }
デバイスの向きが変わったときに呼び出すメソッドを書き換えます。
func changedDeviceOrientation(_ notification :Notification) { let videoLayer: AVCaptureVideoPreviewLayer = previewView.layer.sublayers?[0] as! AVCaptureVideoPreviewLayer videoLayer.frame = view.bounds switch UIDevice.current.orientation { case .portrait: videoLayer.connection.videoOrientation = .portrait portraitShutterButton() case .portraitUpsideDown: videoLayer.connection.videoOrientation = .portraitUpsideDown portraitUpsideDownShutterButton() case .landscapeLeft: videoLayer.connection.videoOrientation = .landscapeRight landscapeLeftShutterButton() case .landscapeRight: videoLayer.connection.videoOrientation = .landscapeLeft landscapeRightShutterButton() default: print("unknown") } // photoOutput.connectionの回転向きをデバイスと合わせる if let photoOutputConnection = self.photoOutput.connection(withMediaType: AVMediaTypeVideo) { switch UIDevice.current.orientation { case .portrait: photoOutputConnection.videoOrientation = .portrait case .portraitUpsideDown: photoOutputConnection.videoOrientation = .portraitUpsideDown break case .landscapeLeft: photoOutputConnection.videoOrientation = .landscapeRight case .landscapeRight: photoOutputConnection.videoOrientation = .landscapeLeft default: break } } }
ボタンの配置を行うメソッド
func portraitShutterButton() { shutterButton.center.x = self.view.center.x shutterButton.frame.origin.y = self.view.frame.height - shutterButton.frame.height - 20 shutterButton.isHidden = false } func landscapeLeftShutterButton() { shutterButton.frame.origin.x = self.view.frame.size.width - shutterButton.frame.size.width - 20 shutterButton.center.y = self.view.center.y shutterButton.isHidden = false } func landscapeRightShutterButton() { shutterButton.frame.origin.x = 20 shutterButton.center.y = self.view.center.y shutterButton.isHidden = false } func portraitUpsideDownShutterButton() { shutterButton.center.x = self.view.center.x shutterButton.frame.origin.y = 20 shutterButton.isHidden = false }
Xcode: 8.3.2
Swift: 3.1
OS: Sierra 10.12