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 thePetnowUImodule. It displays the camera preview and works withCameraViewModelto handle the pet detection process.CameraViewModel(SwiftUI ObservableObject): AnObservableObjectalso provided byPetnowUI. It manages the camera session, detection logic, and communicates results and status updates toCameraViewor other observing components.
Important Implementation Notes
- The
CameraViewModelnow uses an async initialization pattern with built-in license validation for improved security and error handling. CameraViewis typically presented modally or embedded within your existing SwiftUI view hierarchy.- The
CameraViewModelmust be initialized with the appropriatePetnowUI.SpeciesandPetnowUI.CameraPurpose. If you need to use a separatePetnowAPIClientfor uploads, these UI-specific enums will need to be mapped to theirPetnowAPIcounterparts 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 thePetnowAPImodule), ensure you pass the correct mappedPetnowAPI.PetSpeciesandPetnowAPI.DetectionPurposewith 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.Species | PetnowAPI.PetSpecies |
|---|---|
PetnowUI.Species.dog | PetnowAPI.PetSpecies.DOG |
PetnowUI.Species.cat | PetnowAPI.PetSpecies.CAT |
CameraPurpose to DetectionPurpose (for image uploads):
| PetnowUI.CameraPurpose | PetnowAPI.DetectionPurpose | Notes |
|---|---|---|
forRegisterFromProfile | PetnowAPI.DetectionPurpose.PET_PROFILE_REGISTRATION | |
forSearch | PetnowAPI.DetectionPurpose.PET_VERIFICATION | For image uploads prior to identification |
e.g., identifyPetnowPets) | ||
forWitness | PetnowAPI.DetectionPurpose.PET_VERIFICATION | For image uploads in witness report related identification scenarios |
appendNose | Currently not supported |
It's crucial to pass these mapped PetnowAPI.PetSpecies and PetnowAPI.DetectionPurpose values when calling apiClient.uploadFingerPrints(...) or apiClient.uploadAppearances(...).
Implementation Notes
CameraViewModelhandles camera permission requests and initialization via the new asyncinitializeCamera()method. While you can optionally callcheckAndRequestCameraPermission()separately for nicer permission handling,initializeCamera()will automatically request permissions if needed.- Detection logic prioritizes dog noses or cat faces based on the
PetnowUI.Speciespassed toCameraViewModel. - Visual feedback (e.g., guiding overlays via
DetectionOverlayView, progress indicators) is provided byCameraViewbased on states published byCameraViewModel.
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.
- Create
CameraViewModel: InstantiateCameraViewModelwith the desiredPetnowUI.SpeciesandPetnowUI.CameraPurpose. - Create
CameraView: InitializeCameraViewwith theCameraViewModelinstance. - Host in
UIHostingController: Create aUIHostingController(rootView: cameraView). - Present or Embed: Present the
UIHostingControllermodally, or add its view to your UIKit view hierarchy. - Handle Results: The
CameraResultCallbackprovided tocameraViewModel.initializeCamera(...)will still be called. You can handle the results within the hostingUIViewControlleror 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.@Publishedproperties:CameraViewModelexposes several@Publishedproperties 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: AVAuthorizationStatuspreviewLayer: AVCaptureVideoPreviewLayer?(used byCameraPreviewView)currentCameraPosition: AVCaptureDevice.Position(current camera position)isSwitchButtonEnabled: Bool(whether camera switching is supported)
-
CameraResultCallback: This callback is passed tocameraViewModel.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):CameraViewitself 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 aZStackwithCameraView. - The visual feedback for detection (e.g., bounding boxes, status messages within the camera area) is typically handled by
DetectionOverlayView, which is used internally byCameraViewand configured byCameraViewModel.
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.