Basic Usage
Step-by-step guide to integrate CameraView in your app and capture biometric data.
Before You Begin
Before starting this guide, complete Getting Started. Petnow API key issuance and SPM package installation are required.
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.
Create CameraViewModel
Set up the ViewModel with species and capture purpose.
Initialize Camera
Validate the license and connect the capture session.
Handle Capture Results
Process success/failure callbacks.
Error Handling
Handle initialization errors correctly.
Additional Features
Use progress/status observation, camera switching, and resume detection.
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 {
@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
captureSessionId Required
You must pass a captureSessionId obtained from your server when initializing the camera. See Getting Started for session creation.
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: false // deprecated: always false
),
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 // deprecated: always false
}initialPosition - Initial camera position
.back // Rear camera (recommended)
.front // Front cameracaptureSessionId - Capture session ID
let captureSessionId: UUID // Session ID created via server APIcallback - 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.
Step 3: Handle Capture Results
In Step 2, CameraResult is delivered through the initializeCamera() callback. Implement the callback body.
try await cameraViewModel.initializeCamera(
licenseInfo: licenseInfo,
initialPosition: .back,
captureSessionId: sessionId
) { result in
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: Retry or dismiss the screen
showRetryOrCancelAlert()
}
}Server Session Management on Failure
When you receive CameraResult.fail, the server capture session is still open.
- Retry: Call
cameraViewModel.startDetection()to restart capture within the same session - Close: Dismiss the camera screen
If you close without retrying, the session may appear as "processing" in Petify Console. The server automatically terminates the session (ABORTED) after approximately 5 minutes.
// Example: retry/close handling on Fail
private func showRetryOrCancelAlert() {
// SwiftUI Alert or UIKit AlertController
// "Retry" → cameraViewModel.startDetection()
// "Close" → dismiss()
}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: Additional Features
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
}Front/Rear Camera Switching
if cameraViewModel.isSwitchButtonEnabled {
cameraViewModel.switchCamera()
}If isSwitchButtonEnabled is false, the device doesn't support a front camera or the switch isn't ready yet.
Resume Detection
You can restart Detection from the beginning within the current session.
cameraViewModel.startDetection() This method restarts detection from the beginning.
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: false),
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: false)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. isDebugMode (Deprecated)
isDebugMode is deprecated. Always pass false.
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. Screen freezes after capture failure
A. If you don't handle the UI after receiving CameraResult.fail, the camera screen remains in a frozen state. You must either call startDetection() to retry, or dismiss() to navigate back. See Step 3 for details.
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