Petnow LogoPetnow

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

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 camera

captureSessionId - Capture session ID

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

How to Create captureSessionId

You must create a session by calling the server's createCaptureSession API before camera initialization:

  1. Request session creation from server (POST /api/capture-sessions)
  2. Save the captureSessionId from the response
  3. 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) -> 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.

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

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:

  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. 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