【Swift/AVFoundation】アプリ内からカメラを起動し写真撮影をする方法
この記事からわかること
- Swift/UIKitでカメラアプリの実装方法
- AVFoundationの使い方や特徴、メリット
- カスタマイズできるカメラビューの作り方
- AVCaptureSessionとは?
- AVCaptureDeviceInputとAVCapturePhotoOutputとは?
- UIImagePickerControllerとの違い
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Xcode:15.0.1
- iOS:17.0
- Swift:5.9
- macOS:Sonoma 14.1
AVFoundationとは?
公式リファレンス:AVFoundation Framework
AVFoundationとはAppleプラットフォームにおける画像や動画、音声などのオーディオビジュアル(AV)メディアの再生や操作、デバイスカメラの制御といった機能を包括するフレームワークです。
AVFoundationを活用することでアプリ内から写真や動画の撮影、再生、編集、検査、再生だけでなく、派生してバーコードの読み取りなども可能になります。
おすすめ記事:【Swift/AVFoundation】バーコード読取機能の作り方!JANやISBNコード
カメラを起動して写真を撮影する方法
Swiftを使って自作したアプリ内からカメラを起動して写真を撮影する方法は2つ存在します。
方法1:UIImagePickerControllerクラス
1つ目はUIImagePickerController
クラスを使用した方法です。フレームワークなどを導入せずに実装でき、コードもシンプルなので実装の手軽さでは圧倒的に軍配が上がります。
またiOSの「カメラ」アプリのようなUIをそのまま使用できるのもメリットの1つです。
おすすめ記事:【Swift/UIImagePickerController】アプリ内からカメラで写真を撮影する方法!
方法2:AVFoundation
2つ目はAVFoundation
を使用した方法です。実装するのはなかなか手間がかかりますがカスタマイズ性が高く柔軟なカメラアプリを開発することができます。
撮影ボタンの位置やカメラビューのサイズなどを好きなように配置できるので独自のカメラアプリを作成するのにおすすめです。
AVCaptureSession
AVFoundationを使用して写真撮影を行うためにはAVCaptureSession
クラスが重要になってきます。まずはこの取り扱いを整理しておきます。
AVFoundationでは明示的にinputsとoutputsを指定する必要があり、それを管理するためのクラスが用意されています。
- inputs:入力ソースのこと。カメラやマイクなどデバイスからデータを取得する方法
- outputs:出力形式のこと。入力されたデータをどのように出力するか
inputsを管理しているクラスがAVCaptureDeviceInput
、outputsを管理しているクラスがAVCaptureOutput
になります。
そしてその2つの仲介役を行なっているのがAVCaptureSession
クラスになります。
AVCaptureSession実装の流れ
- AVCaptureDeviceインスタンスを生成
- AVCaptureDeviceインスタンスからVCaptureDeviceInputを構築
- AVCaptureSessionにAVCaptureDeviceInputを登録
- AVCaptureSessionにAVCaptureOutputを登録
- AVCaptureVideoPreviewLayerで画面を構築
1.AVCaptureDeviceInputの構築
AVCaptureDeviceInputインスタンスを構築するためにはAVCaptureDevice
クラスを用いてまず使用するデバイスを設定する必要があります。
使用するAVCaptureDeviceインスタンスを生成するには以下の2つのどちらかを使用します。
- AVCaptureDevice.defaultメソッド
- AVCaptureDevice.DiscoverySessionクラス
後者を使用することでカメラの種類や位置などを柔軟にカスタマイズして指定することが可能になります。
1-1.AVCaptureDevice.defaultメソッド
default
メソッドは引数に指定されたタイプのデフォルトデバイスを返します。引数にはAVMediaType
型の任意の値を渡します。
guard let audioDevice = AVCaptureDevice.default(for: .audio) else { return }
guard let videoDevice = AVCaptureDevice.default(for: .video) else { return }
1-2.AVCaptureDevice.DiscoverySession
公式リファレンス:AVCaptureDevice.DiscoverySession
AVCaptureDevice.DiscoverySession
は特定の条件にマッチするAVCaptureDeviceを検索するクラスです。引数にはデバイスタイプ(カメラの種類)とメディアタイプ(videoやaudioなど)、ポジション(カメラの位置)を渡します。
// カメラデバイスのプロパティ設定
let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(
deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera],
mediaType: AVMediaType.video,
position: AVCaptureDevice.Position.unspecified)
// プロパティの条件を満たしたカメラデバイスの取得
let devices = deviceDiscoverySession.devices
1-2-1.AVCaptureDevice.DeviceType
1つ目の引数ではAVCaptureDevice.DeviceType
構造体の任意の値を渡します。
.builtInWideAngleCamera // ワイドカメラ
.builtInUltraWideCamera // 近接カメラ
.builtInTelephotoCamera // 望遠カメラ
.builtInDualCamera // デュアルカメラ
.builtInDualWideCamera // デュアルワイドカメラ
.builtInTripleCamera // トリプルカメラ
iPhoneの機種によっては対応していない値もあるので注意が必要です。
position
はカメラの位置を指定します。
AVCaptureDevice.Position
.front // 前面カメラ
.back // 背面カメラ
.unspecified // 未指定
2.AVCaptureDeviceインスタンスからVCaptureDeviceInputを構築
設定したデバイスを元にAVCaptureDeviceInput
インスタンスを生成します。またAVCaptureSession
インスタンスもここで生成しておきます。このインスタンスに対してInputとOutputを登録していきます。
// 方法1
guard let videoDevice = AVCaptureDevice.default(for: .video) else { return }
// or
// 方法2
let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [AVCaptureDevice.DeviceType.builtInWideAngleCamera], mediaType: AVMediaType.video, position: AVCaptureDevice.Position.unspecified)
let videoDevice = deviceDiscoverySession.devices
// セッションの生成
self.avSession = AVCaptureSession()
do {
// 1 or 2 でデバイスを設定後AVCaptureDeviceInputを生成
let deviceInput = try AVCaptureDeviceInput(device: videoDevice)
// avInputを登録
// avOutputを登録
} catch {
print(error.localizedDescription)
}
3.AVCaptureSessionにAVCaptureDeviceInputを登録
Inputを登録する際はcanAddInput
メソッドを使用して追加が可能かを識別し、問題なければ登録します。
if self.avSession.canAddInput(deviceInput) {
self.avSession.addInput(deviceInput)
self.avInput = deviceInput
// avOutputを登録
}
4.AVCaptureSessionにAVCaptureOutputを登録
Outputも同様にcanAddOutput
メソッドを使用して追加が可能かを識別し、問題なければ登録します。
let photoOutput = AVCapturePhotoOutput()
if self.avSession.canAddOutput(photoOutput) {
self.avSession.addOutput(photoOutput)
self.avOutput = photoOutput
// AVCaptureVideoPreviewLayerで画面を構築
}
5.AVCaptureVideoPreviewLayerで画面を構築
公式リファレンス:AVCaptureVideoPreviewLayer
カメラデバイスからビューを表示するためのレイヤーを構築します。
let previewLayer = AVCaptureVideoPreviewLayer(session: self.avSession)
previewLayer.frame = self.photoView.bounds
previewLayer.videoGravity = .resizeAspectFill
self.avSession.sessionPreset = AVCaptureSession.Preset.photo
self.view.layer.addSublayer(previewLayer)
最後にstartRunning
メソッドを実行します。このメソッドでセッションの入力から出力へのデータフローを開始します。
self.avSession.startRunning()
ここまでがカメラ撮影機能を実装する基本の流れです。ここからは実際に使用できるサンプルを作成してみます。
AVFoundationの実装の流れ
- 「info.plist」にNSCameraUsageDescriptionキーを追加
- カメラ起動ボタン画面の構築
- カメラなどへのアクセス承認申請アラートの実装
- カメラUI画面の構築
- カメラを起動する処理を定義
- 撮影後の処理を実装
今回はサンプルとしてAVFoundationを使ってカメラを起動後撮影した画像をフォトライブラリに保存するアプリを作っていきたいと思います。フォトライブラリ操作のためにPhotos
フレームワークも使用します。
おすすめ記事:【Swift/PhotoKit】デバイスに写真を保存・削除・更新する方法!
また今回作成するUIViewControllerクラスは2つです。
- MainViewController:カメラ起動ボタン設置画面
- AudioViewController:カメラ画面
Swift UIで作成したい場合は以下の記事を参考にしてください。
1.info.plistにNSCameraUsageDescriptionキーを追加
アプリ内からデバイスのカメラにアクセスするためには「info.plist」に「NSCameraUsageDescription」キーを追加する必要があります。
「info.plist」を開いたらKeyにNSCameraUsageDescription
と入力し、Valueにはカメラ使用する旨を記載しておきます。自動でPrivacy - Camera Usage Description
に変換されます。
また今回はPhotos
フレームワークを使用してフォトライブラリも操作したいのでデバイスの写真アプリにアクセスできるように「info.plist」に「NSPhotoLibraryUsageDescription」キーも追加しておきます。
2.カメラ起動ボタン画面の構築
まずはカメラ起動ボタンを配置するビューを構築します。ここはシンプルにボタンの配置と承認申請アラートを表示させるようのメソッドを準備しておきます。
import UIKit
import Photos
import AVFoundation
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.backgroundColor = .orange
button.frame = CGRect(x: UIScreen.main.bounds.width/3, y: 200, width: UIScreen.main.bounds.width/3, height: 50)
button.setTitle("カメラ起動", for: .normal)
button.addTarget(self, action: #selector(tapedAudio), for: .touchUpInside)
view.addSubview(button)
allowedRequestStatus()
}
@objc func tapedAudio(sender: UIButton) {
self.present(AudioViewController(), animated: true, completion: nil)
}
func allowedRequestStatus() -> Bool{
}
}
3.カメラなどへのアクセス承認申請アラートの実装
カメラなどへのアクセスはユーザーに明示的な許可が必要になります。各クラスのメソッドには承認状態を取得するメソッドと以下のような承認申請アラートを表示するメソッドが用意されています。
カメラへのアクセス承認状態取得
AVCaptureDevice.authorizationStatus(for: .video)
カメラへのアクセス承認申請アラート表示
AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
})
フォトライブラリへのアクセス承認状態取得
PHPhotoLibrary.authorizationStatus(for: .addOnly)
フォトライブラリへのアクセス承認申請アラート表示
PHPhotoLibrary.requestAuthorization(for: .addOnly, handler: { status in
})
おすすめ記事:【Swift/PhotoKit】#承認状態を取得する
これらを駆使して現在の承認状態を取得し必要であれば申請を表示させるようにしておきます。
func allowedRequestStatus() -> Bool{
var avState = false
var phState = false
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
avState = true
break
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
DispatchQueue.main.async {
avState = granted
}
})
default:
avState = false
break
}
switch PHPhotoLibrary.authorizationStatus(for: .addOnly) {
case .authorized:
phState = true
break
case .notDetermined:
PHPhotoLibrary.requestAuthorization(for: .addOnly, handler: { status in
DispatchQueue.main.async {
if status == .authorized {
phState = true
}
}
})
default :
phState = false
break
}
if avState && phState {
return true
}else{
return false
}
}
4.カメラUI画面の構築
続いてカメラUI画面を構築します。AVFoundation
ではカメラ画面(カメラビューや撮影ボタンなど)も自分でカスタマイズして実装できるのでそのために必要となるphotoView
(カメラ画面)とshutterBtn
(撮影ボタン)を定義しておきます。
さらに後述しますがAVFoundation
で必要となる3つのプロパティもここで定義しておきます。
import UIKit
import Photos
import AVFoundation
class AudioViewController: UIViewController , AVCapturePhotoCaptureDelegate, AVCaptureMetadataOutputObjectsDelegate {
var photoView:UIImageView! = UIImageView()
let shutterBtn:UIButton! = UIButton() // これは後からaddSubviewします
var avSession: AVCaptureSession!
var avInput: AVCaptureDeviceInput!
var avOutput: AVCapturePhotoOutput!
override func viewDidLoad() {
super.viewDidLoad()
photoView.frame = CGRect(x: 0 ,y:0, width: UIScreen.main.bounds.width , height: UIScreen.main.bounds.height)
self.view.addSubview(photoView)
}
func allowedStatus() -> Bool{
//
}
func setupAVCapture() {
//
}
@objc func takePictureTapped(_ sender: UIButton) {
//
}
}
ここで実装するメソッドは以下の3つです。
- allowedStatus:承認状態識別(許可ならsetupAVCapture呼び出し)
- setupAVCapture:カメラ画面構築
- takePictureTapped:撮影ボタン
カメラを起動する処理を定義
allowedStatusメソッド
これは承認状態を識別して真偽値を返すメソッドです。
func allowedStatus() -> Bool{
var avState = false
var phState = false
if AVCaptureDevice.authorizationStatus(for: .video) == .authorized{
avState = true
}
if PHPhotoLibrary.authorizationStatus(for: .addOnly) == .authorized{
phState = true
}
if avState && phState {
return true
}else{
return false
}
}
setupAVCapture
ここでは実際にカメラ画面を使用するための準備とUIの配置を行います。AVCaptureSessionで解説した流れで実装しています。また撮影ボタンのUI部品も追加しておきます。
func setupAVCapture() {
self.avSession = AVCaptureSession()
guard let videoDevice = AVCaptureDevice.default(for: .video) else { return }
do {
let deviceInput = try AVCaptureDeviceInput(device: videoDevice)
if self.avSession.canAddInput(deviceInput) {
self.avSession.addInput(deviceInput)
self.avInput = deviceInput
let photoOutput = AVCapturePhotoOutput()
if self.avSession.canAddOutput(photoOutput) {
self.avSession.addOutput(photoOutput)
self.avOutput = photoOutput
let previewLayer = AVCaptureVideoPreviewLayer(session: self.avSession)
previewLayer.frame = self.photoView.bounds
previewLayer.videoGravity = .resizeAspectFill
self.avSession.sessionPreset = AVCaptureSession.Preset.photo
self.view.layer.addSublayer(previewLayer)
// 撮影ボタン
shutterBtn.frame = CGRect(x: 20, y: 20, width: 60, height: 60)
shutterBtn.setTitle("", for: UIControl.State.normal)
shutterBtn.backgroundColor = UIColor.white
shutterBtn.addTarget(self, action: #selector(takePictureTapped), for: .touchUpInside)
shutterBtn.layer.cornerRadius = 30
self.view.addSubview(shutterBtn)
self.avSession.startRunning()
}
}
} catch {
print(error.localizedDescription)
}
}
takePictureTapped:撮影ボタン
ここではカメラ画面に配置する撮影ボタンのアクションを記述します。capturePhoto
メソッドが実際に撮影を実行するメソッドです。
@objc func takePictureTapped(_ sender: UIButton) {
let settings = AVCapturePhotoSettings()
settings.flashMode = .auto
settings.isHighResolutionPhotoEnabled = false
self.avOutput?.capturePhoto(with: settings, delegate: self)
}
撮影後の処理を実装
最後に撮影した画像をデバイスに保存する処理を実装します。これはPhotoKit
の機能を使用します。詳しい実装方法は以下の記事を参考にしてください。
おすすめ記事:【Swift/PhotoKit】デバイスに写真を保存・削除・更新する方法!
また撮影後の処理を実装するためにAVCapturePhotoCaptureDelegate
プロトコルに準拠させphotoOutputメソッド(delegateメソッド)が呼び出せるようにしておきます。
公式リファレンス:AVCapturePhotoCaptureDelegate
func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let imageData = photo.fileDataRepresentation() {
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAsset(from:UIImage(data: imageData)!)
} completionHandler: { success, error in
print("Finished updating asset. " + (success ? "Success." : error!.localizedDescription))
}
}
}
これで全ての実装が完了しました。シミュレーターではテストできないので実機にビルドして試してみてください。
全体のコード
import UIKit
import Photos
import AVFoundation
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.backgroundColor = .orange
button.frame = CGRect(x: UIScreen.main.bounds.width/3, y: 200, width: UIScreen.main.bounds.width/3, height: 50)
button.setTitle("カメラ起動", for: .normal)
button.addTarget(self, action: #selector(tapedAudio), for: .touchUpInside)
view.addSubview(button)
allowedRequestStatus()
}
@objc func tapedAudio(sender: UIButton) {
self.present(AudioViewController(), animated: true, completion: nil)
}
func allowedRequestStatus() -> Bool{
var avState = false
var phState = false
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
avState = true
break
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
DispatchQueue.main.async {
avState = granted
}
})
default:
avState = false
break
}
switch PHPhotoLibrary.authorizationStatus(for: .addOnly) {
case .authorized:
phState = true
break
case .notDetermined:
PHPhotoLibrary.requestAuthorization(for: .addOnly, handler: { status in
DispatchQueue.main.async {
if status == .authorized {
phState = true
}
}
})
default :
phState = false
break
}
if avState && phState {
return true
}else{
return false
}
}
}
import UIKit
import Photos
import AVFoundation
class AudioViewController: UIViewController , AVCapturePhotoCaptureDelegate, AVCaptureMetadataOutputObjectsDelegate {
let photoView:UIImageView! = UIImageView()
let shutterBtn:UIButton! = UIButton()
var avSession: AVCaptureSession!
var avInput: AVCaptureDeviceInput!
var avOutput: AVCapturePhotoOutput!
override func viewDidLoad() {
super.viewDidLoad()
photoView.frame = CGRect(x: 0 ,y:0, width: UIScreen.main.bounds.width , height: UIScreen.main.bounds.height)
self.view.addSubview(photoView)
if allowedStatus() {
setupAVCapture()
}
}
func allowedStatus() -> Bool{
var avState = false
var phState = false
if AVCaptureDevice.authorizationStatus(for: .video) == .authorized{
avState = true
}
if PHPhotoLibrary.authorizationStatus(for: .addOnly) == .authorized{
phState = true
}
if avState && phState {
return true
}else{
return false
}
}
func setupAVCapture() {
self.avSession = AVCaptureSession()
guard let videoDevice = AVCaptureDevice.default(for: .video) else { return }
do {
let deviceInput = try AVCaptureDeviceInput(device: videoDevice)
if self.avSession.canAddInput(deviceInput) {
self.avSession.addInput(deviceInput)
self.avInput = deviceInput
let photoOutput = AVCapturePhotoOutput()
if self.avSession.canAddOutput(photoOutput) {
self.avSession.addOutput(photoOutput)
self.avOutput = photoOutput
let previewLayer = AVCaptureVideoPreviewLayer(session: self.avSession)
previewLayer.frame = self.photoView.bounds
previewLayer.videoGravity = .resizeAspectFill
self.avSession.sessionPreset = AVCaptureSession.Preset.photo
self.view.layer.addSublayer(previewLayer)
// 撮影ボタン
shutterBtn.frame = CGRect(x: 20, y: 20, width: 60, height: 60)
shutterBtn.setTitle("", for: UIControl.State.normal)
shutterBtn.backgroundColor = UIColor.white
shutterBtn.addTarget(self, action: #selector(takePictureTapped), for: .touchUpInside)
shutterBtn.layer.cornerRadius = 30
self.view.addSubview(shutterBtn)
self.avSession.startRunning()
}
}
} catch {
print(error.localizedDescription)
}
}
@objc func takePictureTapped(_ sender: UIButton) {
let settings = AVCapturePhotoSettings()
settings.flashMode = .auto
settings.isHighResolutionPhotoEnabled = false
self.avOutput?.capturePhoto(with: settings, delegate: self)
}
func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let imageData = photo.fileDataRepresentation() {
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAsset(from:UIImage(data: imageData)!)
} completionHandler: { success, error in
print("Finished updating asset. " + (success ? "Success." : error!.localizedDescription))
}
}
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。