【Swift UI/AVFoundation】カメラアプリを開発する方法!撮影と保存

この記事からわかること

  • Swift UIカメラアプリ実装方法
  • AVFoundation使い方特徴
  • カスタマイズできるカメラビュー作り方

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Swift UIでカメラアプリを実装したい場合はUIImagePickerControllerをSwift UIで表示できるようにしたり、AVFoundationを駆使して実装する必要があります。今回はAVFoundationを使用してSwift UIでカメラアプリを作ってみたいと思います。

AVFoundationとは?

AVFoundation実装するのはなかなか手間がかかりますがカスタマイズ性が高く柔軟なカメラアプリを開発することができるフレームワークで、撮影ボタンの位置やデザイン、カメラビューのサイズなどを好きなように配置できるので独自のカメラアプリを作成するのにおすすめです。

AVFoundationの基本的な使い方はUIKitでの実装方法とともに以下の記事で紹介しているので今回はSwift UIで実装する方法をまとめていきます。

【Swift/AVFoundation】アプリ内からカメラを起動し写真撮影をする方法

Swift UIでAVFoundationを使用する方法

Swift UIでAVFoundationを使用するためには以下の3ファイルを用意します。ファイルの構成のアドバイスがあればお願いします。

作成するファイル

  1. Viewファイル
  2. ViewModelファイル
  3. Repositoryファイル

全体像はGitHubに上げているので参考にしてください。

おすすめ記事:GitHub-MyCameraApp

Repositoryファイル


import SwiftUI
import AVFoundation
import Combine

class CameraFunctionRepository: NSObject, AVCapturePhotoCaptureDelegate, AVCaptureMetadataOutputObjectsDelegate {
    
    /// 撮影された写真
    public var image: AnyPublisher<UIImage, Never> {
        _image.eraseToAnyPublisher()
    }
    
    private let _image = PassthroughSubject<UIImage, Never>()
    
    /// 撮影プレビュー領域
    public var previewLayer: AnyPublisher<CALayer, Never> {
        _previewLayer.eraseToAnyPublisher()
    }
    
    private let _previewLayer = PassthroughSubject<CALayer, Never>()
    
    /// 撮影デバイス
    private var capturepDevice: AVCaptureDevice!
    
    private var avSession: AVCaptureSession = AVCaptureSession()
    private var avInput: AVCaptureDeviceInput!
    private var avOutput: AVCapturePhotoOutput!
}

extension CameraFunctionRepository {
    
    ///  初期準備
    public func prepareSetting() {
        setUpDevice()
        beginSession()
    }
    
    /// 写真撮影
    public func takePhoto() {
        let settings = AVCapturePhotoSettings()
        settings.flashMode = .auto
        settings.isHighResolutionPhotoEnabled = false
        // settings.maxPhotoDimensions = CGSize(width: desiredWidth, height: desiredHeight)
        self.avOutput?.capturePhoto(with: settings, delegate: self)
    }
    
    /// セッション開始
    public func startSession() {
        DispatchQueue.global(qos: .background).async { [weak self] in
            guard let self else { return }
            if self.avSession.isRunning { return }
            self.avSession.startRunning()
        }
    }
    
    /// セッション終了
    public func endSession() {
        if !avSession.isRunning { return }
        avSession.stopRunning()
    }
}

extension CameraFunctionRepository {
    
    // 使用するデバイスを取得
    private func setUpDevice() {
        avSession.sessionPreset = .photo
        
        guard let device = AVCaptureDevice.default(for: .video) else { return }
        capturepDevice = device
        //        if let availableDevice = AVCaptureDevice.DiscoverySession(
        //            deviceTypes: [.builtInWideAngleCamera],
        //            mediaType: AVMediaType.video,
        //            position: .back).devices.first {
        //            capturepDevice = availableDevice
        //        }
    }
    
    // セッションの開始
    private func beginSession() {
        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.videoGravity = .resize
                    self._previewLayer.send(previewLayer)
                    
                    self.avSession.sessionPreset = AVCaptureSession.Preset.photo
                }
            }
        } catch {
            print(error.localizedDescription)
        }
    }
    // デリゲートメソッド
    func photoOutput(_ output: AVCapturePhotoOutput,
                     didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let imageData = photo.fileDataRepresentation() {
            _image.send(UIImage(data: imageData)!)
        }
    }
}

// カメラプレビュー
struct CALayerView: UIViewControllerRepresentable {
    var caLayer: CALayer
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<CALayerView>) -> UIViewController {
        let viewController = UIViewController()
        // プレビューの大きさを指定 iPhoneのカメラは4:3なのでそれに合わせる
        /// `previewLayer.videoGravity = .resize` を指定することでレイヤーいっぱいに合わせる
        caLayer.frame = CGRect(x: 0, y: 0, width: DeviceSizeManager.deviceWidth, height: (4 / 3) * DeviceSizeManager.deviceWidth)
        viewController.view.layer.addSublayer(caLayer)
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<CALayerView>) {
        // プレビューの大きさを指定 iPhoneのカメラは4:3なのでそれに合わせる
        /// `previewLayer.videoGravity = .resize` を指定することでレイヤーいっぱいに合わせる
        caLayer.frame = CGRect(x: 0, y: 0, width: DeviceSizeManager.deviceWidth, height: (4 / 3) * DeviceSizeManager.deviceWidth)
    }
}

ViewModelファイル


import UIKit
import Combine

class CameraFunctionViewModel: ObservableObject {
    
    // MARK: - Model
    @Published  var image: UIImage?
    @Published  var previewLayer: CALayer?
    
    // MARK: - Repository
    private var cameraRepository = CameraFunctionRepository()
    // MARK: - Combine
    private var cancellables:Set<AnyCancellable> = Set()

    init() {
        cameraRepository.image.sink { [weak self] image in
            guard let self else { return }
            self.image = image
        }.store(in: &cancellables)
        
        cameraRepository.previewLayer.sink { [weak self] previewLayer in
            guard let self else { return }
            self.previewLayer = previewLayer
        }.store(in: &cancellables)
        
        cameraRepository.prepareSetting()
    }
}

// MARK: - カメラ機能
extension CameraFunctionViewModel {
    /// 写真撮影
    public func takePhoto() {
        cameraRepository.takePhoto()
    }
    /// セッション開始
    public func startSession() {
        cameraRepository.startSession()
    }
    /// セッション終了
    public func endSession() {
        cameraRepository.endSession()
    }
}

Viewファイル


import SwiftUI

struct CameraFunctionView: View {
    
    @ObservedObject  private var viewModel = CameraFunctionViewModel()
    
    var body: some View {
        VStack(spacing: 0) {
            if let image = viewModel.image {
                
                Image(uiImage: image)
                    .resizable()
                    .frame(width: DeviceSizeManager.deviceWidth, height: (4 / 3) * DeviceSizeManager.deviceWidth)
                
                Spacer()
                
                HStack {
                    
                    Button(action: {
                        self.viewModel.image = nil
                    }) {
                        Image(systemName: "trash")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 20)
                            .foregroundStyle(.white)
                    }.frame(width: 40)
                        .padding(.leading, 20)
                    
                    Spacer()
                    
                    Button(action: {
                        // TODO: 保存処理など
                    }) {
                        Image(systemName: "checkmark")
                            .resizable()
                            .foregroundStyle(.white)
                            .frame(width: 40, height: 40)
                            .overlay {
                                RoundedRectangle(cornerRadius: 80)
                                    .stroke(Color.white, lineWidth: 2)
                                    .frame(width: 80, height: 80)
                            }.padding(.bottom, 10)
                    }
                    
                    Spacer()
                    
                    Spacer()
                        .frame(width: 40)
                        .padding(.trailing, 20)
                }.padding(.bottom)
                
            } else {
                
                if let previewLayer = viewModel.previewLayer {
                    CALayerView(caLayer: previewLayer)
                }
                
                
                HStack {
                    Spacer()
                    
                    Button(action: {
                        self.viewModel.takePhoto()
                    }) {
                        Image(systemName: "camera")
                            .resizable()
                            .foregroundStyle(.white)
                            .frame(width: 50, height: 40)
                            .overlay {
                                RoundedRectangle(cornerRadius: 80)
                                    .stroke(Color.white, lineWidth: 2)
                                    .frame(width: 80, height: 80)
                            }.padding(.bottom, 10)
                    }
                    Spacer()
                }.padding(.bottom)
                
            }
        }.background(Color.black)
            .onAppear {
                self.viewModel.startSession()
            }.onDisappear {
                self.viewModel.endSession()
            }
    }
}

#Preview {
    CameraFunctionView()
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index