Memo

メモ > 技術 > IDE: Xcode > SwiftUI+Camera

SwiftUI+Camera
■画像取得(カメラ&ライブラリ) SwiftUIでUIImagePickerControllerを使う https://zenn.dev/yorifuji/articles/swiftui-imagepicker SwiftUIでAVFoundationを使ってみた - Qiita https://qiita.com/From_F/items/759544896fe89e828898 【SwiftUI】カメラ機能の実装方法【撮影画像とライブラリー画像の利用】 https://tomato-develop.com/swiftui-how-to-use-camera-and-select-photos-from-library/ iOSアプリCamera撮影, UIImagePickerController https://i-app-tec.com/ios/camera.html 「App」で以下の設定で新規作成する Product Name: imagepicker Interface: SwiftUI Language: Swift Info.plist にKey「Privacy - Camera Usage Description」を追加し、Valueに「写真を撮影します。」と記載しておく ファイルの内容を直接確認すると、dictタグ内に以下が追加されている Info.plist
<key>NSCameraUsageDescription</key> <string>写真を撮影します。</string>
ContentView.swift
import SwiftUI struct ContentView: View { @State var showingPicker = false @State var image: UIImage? var body: some View { VStack { if let image = image { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fit) } Text("Image") .onTapGesture { showingPicker.toggle() } } .sheet(isPresented: $showingPicker) { ImagePickerView(image: $image, sourceType: .camera) //ImagePickerView(image: $image, sourceType: .camera, allowsEditing: true) //ImagePickerView(image: $image, sourceType: .library) } } } #Preview { ContentView() }
ImagePickerView.swift
import SwiftUI struct ImagePickerView: UIViewControllerRepresentable { typealias UIViewControllerType = UIImagePickerController @Environment(\.presentationMode) var presentationMode @Binding var image: UIImage? enum SourceType { case camera case library } var sourceType: SourceType var allowsEditing: Bool = false class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { let parent: ImagePickerView init(_ parent: ImagePickerView) { self.parent = parent } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { if let image = info[.editedImage] as? UIImage { parent.image = image } else if let image = info[.originalImage] as? UIImage { parent.image = image } parent.presentationMode.wrappedValue.dismiss() } } func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIViewController(context: Context) -> UIImagePickerController { let viewController = UIImagePickerController() viewController.delegate = context.coordinator switch sourceType { case .camera: viewController.sourceType = UIImagePickerController.SourceType.camera case .library: viewController.sourceType = UIImagePickerController.SourceType.photoLibrary } viewController.allowsEditing = allowsEditing return viewController } func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { } }
「ImagePickerView」で撮影する際に「allowsEditing: true」を指定すると、撮影後にトリミング位置を指定できるみたい また、カメラUIを日本語化するには、 Info.plist のKey「Localization native development region」のValueを「Japan」に変更する [iOS]カメラ機能作成のメモ - ワークレ https://reftec.work/posts/2019/10/126/ ■画像取得(カメラ&ライブラリ+画像を保存) Info.plist にKey「Privacy - Photo Library Additions Usage Description」を追加し、Valueに「画像を保存します。」と記載しておく ファイルの内容を直接確認すると、dictタグ内に以下が追加されている Info.plist
<key>NSPhotoLibraryAddUsageDescription</key> <string>画像を保存します。</string>
ContentView.swift
@State var saved: Bool = false var body: some View { VStack { if let image = image { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fit) Button(action: { // 画像を保存 UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) // 保存完了アラートを表示 saved = true }) { Text("保存") }.alert(isPresented: $saved, content: { Alert( title: Text("保存"), message: Text("画像が保存されました。") ) }) }
保存された画像は、標準カメラ並みの画質みたい ■カメラプレビュー(最低限のプレビューを独自に作成) 【SwiftUI】最低限のコードでカメラのプレビューを表示する - Qiita https://qiita.com/eb4gh/items/3918f1d28c9e68fc1705 SwiftUIでAVFundationを導入する【Video Capture偏】 https://blog.personal-factory.com/2020/06/14/introduce-avfundation-by-swiftui/ SwiftUIでAVFoundationを使ってみた - Qiita https://qiita.com/From_F/items/759544896fe89e828898 UIViewにおけるレイアウトのライフサイクル - Qiita https://qiita.com/shoheiyokoyama/items/2f76938dffa845130acc 「App」で以下の設定で新規作成する Product Name: camera Interface: SwiftUI Language: Swift Info.plist にKey「Privacy - Camera Usage Description」を追加し、Valueに「写真を撮影します。」と記載しておく ファイルの内容を直接確認すると、dictタグ内に以下が追加されている Info.plist
<key>NSCameraUsageDescription</key> <string>写真を撮影します。</string>
ContentView.swift
import SwiftUI import AVFoundation struct ContentView: View { var body: some View { CameraView() } } // SwiftUIでUIKitのViewを使いたい場合、UIViewRepresentableを継承する struct CameraView: UIViewRepresentable { // 画面が作成されたときに呼ばれる(実装必須) func makeUIView(context: Context) -> UIView { BaseCameraView() } // 画面が更新されたときに呼ばれる(実装必須) func updateUIView(_ uiView: UIViewType, context: Context) {} } class BaseCameraView: UIView { // 利用されるまで初期化されない変数として定義 lazy var initCaptureSession: Void = { var device: AVCaptureDevice? if let availableDevice = AVCaptureDevice.DiscoverySession( deviceTypes: [.builtInWideAngleCamera], mediaType: .video, //position: .front position: .back ).devices.first { device = availableDevice } do { let input = try AVCaptureDeviceInput(device: device!) let session = AVCaptureSession() session.addInput(input) session.startRunning() layer.insertSublayer(AVCaptureVideoPreviewLayer(session: session), at: 0) } catch let error { print(error.localizedDescription) } }() // 画面の制約が更新されると呼ばれる override func layoutSubviews() { super.layoutSubviews() _ = initCaptureSession (layer.sublayers?.first as? AVCaptureVideoPreviewLayer)?.frame = frame } } #Preview { ContentView() }
■カメラプレビュー(カメラ撮影を独自に作成) SwiftUIでAVFoundationを使ってフレームバッファを取得する https://zenn.dev/yorifuji/articles/swiftui-avfoundation 「App」で以下の設定で新規作成する Product Name: camera Interface: SwiftUI Language: Swift Info.plist にKey「Privacy - Camera Usage Description」を追加し、Valueに「写真を撮影します。」と記載しておく さらにKey「Privacy - Photo Library Additions Usage Description」を追加し、Valueに「写真を保存します。」と記載しておく ファイルの内容を直接確認すると、dictタグ内に以下が追加されている Info.plist
<key>NSCameraUsageDescription</key> <string>写真を撮影します。</string> <key>NSPhotoLibraryAddUsageDescription</key> <string>写真を保存します。</string>
VideoCapture.swift
import Foundation import AVFoundation class VideoCapture: NSObject { let captureSession = AVCaptureSession() var handler: ((CMSampleBuffer) -> Void)? override init() { super.init() setup() } // キャプチャ設定 func setup() { captureSession.beginConfiguration() let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) guard let deviceInput = try? AVCaptureDeviceInput(device: device!), captureSession.canAddInput(deviceInput) else { return } captureSession.addInput(deviceInput) let videoDataOutput = AVCaptureVideoDataOutput() videoDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "mydispatchqueue")) videoDataOutput.alwaysDiscardsLateVideoFrames = true guard captureSession.canAddOutput(videoDataOutput) else { return } captureSession.addOutput(videoDataOutput) // アウトプットの画像を縦向きに変更(標準は横) for connection in videoDataOutput.connections { if connection.isVideoOrientationSupported { connection.videoOrientation = .portrait } } captureSession.commitConfiguration() } // キャプチャ開始 func run(_ handler: @escaping (CMSampleBuffer) -> Void) { if !captureSession.isRunning { self.handler = handler captureSession.startRunning() } } // キャプチャ停止 func stop() { if captureSession.isRunning { captureSession.stopRunning() } } } extension VideoCapture: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { if let handler = handler { handler(sampleBuffer) } } }
ContentView.swift
import SwiftUI import AVFoundation struct ContentView: View { let videoCapture = VideoCapture() @State var image: UIImage? = nil @State var saved: Bool = false var body: some View { VStack { if let image = image { Image(uiImage: image) .resizable() .scaledToFit() } HStack { Button("撮影") { // カメラプレビューを停止 stopCameraPreview() // シャッター音を鳴らす AudioServicesPlaySystemSound(1108) // 画像を保存 UIImageWriteToSavedPhotosAlbum(self.image!, nil, nil, nil) // 保存完了アラートを表示 saved = true }.alert(isPresented: $saved, content: { Alert( title: Text("保存"), message: Text("画像が保存されました。"), dismissButton: .default(Text("OK"), action: { // カメラプレビューを開始 startCameraPreview() }) ) }) } } .onAppear { // カメラプレビューを開始 startCameraPreview() } } // カメラプレビューを開始 func startCameraPreview() { // キャプチャ開始 videoCapture.run { sampleBuffer in if let convertImage = UIImageFromSampleBuffer(sampleBuffer) { DispatchQueue.main.async { self.image = convertImage } } } } // カメラプレビューを停止 func stopCameraPreview() { // キャプチャ停止 videoCapture.stop() } // カメラプレビューからイメージを取得 func UIImageFromSampleBuffer(_ sampleBuffer: CMSampleBuffer) -> UIImage? { if let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { let ciImage = CIImage(cvPixelBuffer: pixelBuffer) let imageRect = CGRect(x: 0, y: 0, width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer)) let context = CIContext() if let image = context.createCGImage(ciImage, from: imageRect) { return UIImage(cgImage: image) } } return nil } } #Preview { ContentView() }
ビューに表示したサイズの画像を保存するためか、 標準カメラに比べると画質は低い 「AudioServicesPlaySystemSound(1108)」でシャッター音を鳴らすことはできたが、マナーモードだと鳴らなかった 強制的に鳴らすことはできるか、もしくはこのままでいいのか 引き続き調べたい iOS - iPhoneのデフォルトのシャッター音を鳴らすには|teratail https://teratail.com/questions/89195 ios - ボリューム設定が0(ミュート)であっても音を鳴らす方法 - スタック・オーバーフロー https://ja.stackoverflow.com/questions/6068/%E3%83%9C%E3%83%AA%E3%83%A5%E3%83%BC%E3%83%A0%E8%A8%AD%E... iOS - 実機で音声が再生されない|teratail https://teratail.com/questions/40086 未検証だが、以下も参考になりそう Swiftでカメラアプリを作成する(1) - Qiita https://qiita.com/t_okkan/items/f2ba9b7009b49fc2e30a Swiftでカメラアプリを作成する(2) - Qiita https://qiita.com/t_okkan/items/b2dd11426eab107c5d15 ■カメラプレビュー(プレビューの映像を反転) 未検証 iOSでの動画処理における「回転」「向き」の取り扱いでもう混乱したくない - Qiita https://qiita.com/shu223/items/057351d41229861251af ■Live Photos Live Photosを表示する解説はあるが、作成する解説はすぐに見つからなかった 独自に実装するなら、画像と動画の作成&保存処理をゴリゴリと書いていくくらいか もしくは、専用のファイル形式やそれを扱うための命令があるか 要調査 【SwiftUI】Live Photoの表示 https://zenn.dev/harumaru/articles/6f7ec2659261f6 Live Photos(ライブフォト)を表示するクラス PHLivePhotoView を試す - Qiita https://qiita.com/shu223/items/e87ea139512ba732997d

Advertisement