次
概要
前回はOSのカメラ(フォトアプリに近いもの)を実装したが、以下のメリット・デメリットがある。
メリット
- とにかく手軽に実装できる
デメリット
- カメラから取得した映像の範囲が狭い
- 見栄えが悪い
- カスタマイズしづらい
そこで、今回はAVCapture関連のクラスを使用してカスタマイズしやすいカメラの実装をしていく。
調べれば色々な記事は出てくるが、処理部分とView部分を分けて、かつSwiftUIで実装している記事はほとんどなかったため、ここに記す。
ソースコード
アプリ起動部分
import SwiftUI
@main
struct TestApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
CaptureCameraPrepareView()
}
}
}
画面遷移前(撮影した画像を表示する画面)
View部分
import SwiftUI
struct CaptureCameraPrepareView: View {
@ObservedObject private var viewModel = CaptureCameraPrepareViewModel()
var body: some View {
NavigationView {
VStack(alignment: .center) {
ZStack {
Rectangle()
.fill(Color.gray.opacity(0.5))
.padding()
Text("P h o t o")
.font(.system(size: 30))
.bold()
.foregroundColor(Color.white)
if let uiImage = viewModel.imageData {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
}
}
NavigationLink {
CaptureCameraView(delegate: self)
} label: {
Text("カメラ起動")
}
}
}
}
}
extension CaptureCameraPrepareView: CaptureCameraViewDelegate {
func passImage(image: UIImage) {
print(image)
viewModel.imageData = image
}
}
処理部分
import Foundation
import UIKit
class CaptureCameraPrepareViewModel: ObservableObject {
@Published var imageData: UIImage?
}
画面遷移後(カメラを起動して撮影を行う画面)
View部分
import SwiftUI
protocol CaptureCameraViewDelegate {
func passImage(image: UIImage)
}
struct CaptureCameraView: View {
@Environment(\.presentationMode) var presentation
@ObservedObject private(set) var viewModel = CaptureCameraViewModel()
var delegate: CaptureCameraViewDelegate?
var body: some View {
ZStack {
CapturePreview(captureCameraViewModel: viewModel)
VStack {
Spacer()
Button {
viewModel.capture()
} label: {
Image(systemName: "circle.inset.filled")
.resizable(resizingMode: .stretch)
.foregroundColor(.white)
}
.frame(width: 60, height: 60)
}
}
.onChange(of: viewModel.imageData) { imageData in
guard let imageData = imageData else {
return
}
delegate?.passImage(image: imageData)
presentation.wrappedValue.dismiss()
}
}
}
struct CapturePreview: UIViewRepresentable {
private var captureCameraViewModel: CaptureCameraViewModel
init(captureCameraViewModel: CaptureCameraViewModel) {
self.captureCameraViewModel = captureCameraViewModel
}
public func makeUIView(context: Context) -> some UIView {
let cameraView = captureCameraViewModel.previewView
cameraView.frame = UIScreen.main.bounds
captureCameraViewModel.launchCamera()
return cameraView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
処理部分
import AVFoundation
import Foundation
import UIKit
protocol CaptureCameraViewModelDelegate {
func afterCaptureProcess(image: UIImage)
}
class CaptureCameraViewModel: NSObject, ObservableObject {
@Published private(set) var imageData: UIImage?
var previewView: UIView = UIView()
private var session: AVCaptureSession?
private var photoOutput: AVCapturePhotoOutput?
private var captureSetting: AVCapturePhotoSettings?
func launchCamera() {
if let session = self.session {
DispatchQueue.global(qos: .background).async {
session.startRunning()
}
return
}
guard let device = AVCaptureDevice.default(.builtInDualWideCamera, for: .video, position: .back),
let deviceInput = try? AVCaptureDeviceInput(device: device) else {
return
}
let photoOutput = AVCapturePhotoOutput()
let session = AVCaptureSession()
switch UIDevice.current.userInterfaceIdiom {
case .phone:
session.sessionPreset = .high
default:
session.sessionPreset = .photo
}
session.addInput(deviceInput)
session.addOutput(photoOutput)
let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: session)
videoPreviewLayer.videoGravity = .resizeAspectFill
videoPreviewLayer.connection?.videoOrientation = .portrait
videoPreviewLayer.frame = previewView.bounds
DispatchQueue.global(qos: .background).async {
session.startRunning()
}
self.photoOutput = photoOutput
self.session = session
previewView.layer.insertSublayer(videoPreviewLayer, at: 0)
}
func capture() {
Task { @MainActor in
let captureSetting = AVCapturePhotoSettings()
captureSetting.flashMode = .off
photoOutput?.capturePhoto(with: captureSetting, delegate: self)
self.captureSetting = captureSetting
}
}
}
extension CaptureCameraViewModel: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if error != nil {
print("error")
return
}
guard let fileData = photo.fileDataRepresentation(),
let image = UIImage(data: fileData) else {
return
}
print("image")
imageData = image
}
}
デモ動画
詳細
画面遷移後(カメラを起動して撮影を行う画面)
カメラ起動の準備、そして撮影した時の処理を行う。
詳細は【Swift/SwiftUI】AVCaptureを使用してカメラ起動(準備編)を参照。
画面遷移前(撮影した画像を表示する画面)
こちらは特に難しい実装はない。
撮影した画像を表示する以下の部分。内容も単純で、画像のデータ(viewModel.imageData)が格納されていたら、その画像を表示する。
ZStack {
Rectangle()
.fill(Color.gray.opacity(0.5))
.padding()
Text("P h o t o")
.font(.system(size: 30))
.bold()
.foregroundColor(Color.white)
if let uiImage = viewModel.imageData {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
}
}
ただし、撮影画面に写る際に少し工夫が必要。
画面遷移する際、自身をデリゲートとして渡すことで、カメラ撮影後に画面遷移前であるこの画面に画像データを渡せるようにしている。
NavigationLink {
CaptureCameraView(delegate: self)
} label: {
Text("カメラ起動")
}
こうすることで、画面遷移後にあるpassImageメソッドが呼び出されるようになる。
.onChange(of: viewModel.imageData) { imageData in
guard let imageData = imageData else {
return
}
delegate?.passImage(image: imageData)
presentation.wrappedValue.dismiss()
}
extension CaptureCameraPrepareView: CaptureCameraViewDelegate {
func passImage(image: UIImage) {
print(image)
viewModel.imageData = image
}
}
参考ページ
Super Hahnah「[Swift] AVFoundation による動画撮影の設定: カメラ種類 / ズーム / 録画時間 / 画質」
zenn「AVFoundationでカメラ種類判定(二眼・三眼など)」