Petnow LogoPetnow

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 images
  • forSearch, 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 camera

captureSessionId - Capture session ID

let captureSessionId: UUID  // Session ID created via server API

callback - Callback function to receive capture results

typealias CameraResultCallback = (_ result: CameraResult) -> Void

initializeCamera() 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: Bool

DetectionStatus 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:

  1. Verify API key is correct
  2. Check if NSCameraUsageDescription is added to Info.plist
  3. Test on actual device (simulator doesn't support camera)

Q. Capture doesn't complete

A. Try the following:

  1. Capture in a well-lit area
  2. Maintain proper distance between camera and pet (30-50cm)
  3. 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:

On this page