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 imagespetVerification,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 cameracaptureSessionId - Capture session ID
let captureSessionId: UUID // Session ID created via the server APIcallback - Callback function that receives the capture result
typealias CameraResultCallback = (_ result: CameraResult) -> VoidinitializeCamera() 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: BoolCameraController 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 pausedUsing 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:
- Verify that the API key is correct
- Whether
NSCameraUsageDescriptionhas been added toInfo.plist - Test on a real device (the simulator does not support the camera)
Q. Capture never completes
A. Try the following:
- Capture in a bright location
- Maintain an appropriate distance between the camera and the pet (30–50 cm)
- 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:
- Customization - Overlay customization
- Sound Guide - Playing sounds