Petnow LogoPetnow

Basic Usage

A step-by-step guide to integrating CameraView into your app and capturing biometric data.


Before You Start

Complete Getting Started before following this guide. Issuing a Petnow API key and installing the SPM package are required.

This guide walks you step by step through integrating pet nose-print/face capture into your app using CameraView and CameraController, the core components of the PetnowUI module.

Create a CameraController

Create the controller with your capture settings (DetectionConfiguration) and license (LicenseInfo).

Initialize the Camera

Connect the capture session and start detection with initializeCamera().

Handle Capture Results

Handle the success/failure callbacks.

Handle Errors

Handle initialization errors correctly.

Additional Features

Observe progress/status, switch cameras, and re-run detection.


Step 1: Create a CameraController

CameraController is the core object that manages the camera session and detection logic. You pass the capture settings and API key to the initializer, and in SwiftUI you create it as a @StateObject.

import SwiftUI
import PetnowUI

struct PetCameraView: View {
    @StateObject private var controller: CameraController 
    private let captureSessionId: UUID  // Session ID received from the server

    init(captureSessionId: UUID) {
        self.captureSessionId = captureSessionId
        _controller = StateObject(wrappedValue: CameraController( 
            configuration: DetectionConfiguration( 
                species: .dog,                    // .dog or .cat
                purpose: .petProfileRegistration  // Capture purpose
            ), 
            licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY", isDebugMode: false) 
        )) 
    }

    var body: some View {
        // Display CameraView in Step 2
        CameraView(controller: controller)
            .task { await initializeCamera() }
    }
}

Understanding the Parameters

DetectionConfiguration - Capture settings

public struct DetectionConfiguration {
    public let species: Species              // .dog / .cat
    public let purpose: DetectionPurpose     // Capture purpose
    public let enableFakeDetection: Bool     // Fake (photo/video) detection, default false
    public let difficultyMode: DifficultyMode?  // Difficulty, default nil (server/default value)
}

species - Pet type

public enum Species {
    case dog  // Dog nose-print capture
    case cat  // Cat face capture
}

purpose - Capture purpose (DetectionPurpose)

public enum DetectionPurpose {
    case petProfileRegistration  // Profile registration (requires multiple images)
    case petVerification         // Verification
    case petIdentification       // Search/identification
}

Differences by Capture Purpose

The number of required images varies depending on the purpose:

  • petProfileRegistration: Requires multiple images
  • petVerification, petIdentification: Minimum number of images for fast verification/search

Step 2: Initialize the Camera

captureSessionId Is Required

You must pass the captureSessionId issued by the server when initializing the camera. See Getting Started for how to create a session.

Call initializeCamera() to initialize the camera and start detection. Since the license was already passed in the Step 1 initializer, you do not pass it here. It is a good idea to display a loading screen until initialization completes.

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", isDebugMode: false)
        ))
    }

    var body: some View {
        ZStack {
            // Display a loading screen while initializing
            if controller.isInitialized {
                CameraView(controller: controller)
            } else {
                CameraLoadingView()
            }
        }
        .task { await initializeCamera() }
    }

    private func initializeCamera() async {
        do {
            try await controller.initializeCamera( 
                initialPosition: .back,          // Use the rear camera
                captureSessionId: captureSessionId  // Session ID created on the server
            ) { result in
                // Callback that handles the capture result (implemented in Step 3)
                print("Capture complete: \(result)")
            }
            // When await returns, isInitialized becomes true and the loading screen disappears
        } catch {
            print("Camera initialization failed: \(error.localizedDescription)")
        }
    }
}

initializeCamera Parameters

initialPosition - Initial camera position

.back   // Rear camera (recommended)
.front  // Front camera

captureSessionId - Capture session ID

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

callback - Callback function that receives the capture result

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

initializeCamera() is an asynchronous function, so it must be called with await. In SwiftUI, using the .task modifier is recommended. When the function finishes, camera initialization is complete, and the isInitialized property of CameraController is also set to true.

The previous approach of passing the license via initializeCamera(licenseInfo:…) is deprecated. As shown above, inject licenseInfo into the initializer and call initializeCamera without the license argument.


Step 3: Handle Capture Results

In Step 2, CameraResult is delivered through the initializeCamera() callback. Now you implement the inside of this callback.

try await controller.initializeCamera(
    initialPosition: .back,
    captureSessionId: captureSessionId
) { result in
    switch result { 
    case let .success(fingerprintImages, appearanceImages):
        // Success: use the image URL arrays
        print("Nose-print: \(fingerprintImages.count) images")
        print("Appearance: \(appearanceImages.count) images")

        // Upload to the server or move to the 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's capture session is still open.

  • Retry: Call controller.startDetection() to restart capture within the same session
  • Exit: Close the camera screen and return to the previous screen

If you close the screen without retrying, the server automatically terminates the session (ABORTED) after about 5 minutes.

CameraResult Type

public enum CameraResult {
    case success(
        fingerprintImages: [URL],  // Biometric recognition images (file:// URL)
        appearanceImages: [URL]    // Appearance images (file:// URL)
    )
    case fail  // Capture failed
}

Using the Image URLs

Captured images are stored in the app's temporary directory as file:// URLs.

// Convert an image to UIImage
if let image = UIImage(contentsOfFile: fingerprintImages[0].path) {
    imageView.image = image
}

// Convert to Data and upload to the server
if let imageData = try? Data(contentsOf: fingerprintImages[0]) {
    await uploadToServer(imageData)
}

Image Storage Location

Captured images are stored in the app's temporary directory. If needed, copy them to another location, or delete them after uploading to the server.


Step 4: Handle Errors

initializeCamera() can throw various errors. Handle each error appropriately.

private func initializeCamera() async {
    do {
        try await controller.initializeCamera(
            initialPosition: .back,
            captureSessionId: captureSessionId
        ) { result in /* ... */ }
    } 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 is required.\nPlease grant the permission in Settings.")
        showSettingsAlert()

    } catch {
        // Other errors
        showError("Camera initialization failed: \(error.localizedDescription)")
    }
}

private func showSettingsAlert() {
    if let url = URL(string: UIApplication.openSettingsURLString) {
        UIApplication.shared.open(url)
    }
}

PetnowUIError Type (Main Cases)

public enum PetnowUIError: Error {
    case invalidLicense(underlyingError: Error)  // API key error
    case notInitialized                          // Used before initialization
    case permissionDenied(message: String)       // Permission denied
    // There are additional network/response-related cases.
}

Handling Permission Errors Is Required

When a permissionDenied error occurs, you must direct the user to the Settings app. Otherwise, the user will not be able to use the camera.


Step 5: Additional Features

CameraController exposes the real-time capture state through @Published properties. You can use these to build a custom UI.

Key @Published Properties

// Representative detection status (updated about once per second)
@Published public var detectionStatus: DetectionStatus

// Progress (0–100, Int)
@Published public var currentDetectionProgress: Int

// Detected region (normalized coordinates 0.0–1.0)
@Published public var detectedObjectNormalizedRect: CGRect?

// Nose/face BoundingBox
@Published public var detectionResult: DetectionResult?

// Whether the camera switch button is enabled
@Published public var isSwitchButtonEnabled: Bool

// Whether initialization is complete
@Published public var isInitialized: Bool

CameraController does not expose the camera permission status or the current camera orientation as public properties. Check permissions directly in your app with AVCaptureDevice (see Best Practices below).

DetectionStatus Type

public enum DetectionStatus {
    case noObject                           // No target detected
    case processing                         // Detection in progress
    case detected                           // Detection successful
    case failed(reason: DetectionFailureReason)  // Failed (includes reason)
    case finished                           // Capture complete
}

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
}

Example of mapping the status to a UI message:

switch controller.detectionStatus {
case .noObject:   statusLabel.text = "Please fit your pet into the frame"
case .processing: statusLabel.text = "Detecting..."
case .detected:   statusLabel.text = "Great! Hold still"
case .finished:   statusLabel.text = "Capture complete"
case .failed(let reason):
    statusLabel.text = (reason == .tooClose) ? "Please move back a little" : "Please adjust the pose"
}

Switching Between Front and Rear Cameras

if controller.isSwitchButtonEnabled {
    controller.switchCamera() 
}

When isSwitchButtonEnabled is false, the device does not support a front camera, or the switch is not ready yet.

Re-running / Pausing / Resuming Detection

controller.startDetection()   // Restart from the beginning (re-capture)
controller.pauseDetection()   // Pause detection only
controller.resumeDetection()  // Resume from where it was paused

Using It in UIKit

In a UIKit app, integrate CameraView using a UIHostingController. CameraView(controller:) is of type 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()

        // Create the CameraController (settings + license)
        controller = CameraController(
            configuration: DetectionConfiguration(species: .dog, purpose: .petProfileRegistration),
            licenseInfo: LicenseInfo(apiKey: "YOUR_API_KEY", isDebugMode: false)
        )

        // Wrap CameraView in a 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) }
                }
            } catch {
                print("Initialization failed: \(error)")
            }
        }
    }

    private func handleCameraResult(_ result: CameraResult) {
        switch result {
        case let .success(fingerprints, appearances):
            print("Capture succeeded: \(fingerprints.count) images")
        case .fail:
            print("Capture failed")
        }
    }

    deinit {
        controller?.finalizeCamera()
    }
}

Notes for UIKit Usage

The callback may be invoked on a background thread, so always perform UI updates on the main thread.


Best Practices

1. Manage Your API Key Securely

// 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
CameraView(controller: controller)
    .onDisappear { controller.finalizeCamera() }

// UIKit
deinit { controller?.finalizeCamera() }

3. Check Permissions in Advance (Optional)

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

4. isDebugMode (Deprecated)

isDebugMode is deprecated. Always pass false.


Troubleshooting

Q. The camera won't initialize

A. Check the following:

  1. Verify that the API key is correct
  2. Whether NSCameraUsageDescription has been added to Info.plist
  3. Test on a real device (the simulator does not support the camera)

Q. Capture never completes

A. Try the following:

  1. Capture in a bright location
  2. Maintain an appropriate distance between the camera and the pet (30–50 cm)
  3. Capture steadily so the pet does not move

Q. After a capture failure, the screen stays frozen

A. If you don't handle the UI after receiving CameraResult.fail, the camera screen stays frozen. Be sure to implement handling that either retries with startDetection() or returns to the previous screen by calling dismiss().

Q. How do I use the image URLs?

A. Captured images are stored in the temporary directory as file:// URLs:

case let .success(fingerprintImages, appearanceImages):
    if let firstImage = UIImage(contentsOfFile: fingerprintImages[0].path) {
        // Use the image
    }
    if let imageData = try? Data(contentsOf: fingerprintImages[0]) {
        // Use the Data (server upload, etc.)
    }

Next Steps

Once you've learned the basics, refer to the following documents:

On this page