メモ > 技術 > 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