Basic Usage
Step-by-step guide to integrate CameraView in your app and capture biometric data.
Before You Begin
This guide provides step-by-step instructions for integrating pet nose/face capture functionality into your app using CameraView and CameraViewModel, the core components of the PetnowUI module.
Prerequisites
- Xcode 16.0 or later
- Project targeting iOS 16.0 or later
- Petnow API key (Contact support@petnow.io if you haven't received one)
Learning Objectives
After completing this guide, you will be able to:
- Integrate CameraView into your app
- Process capture results
- Handle error scenarios correctly
- Reflect capture status in the UI
Step 1: Create CameraViewModel
CameraViewModel is the core object that manages the camera session and detection logic. In SwiftUI, create it as a @StateObject.
import SwiftUI
import PetnowUI
struct PetCameraView: View {
// Create CameraViewModel as @StateObject
@StateObject private var cameraViewModel: CameraViewModel
init(species: Species) {
_cameraViewModel = StateObject(wrappedValue: CameraViewModel(
species: species, // .dog or .cat
cameraPurpose: .forRegisterFromProfile // Capture purpose
))
}
var body: some View {
// TODO: Implement navigation to camera screen
}
}Understanding Parameters
species - Pet species
public enum Species {
case dog // Dog nose capture
case cat // Cat face capture
}cameraPurpose - Capture purpose
public enum CameraPurpose {
case forRegisterFromProfile // Profile registration
case appendNose // Add nose to existing profile (coming soon)
case forSearch // Search/identification
case forWitness // Report/verification
}Differences by Purpose
The number of required images varies by purpose:
forRegisterFromProfile: Requires multiple imagesforSearch,forWitness: Uses minimum images for quick search
About appendNose
appendNose is for adding additional nose prints to an already registered pet. Currently not supported by the API, but planned for future release.
Step 2: Initialize Camera
Call the initializeCamera() method to initialize the camera and validate the license. It's recommended to display a loading screen until initialization is complete.
import SwiftUI
import PetnowUI
struct CameraScreenView: View {
@ObservedObject var cameraViewModel: CameraViewModel
@Environment(\.dismiss) private var dismiss
@State private var captureSessionId: UUID?
var body: some View {
ZStack {
// Show loading screen during initialization
if cameraViewModel.isInitialized {
CameraView(viewModel: cameraViewModel)
} else {
CameraLoadingView()
}
}
.task {
await initializeCamera()
}
}
private func initializeCamera() async {
do {
// 1. Create capture session from server
captureSessionId = try await createCaptureSessionFromServer()
guard let sessionId = captureSessionId else {
print("Failed to create session ID")
dismiss()
return
}
// 2. Initialize camera
try await cameraViewModel.initializeCamera(
licenseInfo: LicenseInfo(
apiKey: "YOUR_API_KEY",
isDebugMode: true // Use Stage environment
),
initialPosition: .back, // Use rear camera
captureSessionId: sessionId // Session ID from server
) { result in
// Callback to process capture result (implement in Step 3)
print("Capture complete: \(result)")
}
// When await returns, isInitialized becomes true and loading screen disappears
} catch {
print("Camera initialization failed: \(error.localizedDescription)")
dismiss()
}
}
// Create capture session from server
private func createCaptureSessionFromServer() async throws -> UUID {
// TODO: Implement actual server API call
return UUID()
}
}
initializeCamera Parameters
licenseInfo - API key and environment settings
public struct LicenseInfo {
let apiKey: String // Petnow API key
let isDebugMode: Bool // true: Stage, false: Production
}initialPosition - Initial camera position
.back // Rear camera (recommended)
.front // Front cameracaptureSessionId - Capture session ID
let captureSessionId: UUID // Session ID created via server APIHow to Create captureSessionId
You must create a session by calling the server's createCaptureSession API before camera initialization:
- Request session creation from server (
POST /api/capture-sessions) - Save the
captureSessionIdfrom the response - Pass this ID to
initializeCamera()
This session ID is used to track the capture process and analyze metrics in Petify Console.
callback - Callback function to receive capture results
typealias CameraResultCallback = (_ result: CameraResult) -> VoidinitializeCamera() is an async function and must be called with await.
Using the .task modifier is recommended in SwiftUI.
When the function completes, camera initialization is finished, and the CameraViewModel's isInitialized property is also set to true.
Screen Transition Method
Capture can take from 5 seconds to over 1 minute. This guide uses a standard screen transition with NavigationLink. Modal methods like .fullScreenCover or .sheet are not recommended as users can easily cancel them.
Step 3: Process Capture Results
When capture is complete, CameraResult is passed to the initializeCamera() callback. You can process this result to use the images or handle errors.
struct CameraScreenView: View {
@ObservedObject var cameraViewModel: CameraViewModel
@Environment(\.dismiss) private var dismiss
let onResult: (CameraResult) -> Void
@State private var captureSessionId: UUID?
var body: some View {
ZStack {
if cameraViewModel.isInitialized {
CameraView(viewModel: cameraViewModel)
} else {
CameraLoadingView()
}
}
.task {
await initializeCamera()
}
}
private func initializeCamera() async {
do {
captureSessionId = try await createCaptureSessionFromServer()
guard let sessionId = captureSessionId else {
print("Failed to create session ID")
dismiss()
return
}
try await cameraViewModel.initializeCamera(
licenseInfo: LicenseInfo(
apiKey: "YOUR_API_KEY",
isDebugMode: true
),
initialPosition: .back,
captureSessionId: sessionId
) { result in
// Execute callback when capture completes
onResult(result)
dismiss()
}
} catch {
print("Camera initialization failed: \(error)")
dismiss()
}
}
private func createCaptureSessionFromServer() async throws -> UUID {
// TODO: Implement actual server API call
return UUID()
}
}Processing CameraResult
Process the callback result as follows:
// Process result in parent View
private func handleCameraResult(_ result: CameraResult) {
switch result {
case .success(let fingerprintImages, let appearanceImages):
// Success: Use image URL arrays
print("Nose prints: \(fingerprintImages.count) images")
print("Appearance: \(appearanceImages.count) images")
// Upload to server or navigate to next screen
uploadImages(fingerprintImages, appearanceImages)
case .fail:
// Failure: Guide user to retry
showRetryAlert()
}
}CameraResult Type
public enum CameraResult {
case success(
fingerprintImages: [URL], // Nose/fingerprint images (file URLs)
appearanceImages: [URL] // Appearance images (file URLs)
)
case fail // Capture failed
}Using Image URLs
Captured images are saved to the app's temporary directory and can be used as follows:
// Convert image to UIImage
if let image = UIImage(contentsOfFile: fingerprintImages[0].path) {
imageView.image = image
}
// Convert to Data and upload to server
if let imageData = try? Data(contentsOf: fingerprintImages[0]) {
await uploadToServer(imageData)
}Image Storage Location
Captured images are saved to the app's temporary directory. Copy them to another location or upload to server and delete them as needed.
Step 4: Error Handling
initializeCamera() can throw various errors. Handle each error appropriately.
private func initializeCamera() async {
do {
try await cameraViewModel.initializeCamera( /* ... */ ) { /* ... */ }
} catch PetnowUIError.invalidLicense(let underlyingError) {
// License validation failed
showError("Invalid API key.\n\(underlyingError.localizedDescription)")
} catch PetnowUIError.permissionDenied(let message) {
// Camera permission denied
showError("Camera permission required.\nPlease allow permission in Settings.")
showSettingsAlert()
} catch {
// Other errors
showError("Camera initialization failed: \(error.localizedDescription)")
}
showCamera = false
}
private func showError(_ message: String) {
// Display error message
print("❌ \(message)")
}
private func showSettingsAlert() {
// Show alert to navigate to Settings app
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}PetnowUIError Type
public enum PetnowUIError: Error {
case invalidLicense(underlyingError: Error) // API key error
case permissionDenied(message: String) // Permission denied
}Permission Error Handling Required
When permissionDenied error occurs, you must guide the user to the Settings app. Otherwise, the user won't be able to use the camera.
Step 5: Observe Capture Status (Optional)
CameraViewModel provides real-time capture status through @Published properties. You can use these to create custom UI.
Key @Published Properties
// Detection status (updated every 1 second)
@Published public var detectionStatus: DetectionStatus
// Progress (0-100)
@Published public var currentDetectionProgress: Int
// Camera permission status
@Published public var cameraPermissionStatus: AVAuthorizationStatus
// Detected area (normalized coordinates 0.0-1.0)
@Published public var detectedObjectNormalizedRect: CGRect?
// Current camera direction (front, rear)
@Published public var currentCameraPosition: AVCaptureDevice.Position
// Camera switch button enabled state
@Published public var isSwitchButtonEnabled: BoolDetectionStatus Type
public enum DetectionStatus {
case noObject // Object not detected
case processing // Detection in progress
case detected // Detection complete
case failed(DetectionFailureReason) // Failed (with reason)
}
public enum DetectionFailureReason {
case error // Error
case tooBright // Too bright
case tooDark // Too dark
case noseNotFound // Nose detection failed
case notFrontFace // Not front-facing
case notFrontCatFaceHor // Cat face horizontal misalignment
case notFrontCatFaceTop // Cat face tilted too far up
case notFrontCatFaceBottom // Cat face tilted too far down
case tooFarAway // Too far
case tooClose // Too close
case notFrontNoseTop // Nose tilted too far up
case tooBlurred // Blurred
case shadowDetected // Shadow detected
case glareDetected // Glare detected
case motionBlurDetected // Motion blur detected
case defocusedBlurDetected // Defocus blur detected
case notFrontNose // Nose not front-facing
case furDetected // Fur detected
case humanFaceDetected // Human face detected
case fakeDetected // Fake photo detected (e.g., monitor screen)
case unexpected // Unexpected error
}Using in UIKit
In UIKit apps, integrate CameraView using UIHostingController.
class PetCameraViewController: UIViewController {
private var cameraViewModel: CameraViewModel!
private var hostingController: UIHostingController<CameraView>?
override func viewDidLoad() {
super.viewDidLoad()
// Create CameraViewModel
cameraViewModel = CameraViewModel(
species: .dog,
cameraPurpose: .forRegisterFromProfile
)
// Wrap CameraView with UIHostingController
let cameraView = CameraView(viewModel: cameraViewModel)
hostingController = UIHostingController(rootView: cameraView)
// Add as child view controller
guard let hostingController = hostingController else { return }
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.frame = view.bounds
hostingController.didMove(toParent: self)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
initializeCamera()
}
private func initializeCamera() {
Task {
do {
let sessionId = try await createCaptureSessionFromServer()
try await cameraViewModel.initializeCamera(
licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY", isDebugMode: true),
initialPosition: .back,
captureSessionId: sessionId
) { [weak self] result in
DispatchQueue.main.async {
self?.handleCameraResult(result)
}
}
} catch {
print("Initialization failed: \(error)")
}
}
}
private func handleCameraResult(_ result: CameraResult) {
switch result {
case .success(let fingerprints, let appearances):
print("Capture success: \(fingerprints.count) images")
case .fail:
print("Capture failed")
}
}
deinit {
cameraViewModel?.stopDetection()
}
}UIKit Usage Note
Callbacks may be called on a background thread, so UI updates must always be performed on the main thread.
Best Practices
1. Securely Manage API Keys
// Bad: Hardcoded in code
let apiKey = "sk_live_abc123..."
// Good: Use Info.plist or environment variables
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
}
}
// Usage
LicenseInfo(apiKey: Bundle.main.petnowAPIKey, isDebugMode: isDebug)2. Clean Up Resources
// SwiftUI
struct PetCameraView: View {
@StateObject private var cameraViewModel: CameraViewModel
var body: some View {
CameraView(viewModel: cameraViewModel)
.onDisappear {
// Clean up resources when screen disappears
cameraViewModel.stopDetection()
}
}
}
// UIKit
deinit {
cameraViewModel?.stopDetection()
}3. Pre-check Permissions (Optional)
import AVFoundation
func checkCameraPermission() async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
return true
case .notDetermined:
// Request permission
return await AVCaptureDevice.requestAccess(for: .video)
case .denied, .restricted:
// Guide to Settings app
showPermissionAlert()
return false
@unknown default:
return false
}
}
// Check before showing camera
if await checkCameraPermission() {
showCamera = true
} else {
showError("Camera permission required")
}4. Set isDebugMode Based on Build Environment
private var isDebugBuild: Bool {
#if DEBUG
return true
#else
return false
#endif
}
// Usage
LicenseInfo(
apiKey: Bundle.main.petnowAPIKey,
isDebugMode: isDebugBuild
)Troubleshooting
Q. Camera won't initialize
A. Check the following:
- Verify API key is correct
- Check if
NSCameraUsageDescriptionis added toInfo.plist - Test on actual device (simulator doesn't support camera)
Q. Capture doesn't complete
A. Try the following:
- Capture in a well-lit area
- Maintain proper distance between camera and pet (30-50cm)
- Capture steadily while pet is not moving
Q. How do I use image URLs?
A. Captured images are saved to the temporary directory:
case .success(let fingerprintImages, let appearanceImages):
// Read image
if let firstImage = UIImage(contentsOfFile: fingerprintImages[0].path) {
// Use image
}
// Or convert to Data
if let imageData = try? Data(contentsOf: fingerprintImages[0]) {
// Use Data (server upload, etc.)
}For more troubleshooting, see the Troubleshooting documentation.
Next Steps
Once you've mastered basic usage, refer to these documents:
- Customization - Customize overlays
- Sound Guide - Sound playback