Petnow LogoPetnow
iOS SDK

UI Guide

Using Petnow iOS SDK UI components and customizing them.


Overview

The PetnowUI module for iOS provides developers with ready-to-use UI components for capturing pet biometric data. This guide explains how to integrate and use these components in your iOS application, primarily focusing on the SwiftUI CameraView and its associated CameraViewModel.

Key Components

  • CameraView (SwiftUI): A SwiftUI View provided by the PetnowUI module. It displays the camera preview and works with CameraViewModel to handle the pet detection process.
  • CameraViewModel (SwiftUI ObservableObject): An ObservableObject also provided by PetnowUI. It manages the camera session, detection logic, and communicates results and status updates to CameraView or other observing components.

Important Implementation Notes

  • The CameraViewModel now uses an async initialization pattern with built-in license validation for improved security and error handling.
  • CameraView is typically presented modally or embedded within your existing SwiftUI view hierarchy.
  • The CameraViewModel must be initialized with the appropriate PetnowUI.Species and PetnowUI.CameraPurpose. If you need to use a separate PetnowAPIClient for uploads, these UI-specific enums will need to be mapped to their PetnowAPI counterparts when making API calls.

Prerequisites

  • A valid API key for license validation
  • Camera permissions (automatically requested by the framework when calling initializeCamera())
  • If you intend to upload captured images using a separate PetnowAPIClient (from the PetnowAPI module), ensure you pass the correct mapped PetnowAPI.PetSpecies and PetnowAPI.DetectionPurpose with each call.

Enum Mapping for API Calls

When calling PetnowAPIClient methods after capturing images with PetnowUI, you'll need to map the enums from PetnowUI to their PetnowAPI equivalents. Here's a general guideline:

Species Mapping:

PetnowUI.SpeciesPetnowAPI.PetSpecies
PetnowUI.Species.dogPetnowAPI.PetSpecies.DOG
PetnowUI.Species.catPetnowAPI.PetSpecies.CAT

CameraPurpose to DetectionPurpose (for image uploads):

PetnowUI.CameraPurposePetnowAPI.DetectionPurposeNotes
forRegisterFromProfilePetnowAPI.DetectionPurpose.PET_PROFILE_REGISTRATION
forSearchPetnowAPI.DetectionPurpose.PET_VERIFICATIONFor image uploads prior to identification
e.g., identifyPetnowPets)
forWitnessPetnowAPI.DetectionPurpose.PET_VERIFICATIONFor image uploads in witness report related identification scenarios
appendNoseCurrently not supported

It's crucial to pass these mapped PetnowAPI.PetSpecies and PetnowAPI.DetectionPurpose values when calling apiClient.uploadFingerPrints(...) or apiClient.uploadAppearances(...).

Implementation Notes

  • CameraViewModel handles camera permission requests and initialization via the new async initializeCamera() method. While you can optionally call checkAndRequestCameraPermission() separately for nicer permission handling, initializeCamera() will automatically request permissions if needed.
  • Detection logic prioritizes dog noses or cat faces based on the PetnowUI.Species passed to CameraViewModel.
  • Visual feedback (e.g., guiding overlays via DetectionOverlayView, progress indicators) is provided by CameraView based on states published by CameraViewModel.

Using CameraView and CameraViewModel (SwiftUI)

To use the camera functionality, you primarily interact with CameraView and CameraViewModel from the PetnowUI module.

import SwiftUI
import PetnowUI // Import the PetnowUI module
// Import PetnowAPI if you need to use PetnowAPIClient for uploads later
// import PetnowAPI

struct PetBiometricScanView: View {
    @Environment(\\.dismiss) var dismiss

    // Initialize CameraViewModel.
    // Consider where to initialize this; it could be @StateObject if created here,
    // or passed in if created by a parent view or coordinator.
    @StateObject private var cameraViewModel: CameraViewModel

    // Store your PetnowAPIClient instance if you need it for uploads
    // let apiClient: PetnowAPIClient

    @State private var showResultAlert = false
    @State private var alertMessage = ""

    // Example initializer
    // The actual PetnowAPIClient instance would likely be passed in or accessed via @EnvironmentObject
    init(species: PetnowUI.Species, purpose: PetnowUI.CameraPurpose /*, apiClient: PetnowAPIClient */) {
        // Initialize the ViewModel with UI-specific enums
        _cameraViewModel = StateObject(wrappedValue: CameraViewModel(species: species, cameraPurpose: purpose))
        // self.apiClient = apiClient
    }

    var body: some View {
        NavigationView { // Or NavigationStack for iOS 16+
            ZStack {
                CameraView(viewModel: cameraViewModel)
                    .edgesIgnoringSafeArea(.all)

                // Example: Observe detection status from CameraViewModel
                VStack {
                    Text("Status: \\(String(describing: cameraViewModel.detectionStatus))")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.black.opacity(0.5))
                    Text("Progress: \\(cameraViewModel.currentDetectionProgress)%")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.black.opacity(0.5))
                    Spacer()
                }
            }
            .navigationTitle("Pet Scan")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("Cancel") {
                        cameraViewModel.stopDetection() // Stop any ongoing detection
                        dismiss()
                    }
                }
            }
            .task {
                // New async initialization with license validation
                do {
                    try await cameraViewModel.initializeCamera(
                        licenseInfo: LicenseInfo(apiKey: "your-api-key", isDebugMode: false),
                        initialPosition: .back
                    ) { [weak self] cameraResult in
                        await MainActor.run {
                            self?.handleCameraResult(cameraResult)
                        }
                    }
                } catch {
                    alertMessage = "Initialization failed: \\(error.localizedDescription)"
                    showResultAlert = true
                }
            }
            .alert("Scan Result", isPresented: $showResultAlert) {
                Button("OK", role: .cancel) { dismiss() }
            } message: {
                Text(alertMessage)
            }
        }
    }

    private func handleCameraResult(_ result: CameraResult) {
        switch result {
        case .success(let fingerprintImages, let appearanceImages):
            self.alertMessage = "Capture success! Fingerprints: \\(fingerprintImages.count), Appearances: \\(appearanceImages.count). Ready for upload."
            print(alertMessage)
            // Now, you would typically use your `apiClient` instance to upload these images.
            // Example (pseudo-code, actual implementation depends on your API client handling):
            // Task {
            //     // Example: Map PetnowUI enums to PetnowAPI enums
            //     let apiSpecies: PetnowAPI.PetSpecies
            //     switch cameraViewModel.species {
            //     case .dog: apiSpecies = .DOG
            //     case .cat: apiSpecies = .CAT
            //     // default: throw an error or handle appropriately
            //     }
            //
            //     let apiPurpose: PetnowAPI.DetectionPurpose
            //     switch cameraViewModel.cameraPurpose {
            //     case .forRegisterFromProfile: apiPurpose = .PET_PROFILE_REGISTRATION
            //     case .forSearch: apiPurpose = .PET_VERIFICATION // For uploads prior to identification
            //     case .forWitness: apiPurpose = .PET_VERIFICATION // For uploads prior to witness report related identification
            //     // case .appendNose: // Not supported
            //     // default: throw an error or handle appropriately
            //     }
            //
            //     // You might need to convert URLs to Data or handle them as needed by your API client
            //     let fingerprintIds = try await uploadImages(urls: fingerprintImages, type: .fingerprint, apiClient: apiClient, species: apiSpecies, purpose: apiPurpose)
            //     let appearanceIds = try await uploadImages(urls: appearanceImages, type: .appearance, apiClient: apiClient, species: apiSpecies, purpose: apiPurpose)
            //     // Then register pet with these IDs, also passing apiSpecies
            //     // if let petId = try await apiClient.registerPetProfile(fingerPrints: fingerprintIds, appearances: appearanceIds, metadata: \"{}\", species: apiSpecies) {
            //     //    self.alertMessage = \"Registration successful! Pet ID: \\(petId)\"
            //     // }
            // }
        case .fail:
            self.alertMessage = "Capture failed."
            print(alertMessage)
        }
        self.showResultAlert = true
        // Consider dismissing the camera view or navigating elsewhere after handling the result.
    }

    // Example helper for uploads (pseudo-code)
    /*
    private func uploadImages(urls: [URL], type: UploadType, apiClient: PetnowAPIClient, species: PetnowAPI.PetSpecies, purpose: PetnowAPI.DetectionPurpose) async throws -> [String] {
        var uploadedIds: [String] = []
        for url in urls {
            let imageData = try Data(contentsOf: url)
            let biometricId: PetnowAPI.PetBiometricId // Ensure this refers to PetnowAPI.PetBiometricId
            switch type {
            case .fingerprint:
                biometricId = try await apiClient.uploadFingerPrints(
                    fileData: imageData,
                    fileName: url.lastPathComponent,
                    species: species, // Pass mapped PetnowAPI.PetSpecies
                    purpose: purpose  // Pass mapped PetnowAPI.DetectionPurpose
                )
            case .appearance:
                biometricId = try await apiClient.uploadAppearances(
                    fileData: imageData,
                    fileName: url.lastPathComponent,
                    species: species, // Pass mapped PetnowAPI.PetSpecies
                    purpose: purpose  // Pass mapped PetnowAPI.DetectionPurpose
                )
            }
            uploadedIds.append(biometricId.id)
        }
        return uploadedIds
    }
    enum UploadType { case fingerprint, appearance }
    */
}

// Make sure PetnowUI.Species, PetnowUI.CameraPurpose are defined in your PetnowUI module.
// And CameraResult as well.

UIKit Integration

For UIKit-based applications, you can integrate the SwiftUI CameraView using UIHostingController.

  1. Create CameraViewModel: Instantiate CameraViewModel with the desired PetnowUI.Species and PetnowUI.CameraPurpose.
  2. Create CameraView: Initialize CameraView with the CameraViewModel instance.
  3. Host in UIHostingController: Create a UIHostingController(rootView: cameraView).
  4. Present or Embed: Present the UIHostingController modally, or add its view to your UIKit view hierarchy.
  5. Handle Results: The CameraResultCallback provided to cameraViewModel.initializeCamera(...) will still be called. You can handle the results within the hosting UIViewController or delegate them further.
import UIKit
import SwiftUI // For UIHostingController
import PetnowUI
// import PetnowAPI // If using PetnowAPIClient here

class MyCameraHostingViewController: UIViewController {

    var hostingController: UIHostingController<CameraView>?
    var cameraViewModel: CameraViewModel!
    // var apiClient: PetnowAPIClient! // Your PetnowAPIClient instance

    // Call this method to present the camera
    func setupAndPresentCamera(species: PetnowUI.Species, purpose: PetnowUI.CameraPurpose /*, apiClient: PetnowAPIClient */) {
        // self.apiClient = apiClient

        self.cameraViewModel = CameraViewModel(species: species, cameraPurpose: purpose)

        // Initialize SwiftUI CameraView with the ViewModel
        let cameraSwiftUIView = CameraView(viewModel: cameraViewModel)

        // Create a UIHostingController
        self.hostingController = UIHostingController(rootView: cameraSwiftUIView)
        guard let hc = self.hostingController else { return }

        // Add as a child view controller and add its view to the hierarchy
        addChild(hc)
        view.addSubview(hc.view)
        hc.view.frame = view.bounds
        hc.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        hc.didMove(toParent: self)

        // Initialize camera with async method and set up callback
        Task {
            do {
                try await cameraViewModel.initializeCamera(
                    licenseInfo: LicenseInfo(apiKey: "your-api-key", isDebugMode: false),
                    initialPosition: .back
                ) { [weak self] cameraResult in
                    DispatchQueue.main.async {
                        self?.handleCameraResult(cameraResult)
                    }
                }
            } catch {
                // Handle initialization error
                print("Camera initialization failed: \\(error)")
            }
        }
    }

    private func handleCameraResult(_ result: CameraResult) {
        // Dismiss the hosting controller or handle UI changes
        // hostingController?.willMove(toParent: nil)
        // hostingController?.view.removeFromSuperview()
        // hostingController?.removeFromParent()
        // hostingController = nil
        // print("Camera session ended in UIKit host.")

        switch result {
        case .success(let fingerprintImages, let appearanceImages):
            print("UIKit Host: Capture success! Fingerprints: \\(fingerprintImages.count), Appearances: \\(appearanceImages.count)")
            // Proceed with upload using apiClient if needed
        case .fail:
            print("UIKit Host: Capture failed.")
        }
        // Show an alert or update UI
    }

    // Example: Trigger camera setup when a button is tapped
    // @IBAction func didTapStartCameraButton(_ sender: Any) {
    //     setupAndPresentCamera(species: .dog, purpose: .forRegisterFromProfile /*, apiClient: self.apiClient */)
    // }
}

Callbacks and State Observation

Instead of delegate methods or direct closures on CameraView, you observe the state changes from CameraViewModel and handle results via the CameraResultCallback.

  • CameraViewModel.@Published properties: CameraViewModel exposes several @Published properties that you can observe in your SwiftUI views to react to state changes:

    • detectionStatus: DetectionStatus (e.g., .noObject, .processing, .detected, .failed(reason))
    • currentDetectionProgress: Int (0-100%)
    • detectedObjectNormalizedRect: CGRect? (for drawing overlays)
    • cameraPermissionStatus: AVAuthorizationStatus
    • previewLayer: AVCaptureVideoPreviewLayer? (used by CameraPreviewView)
    • currentCameraPosition: AVCaptureDevice.Position (current camera position)
    • isSwitchButtonEnabled: Bool (whether camera switching is supported)
  • CameraResultCallback: This callback is passed to cameraViewModel.initializeCamera(...). It is invoked when the capture session finishes (successfully or with failure).

    • CameraResult.success(fingerprintImages: [URL], appearanceImages: [URL]): Provides URLs to locally cached images.
    • CameraResult.fail: Indicates the capture process failed.

Refer to CameraViewModel.swift for the exact definitions of these properties and enums like PetnowUI.Species, PetnowUI.CameraPurpose, PetnowUI.DetectionStatus, PetnowUI.CameraResult.

License Information

The framework now requires license validation during initialization:

public struct LicenseInfo {
    let apiKey: String
    let isDebugMode: Bool // Uses stage environment if true, prod if false

    public init(apiKey: String, isDebugMode: Bool) {
        self.apiKey = apiKey
        self.isDebugMode = isDebugMode
    }
}

Fake Detection Control

Anti-spoofing detection can be toggled at runtime for development and testing.

  • Default state: Enabled (true).

Programmatic control is available from the host app via CameraViewModel:

// Enable fake detection
viewModel.setFakeDetection(true)

// Disable fake detection
viewModel.setFakeDetection(false)

UI Customization

  • SwiftUI (CameraView): CameraView itself is a SwiftUI view. You can embed it and overlay other standard SwiftUI views on top of it for custom UI elements (buttons, labels, etc.) by placing them in a ZStack with CameraView.
  • The visual feedback for detection (e.g., bounding boxes, status messages within the camera area) is typically handled by DetectionOverlayView, which is used internally by CameraView and configured by CameraViewModel.

Error Handling

The framework provides comprehensive error handling through PetnowUIError:

do {
    try await viewModel.initializeCamera(...)
} catch PetnowUIError.invalidLicense(let underlyingError) {
    // Handle license validation failure
} catch PetnowUIError.permissionDenied(let message) {
    // Handle camera permission denial
} catch {
    // Handle other errors
}

Support

If you encounter any issues or have questions about the Petnow iOS UI SDK, please contact Petnow support at support@petnow.io.