기본 사용법
CameraView를 앱에 통합하고 생체 데이터를 캡처하는 단계별 가이드.
시작하기 전에
이 가이드를 시작하기 전에 시작하기를 완료하세요. Petnow API 키 발급 및 SPM 패키지 설치가 필수입니다.
이 가이드에서는 PetnowUI 모듈의 핵심 컴포넌트인 CameraView와 CameraController를 사용하여 반려동물 코무늬/얼굴 촬영 기능을 앱에 통합하는 방법을 단계별로 안내합니다.
CameraController 생성
촬영 설정(DetectionConfiguration)과 라이선스(LicenseInfo)로 컨트롤러를 생성합니다.
카메라 초기화
initializeCamera()로 캡처 세션을 연결하고 탐지를 시작합니다.
촬영 결과 처리
성공/실패 콜백을 처리합니다.
에러 처리
초기화 에러를 올바르게 처리합니다.
추가 기능
진행률/상태 관찰, 카메라 전환, Detection 재실행을 사용합니다.
Step 1: CameraController 생성
CameraController는 카메라 세션과 탐지 로직을 관리하는 핵심 객체입니다. 촬영 설정과 API 키를 생성자에 전달하며, SwiftUI에서는 @StateObject로 생성합니다.
import SwiftUI
import PetnowUI
struct PetCameraView: View {
@StateObject private var controller: CameraController
private let captureSessionId: UUID // 서버에서 받은 세션 ID
init(captureSessionId: UUID) {
self.captureSessionId = captureSessionId
_controller = StateObject(wrappedValue: CameraController(
configuration: DetectionConfiguration(
species: .dog, // .dog 또는 .cat
purpose: .petProfileRegistration // 촬영 목적
),
licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY")
))
}
var body: some View {
// Step 2에서 CameraView를 표시합니다
CameraView(controller: controller)
.task { await initializeCamera() }
}
}파라미터 이해하기
DetectionConfiguration - 촬영 설정
public struct DetectionConfiguration {
public let species: Species // .dog / .cat
public let purpose: DetectionPurpose // 촬영 목적
public let enableFakeDetection: Bool // 가짜(사진·영상) 탐지, 기본 false
public let difficultyMode: DifficultyMode? // 난이도, 기본 nil(서버/기본값)
}species - 반려동물 종류
public enum Species {
case dog // 강아지 코무늬 촬영
case cat // 고양이 얼굴 촬영
}purpose - 촬영 목적 (DetectionPurpose)
public enum DetectionPurpose {
case petProfileRegistration // 프로필 등록 (여러 장 필요)
case petVerification // 인증
case petIdentification // 검색/식별
}촬영 목적에 따른 차이
목적에 따라 필요한 이미지 수가 다릅니다:
petProfileRegistration: 여러 장의 이미지 필요petVerification,petIdentification: 빠른 인증/검색을 위한 최소 장 수
Step 2: 카메라 초기화
captureSessionId 전달 필수
카메라 초기화 시 서버에서 발급받은 captureSessionId를 전달해야 합니다. 시작하기에서 세션 생성 방법을 확인하세요.
initializeCamera()를 호출하여 카메라를 초기화하고 탐지를 시작합니다. 라이선스는 Step 1의 생성자에서 이미 전달했으므로 여기서는 전달하지 않습니다. 초기화가 완료될 때까지 로딩 화면을 표시하는 것이 좋습니다.
import SwiftUI
import PetnowUI
struct CameraScreenView: View {
@StateObject private var controller: CameraController
private let captureSessionId: UUID
init(captureSessionId: UUID) {
self.captureSessionId = captureSessionId
_controller = StateObject(wrappedValue: CameraController(
configuration: DetectionConfiguration(species: .dog, purpose: .petProfileRegistration),
licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY")
))
}
var body: some View {
ZStack {
// 초기화 중일 때 로딩 화면 표시
if controller.isInitialized {
CameraView(controller: controller)
} else {
ProgressView() // 직접 만든 로딩 뷰 (SDK 제공 아님)
}
}
.task { await initializeCamera() }
}
private func initializeCamera() async {
do {
try await controller.initializeCamera(
initialPosition: .back, // 후면 카메라 사용
captureSessionId: captureSessionId // 서버에서 생성된 세션 ID
) { result in
// 촬영 결과를 처리하는 콜백 (Step 3에서 구현)
print("촬영 완료: \(result)")
}
controller.startDetection()
// await가 반환되면 isInitialized가 true가 되어 로딩 화면이 사라집니다
} catch {
print("카메라 초기화 실패: \(error.localizedDescription)")
}
}
}initializeCamera 파라미터
initialPosition - 초기 카메라 위치
.back // 후면 카메라 (권장)
.front // 전면 카메라captureSessionId - 캡처 세션 ID
let captureSessionId: UUID // 서버 API로 생성된 세션 IDcallback - 촬영 결과를 받는 콜백 함수
typealias CameraResultCallback = (_ result: CameraResult) -> VoidinitializeCamera()는 비동기 함수이므로 반드시 await와 함께 호출해야 합니다. SwiftUI에서는 .task modifier 사용이 권장됩니다. 함수가 끝나면 카메라 초기화가 완료된 것이며, CameraController의 isInitialized 프로퍼티도 true로 설정됩니다.
라이선스를 initializeCamera(licenseInfo:…)로 전달하던 이전 방식은 deprecated입니다. 위처럼 생성자에 licenseInfo를 주입하고 initializeCamera는 license 인자 없이 호출하세요.
Step 3: 촬영 결과 처리
Step 2에서 initializeCamera()의 콜백으로 CameraResult가 전달됩니다. 이 콜백 내부를 구현합니다.
콜백은 백그라운드 스레드에서 호출될 수 있습니다. @State/@Published 갱신이나 화면 전환 등 UI 작업은 메인 스레드에서 수행하세요(SwiftUI도 동일). 예: await MainActor.run { ... } 또는 DispatchQueue.main.async { ... }.
try await controller.initializeCamera(
initialPosition: .back,
captureSessionId: captureSessionId
) { result in
switch result {
case let .success(fingerprintImages, appearanceImages):
// 성공: 이미지 URL 배열 사용
print("코무늬: \(fingerprintImages.count)장")
print("외형: \(appearanceImages.count)장")
// 서버에 업로드하거나 다음 화면으로 이동
uploadImages(fingerprintImages, appearanceImages)
case .fail:
// 실패: 재시도하거나 화면 dismiss
showRetryOrCancelAlert()
}
}실패 시 서버 세션 관리
CameraResult.fail을 수신하면 서버의 캡처 세션은 아직 열린 상태입니다.
- 재시도:
controller.startDetection()을 호출하여 동일 세션에서 촬영을 다시 시작 - 종료: 카메라 화면을 닫고 이전 화면으로 돌아감
재시도하지 않고 화면을 닫으면 서버가 약 5분 후 자동으로 세션을 종료(ABORTED) 처리합니다.
CameraResult 타입
public enum CameraResult {
case success(
fingerprintImages: [URL], // 생체 인식용 이미지 (file:// URL)
appearanceImages: [URL] // 외형 이미지 (file:// URL)
)
case fail // 촬영 실패
}이미지 URL 사용하기
촬영된 이미지는 앱의 임시 디렉토리에 file:// URL로 저장됩니다.
// 이미지를 UIImage로 변환
if let image = UIImage(contentsOfFile: fingerprintImages[0].path) {
imageView.image = image
}
// Data로 변환하여 서버에 업로드
if let imageData = try? Data(contentsOf: fingerprintImages[0]) {
await uploadToServer(imageData)
}이미지 저장 위치
촬영된 이미지는 앱의 임시 디렉토리에 저장됩니다. 필요시 다른 위치로 복사하거나 서버에 업로드한 후 삭제하세요.
Step 4: 에러 처리
initializeCamera()는 다양한 에러를 throw할 수 있습니다. 각 에러에 맞는 처리를 해주세요.
private func initializeCamera() async {
do {
try await controller.initializeCamera(
initialPosition: .back,
captureSessionId: captureSessionId
) { result in /* ... */ }
} catch PetnowUIError.invalidLicense(let underlyingError) {
// 라이선스 검증 실패
showError("유효하지 않은 API 키입니다.\n\(underlyingError.localizedDescription)")
} catch PetnowUIError.permissionDenied(let message) {
// 카메라 권한 거부
showError("카메라 권한이 필요합니다.\n설정에서 권한을 허용해주세요.")
showSettingsAlert()
} catch {
// 기타 에러
showError("카메라 초기화 실패: \(error.localizedDescription)")
}
}
private func showSettingsAlert() {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}PetnowUIError 타입 (주요 케이스)
public enum PetnowUIError: Error {
case invalidLicense(underlyingError: Error) // API 키 오류
case notInitialized // 초기화 전 사용
case permissionDenied(message: String) // 권한 거부
// 이 외 네트워크/응답 관련 케이스가 있습니다.
}권한 에러 처리 필수
permissionDenied 에러가 발생하면 사용자를 설정 앱으로 안내해야 합니다. 그렇지 않으면 사용자가 카메라를 사용할 수 없습니다.
Step 5: 추가 기능
CameraController는 실시간 촬영 상태를 @Published 프로퍼티로 제공합니다. 이를 활용하여 커스텀 UI를 만들 수 있습니다.
주요 @Published 프로퍼티
// 대표 탐지 상태 (약 1초마다 업데이트)
@Published public var detectionStatus: DetectionStatus
// 진행률 (0~100, Int)
@Published public var currentDetectionProgress: Int
// 탐지된 영역 (정규화된 좌표 0.0~1.0)
@Published public var detectedObjectNormalizedRect: CGRect?
// 코/얼굴 BoundingBox
@Published public var detectionResult: DetectionResult?
// 카메라 전환 버튼 활성화 여부
@Published public var isSwitchButtonEnabled: Bool
// 초기화 완료 여부
@Published public var isInitialized: Bool카메라 권한 상태나 현재 카메라 방향은 CameraController가 공개 프로퍼티로 제공하지 않습니다. 권한은 앱에서 AVCaptureDevice로 직접 확인하세요(아래 모범 사례 참고).
DetectionStatus 타입
public enum DetectionStatus {
case noObject // 대상 미탐지
case processing // 탐지 진행 중
case detected // 탐지 성공
case failed(reason: DetectionFailureReason) // 실패 (사유 포함)
case finished // 촬영 완료
}
public enum DetectionFailureReason {
case error, tooBright, tooDark, noseNotFound
case notFrontFace, notFrontCatFaceHor, notFrontCatFaceTop, notFrontCatFaceBottom
case tooFarAway, tooClose, notFrontNoseTop, tooBlurred
case shadowDetected, glareDetected, motionBlurDetected, defocusedBlurDetected
case notFrontNose, furDetected, humanFaceDetected, fakeDetected, unexpected
}상태를 UI 메시지로 매핑하는 예시:
switch controller.detectionStatus {
case .noObject: statusLabel.text = "반려동물을 프레임에 맞춰주세요"
case .processing: statusLabel.text = "탐지 중..."
case .detected: statusLabel.text = "좋아요! 그대로 유지하세요"
case .finished: statusLabel.text = "촬영 완료"
case .failed(let reason):
statusLabel.text = (reason == .tooClose) ? "조금 멀리 떨어져주세요" : "자세를 조정해주세요"
}전/후면 카메라 전환
if controller.isSwitchButtonEnabled {
controller.switchCamera()
}isSwitchButtonEnabled가 false인 경우 해당 기기에서 전면 카메라를 지원하지 않거나, 전환 준비가 안 된 상태입니다.
Detection 재실행 / 일시정지 / 재개
controller.startDetection() // 처음부터 재시작 (재촬영)
controller.pauseDetection() // 탐지 일시정지
controller.resumeDetection() // 일시정지 시점부터 재개UIKit에서 사용하기
UIKit 앱에서는 UIHostingController를 사용하여 CameraView를 통합합니다. CameraView(controller:)는 CameraView<EmptyView> 타입입니다.
class PetCameraViewController: UIViewController {
private var controller: CameraController!
private var hostingController: UIHostingController<CameraView<EmptyView>>?
private let captureSessionId: UUID
init(captureSessionId: UUID) {
self.captureSessionId = captureSessionId
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
// CameraController 생성 (설정 + 라이선스)
controller = CameraController(
configuration: DetectionConfiguration(species: .dog, purpose: .petProfileRegistration),
licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY")
)
// CameraView를 UIHostingController로 래핑
let cameraView = CameraView(controller: controller)
let hosting = UIHostingController(rootView: cameraView)
hostingController = hosting
addChild(hosting)
view.addSubview(hosting.view)
hosting.view.frame = view.bounds
hosting.didMove(toParent: self)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
Task {
do {
try await controller.initializeCamera(
initialPosition: .back,
captureSessionId: captureSessionId
) { [weak self] result in
DispatchQueue.main.async { self?.handleCameraResult(result) }
}
controller.startDetection()
} catch {
print("초기화 실패: \(error)")
}
}
}
private func handleCameraResult(_ result: CameraResult) {
switch result {
case let .success(fingerprints, appearances):
print("촬영 성공: \(fingerprints.count)장")
case .fail:
print("촬영 실패")
}
}
deinit {
controller?.finalizeCamera()
}
}UIKit 사용 시 주의사항
콜백은 백그라운드 스레드에서 호출될 수 있으므로 UI 업데이트는 반드시 메인 스레드에서 수행하세요.
모범 사례
1. API 키 안전하게 관리하기
// 나쁜 예: 코드에 하드코딩
let apiKey = "sk_live_abc123..."
// 좋은 예: Info.plist 또는 환경변수 사용
extension Bundle {
var petnowAPIKey: String {
guard let key = infoDictionary?["PETNOW_API_KEY"] as? String, !key.isEmpty else {
fatalError("PETNOW_API_KEY not configured in Info.plist")
}
return key
}
}
// 사용
LicenseInfo(apiKey: Bundle.main.petnowAPIKey)2. 리소스 정리하기
// SwiftUI
CameraView(controller: controller)
.onDisappear { controller.finalizeCamera() }
// UIKit
deinit { controller?.finalizeCamera() }3. 권한 사전 확인하기 (선택사항)
import AVFoundation
func checkCameraPermission() async -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: return true
case .notDetermined: return await AVCaptureDevice.requestAccess(for: .video)
case .denied, .restricted: return false
@unknown default: return false
}
}문제 해결
Q. 카메라가 초기화되지 않아요
A. 다음을 확인하세요:
- API 키가 올바른지 확인
Info.plist에NSCameraUsageDescription추가 여부- 실제 디바이스에서 테스트 (시뮬레이터는 카메라 미지원)
Q. 촬영이 완료되지 않아요
A. 다음을 시도하세요:
- 밝은 곳에서 촬영
- 카메라와 반려동물 사이 적정 거리 유지 (30-50cm)
- 반려동물이 움직이지 않도록 안정적으로 촬영
Q. 촬영 실패 후 화면이 멈춤 상태로 남아요
A. CameraResult.fail을 수신한 후 UI 처리를 하지 않으면 카메라 화면이 멈춤 상태로 남습니다. 반드시 startDetection()으로 재시도하거나, 화면을 dismiss()하여 이전 화면으로 돌아가는 처리를 구현하세요.
Q. 이미지 URL을 어떻게 사용하나요?
A. 촬영된 이미지는 임시 디렉토리에 file:// URL로 저장됩니다:
case let .success(fingerprintImages, appearanceImages):
if let firstImage = UIImage(contentsOfFile: fingerprintImages[0].path) {
// 이미지 사용
}
if let imageData = try? Data(contentsOf: fingerprintImages[0]) {
// Data 사용 (서버 업로드 등)
}다음 단계
기본 사용법을 익혔다면 다음 문서를 참고하세요: