안녕하세요! 피피아노입니다 🎵
이번 포스팅에서는 SwiftUI와 AVFoundation을 사용해서 마치 인생네컷 포토 부스처럼 자동으로 촬영이 되는 카메라 기능을 만드는 법을 정리해보려고 합니다.
그럼 바로 시작하겠습니다!
기능 정리
우선 카메라 앱을 만들기 전에 어떤 기능이 필요한지 먼저 정리를 해보겠습니다. 제가 만드는 앱에서는 크게 3가지 기능이 꼭 필요했습니다.
- 자동 5초 카운트 다운 후 사진 촬영
- 촬영된 사진 화면에 띄우기
- 최대 4장의 사진 자동 촬영
이렇게 3가지 입니다.
카메라 모델 구현
먼저, AVFoundation을 사용해서 카메라 기능을 관리할 CameraModel 클래스를 만들어 보겠습니다.
import AVFoundation
import SwiftUI
@Observable
class CameraModel: NSObject {
let session = AVCaptureSession()
private var camera: AVCaptureDevice?
private var input: AVCaptureDeviceInput?
private let output = AVCapturePhotoOutput()
private var photoCompletion: ((UIImage) -> Void)?
override init() {
super.init()
Task { await setupSession() }
}
// 세션 설정
private func setupSession() async {
do {
session.beginConfiguration()
// 기본 카메라를 전면 카메라로 설정
camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
if let camera {
input = try AVCaptureDeviceInput(device: camera)
if session.canAddInput(input!) {
session.addInput(input!)
}
if session.canAddOutput(output) {
session.addOutput(output)
}
}
session.commitConfiguration()
} catch {
print("카메라 설정 오류: \(error.localizedDescription)")
}
}
}
CameraModel은 Observable로 선언되어 SwiftUI 뷰가 카메라 상태 변경에 반응할 수 있게 합니다. 이 클래스는 AVCaptureSession을 관리하여 카메라 입력과 출력을 처리합니다.
카메라 프리뷰 뷰 만들기
카메라 피드를 화면에 표시하기 위해 UIViewRepresentable 프로토콜을 구현한 CameraPreview 뷰를 만들어야 합니다.
카메라 프리뷰를 구현하지 않으면 화면에 아무것도 표시되지 않고 그냥 까만색 화면으로 나오기 때문에 반드시 구현을 해주어야 합니다.
struct CameraPreview: UIViewRepresentable {
let session: AVCaptureSession
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.frame
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
자동 촬영 기능 구현
사실 자동 촬영 기능이 없어도 카메라 촬영에는 아무 문제 없지만 저는 조금 더 디테일을 살리고 싶어서 자동 촬영 기능까지 구현을 해보았습니다.
이 기능은 5초 카운트다운을 화면에 표시해주고 5초 카운트다운이 끝나면 촬영 버튼을 누르지 않아도 자동으로 사진이 찍히는 기능인데 총 4장의 사진이 촬영될 때까지 이 과정을 반복하도록 구현을 했습니다.
private func startAutoCapture() {
photoCount = 0
captureNextPhoto()
}
private func captureNextPhoto() {
if photoCount >= 4 { return } // 사진 4장을 다 찍으면 종료
isCountingDown = true
countDown = 5
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
if countDown > 0 {
countDown -= 1
} else {
timer.invalidate()
isCountingDown = false
capturePhoto()
}
}
}
private func capturePhoto() {
camera.capturePhoto { image in
if let firstEmpty = displayedImages.firstIndex(where: { $0 == nil }) {
var newImages = displayedImages
newImages[firstEmpty] = Image(uiImage: image)
displayedImages = newImages
photoCount += 1
if photoCount < 4 {
captureNextPhoto()
}
}
}
}
CameraView UI 구성
이제 메인 CameraView를 구현하겠습니다. 이 뷰는 카메라 프리뷰, 촬영 버튼, 카메라 전환 버튼, 촬영된 사진을 화면에 띄우는 것 모두 포함한 뷰입니다.
struct CameraView: View {
@Bindable private var camera = CameraModel()
@Binding var displayedImages: [Image?]
@Environment(\.dismiss) var dismiss
@State private var shouldNavigateToContent = false
@State private var countDown = 5
@State private var isCountingDown = false
@State private var photoCount = 0
var body: some View {
ZStack {
// 카메라 미리보기 화면
CameraPreview(session: camera.session)
.ignoresSafeArea()
if isCountingDown {
Text("\(countDown)")
.font(.system(size: 100, weight: .bold))
.bold()
.foregroundColor(.red)
.padding()
.transition(.scale)
}
VStack {
Spacer()
// 찍은 사진을 보여주는 미리보기 (최대 4장)
HStack {
ForEach(0..<4) { index in
if let image = displayedImages[index] {
image
.resizable()
.scaledToFill()
.frame(width: 60, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 8))
} else {
RoundedRectangle(cornerRadius: 8)
.stroke(Color.white, lineWidth: 2)
.frame(width: 60, height: 80)
}
}
}
.padding()
// 카메라 컨트롤 버튼 (닫기, 촬영, 전환)
HStack(spacing: 60) {
// 닫기 버튼
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(.white)
}
// 사진 촬영 버튼
Button {
camera.capturePhoto { image in
if let firstEmpty = displayedImages.firstIndex(where: { $0 == nil }) {
var newImages = displayedImages
newImages[firstEmpty] = Image(uiImage: image)
displayedImages = newImages
}
countDown = 5
}
} label: {
Circle()
.stroke(Color.white, lineWidth: 3)
.frame(width: 65, height: 65)
.overlay(
Circle()
.fill(Color.white)
.frame(width: 50, height: 50)
)
}
// 카메라 전환 버튼
Button {
camera.switchCamera()
} label: {
Image(systemName: "camera.rotate.fill")
.font(.system(size: 30))
.foregroundColor(.white)
}
}
.padding(.bottom, 30)
}
}
.task {
await camera.checkPermissions()
startAutoCapture()
}
.onDisappear {
displayedImages = Array(repeating: nil, count: 4)
}
.navigationDestination(isPresented: $shouldNavigateToContent) {
ContentView(initialImages: displayedImages)
}
.onChange(of: displayedImages) { _, newImages in
if !newImages.contains(where: { $0 == nil }) {
shouldNavigateToContent = true
}
}
}
}
여기에서 중요한 점은 촬영 버튼을 누르면 카운트다운이 그대로 흘러가는 것이 아니라 다시 5초로 초기화를 해주어야 한다는 부분입니다. (사실 처음에 이 부분을 간과하고 제작했다가 버튼을 눌러도 그냥 카운트가 진행돼서 후다닥 고쳤습니다 ㅋㅋㅋㅋ)
카메라 권한 관리
카메라 앱을 만든다면 무조건 필요한 기능인 바로 권한 관리 기능입니다.
iOS에서는 카메라 접근 권한을 사용자에게 요청을 해야 하기 때문에 이 기능을 CameraModel에 추가해보겠습니다.
func checkPermissions() async {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .notDetermined:
let granted = await AVCaptureDevice.requestAccess(for: .video)
if granted {
await startSession()
}
case .restricted:
print("카메라 접근이 제한되었습니다.")
case .authorized:
await startSession()
default:
print("권한이 거부되었습니다.")
}
}
private func startSession() async {
if !session.isRunning {
await Task.detached {
self.session.startRunning()
}.value
}
}
사진 촬영 기능
func capturePhoto(completion: @escaping (UIImage) -> Void) {
photoCompletion = completion
let settings = AVCapturePhotoSettings()
output.capturePhoto(with: settings, delegate: self)
}
// AVCapturePhotoCaptureDelegate 구현
extension CameraModel: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) {
Task { @MainActor in
self.photoCompletion?(image)
}
}
}
}
마지막으로 사진 촬영 기능을 완성해주었습니다.
개선 사항
추가로 구현할 수 있는 개선 사항으로는 아래처럼 정리할 수 있는데 아직은 아래 기능까지 구현하려면 공부를 더 해야 해서 완성이 되면 다시 포스팅을 해보도록 하겠습니다!
- 플래시 제어 기능
- 카메라 설정 (밝기, 대비 등)
- 필터 적용 기능
- 촬영된 사진 QR 공유 기능
오늘은 여기까지 :)
감사합니다.
잘못된 내용이 있거나 더 좋은 내용 피드백은 언제나 환영합니다!
궁금하신 부분은 댓글로 질문 부탁드립니다!
'Apple > SwiftUI' 카테고리의 다른 글
[SwiftUI] ProgressView 생성하기 (15) | 2025.01.15 |
---|---|
[SwiftUI] 애니메이션과 전환 간단하게 알아보기 (8) | 2024.10.02 |
[SwiftUI] SwiftUI와 UIKit 통합하기 (2/2) (2) | 2024.09.21 |
[SwiftUI] SwiftUI와 UIKit 통합하기 (1/2) (5) | 2024.09.14 |
[SwiftUI] @AppStorage와 @SceneStorage 프로퍼티 래퍼 이해하기 (6) | 2024.09.11 |