The passport and selfie capture flow (front page, back page, selfie, gallery and file upload, review and failure sheets), extracted from the VDC app's VDCApp/Modules/IdentityCapture into a standalone SPM package. The remote is atlyslabs/ATCaptureKit (branch master, not tagged yet).
.package(url: "https://github.com/atlyslabs/ATCaptureKit.git", branch: "master")Architecture rule this package follows: the package does everything except run the models. It owns the camera, every screen, the on-device checks (image pre-processing, model output decoding, Vision face checks, brightness and blur) and every capture decision. It also ships the four TFLite models. The host only runs a model when asked: it gets a model file once, then turns an input tensor into an output tensor. OCR, upload and visa-funnel storage stay with the host.
Sources/ATCaptureKit/Domain:- the pieces (
IDCPiece:.passportFront,.passportBack,.selfie),IDCConfiguration,IDCCallbacksandIDCVerifier; - the model-runner seam (
IDCInference.swift) and the per-flow check switches (IDCChecks.swift); IDCUploadCheck, which turns photo findings into failures.
- the pieces (
Sources/ATCaptureKit/Analysis— the on-device pipeline, ported from the VDC app:- the passport detector and glare classifier: YOLOv8 decoding, per-class thresholds, NMS, the travel-document rule, the finger check over the MRZ details;
- the selfie checks: Vision landmarks, phone tilt, framing hysteresis, the 4-way classifier;
- the tensor pre-processing (vImage rotate, scale and letterbox, normalisation);
- brightness and blur;
- the analyzers the screens use.
Sources/ATCaptureKit/Presentation— the flow.IDCFactoryandIDCFlowVieware the entry points. The passport scan, the flip between front and back, the selfie scan, the review sheet and the failure sheet live underPresentation/Views.Sources/ATCaptureKit/Core:- camera session and permission gate;
- the live-scan state machines (
IDCPassportLiveScan,IDCSelfieLiveScan); - the frame renderer that rotates and crops the captured still;
- the capture chime, haptics, image and PDF pickers, and
IDCAnalytics.
Sources/ATCaptureKit/Resources:passport_multi_class_detection.tflite,glare_model_fp16.tflite,passport_blur_model.tfliteandselfie_model.tflite;scanSuccess.mp3.
Sources/ATCaptureKit/Debug—IdentityCaptureDebugView, a harness for driving the flow without a host. It is compiled into the library.AppIntegration/IdentityCaptureHost— the Atlas visa-funnel adapter (ApplicationCaptureLauncher, the OCR client). It is not a target of this package.- It mirrors the app's
VDCApp/Modules/IdentityCaptureHost, which builds against this package. Engines/VDCTFLiteRunner.swiftis the app's model runner and settings mapping.Core/VDCCaptureAnalytics.swiftis the analytics reporter.
- It mirrors the app's
Passport detection art, corner brackets, and the camera preview moved in with the flow. They used to live in the KYC module.
-
Register a model runner once at launch. This is the only ML code a host writes. With TensorFlow Lite (
pod 'TensorFlowLiteSwift'):import ATCaptureKit import TensorFlowLite final class TFLiteRunner: IDCModelRunner { func loadModel(at url: URL) throws -> IDCLoadedModel { try TFLiteModel(url: url) } } private final class TFLiteModel: IDCLoadedModel { private let interpreter: Interpreter init(url: URL) throws { var options = Interpreter.Options() options.threadCount = 1 interpreter = try Interpreter(modelPath: url.path, options: options) try interpreter.allocateTensors() } func run(_ input: Data) throws -> IDCTensor { try interpreter.copy(input, toInputAt: 0) try interpreter.invoke() let output = try interpreter.output(at: 0) return IDCTensor(data: output.data, shape: output.shape.dimensions) } } IDCInference.runner = TFLiteRunner()
- What the package passes in: the URL of one of its own bundled models (
IDCModel), loaded off the main thread when a capture screen appears and released with the screen. Each input is Float32 NHWC data, already resized and normalised. - What the host returns: output tensor 0 with its shape.
- Threading: calls to one loaded model never overlap.
- Without a runner: live scans never fire, and uploads fail with "scanner not ready" after a 5-second wait.
- What the package passes in: the URL of one of its own bundled models (
-
Set
IDCAnalytics.reporteronce at launch. The property is weak, so the host keeps the object alive. Events are dropped while it isnil.final class CaptureAnalytics: IDCAnalyticsReporting { func report(event name: String, parameters: [String: Any?]) { Analytics.track(name, parameters.compactMapValues { $0 }) } } IDCAnalytics.reporter = CaptureAnalytics()
Event names:
identity_capture_started,identity_capture_piece_captured,identity_capture_piece_verified,identity_capture_piece_rejected,identity_capture_completed,identity_capture_exited. Each one carrieshost_contextfromIDCConfiguration.analyticsContext. -
Build an
IDCConfigurationfor the pieces this session should capture, in order.- Per-piece title, subtitle, and eyebrow go on
copy. headerAccessoryis an optional SwiftUI view under the header.passportChecksturns the glare, finger and blur checks off. The blur classifier runs on the detected page crop, for both pages, only on a frame that passed fingers and glare; a blurred frame never counts toward the live-scan streak, and a blurred upload is rejected withIDCCaptureFailure.blurryPhoto.copy[piece].headerStyleis.standard(icon, title, gradient subtitle) or.titleOnly(accessory:)(title plus an optional host view under it).copy[.passportBack].flipHintreplaces the front→back flip prompt.fallbackLink(IDCFallbackLink) replaces the built-in reminder link on both passport pages and the selfie, shows from the start, and hands the tap to the host. KYC v2 uses it for its served "Don't have it handy?" reminder.selfieChecksandselfieUploadCheckschoose the selfie rules for live and uploaded photos. Uploads use the live rules whenselfieUploadChecksisnil.phoneStraight(1.1.0) is the live phone-tilt check: the phone must not lean back or forward, or tilt sideways.glassesandcoveredFacegate the live classifier too, and with both off it does not run..noneturns every switch off. These have no switch: exactly one face, too dark, the face inside the viewfinder and not too far (live), and "not a selfie" on an upload.
var configuration = IDCConfiguration( pieces: [.passportFront, .passportBack], flowTitle: "Scan passport", analyticsContext: "profile_passport" ) configuration.showsBack = true configuration.allowUploadFromDevice = true configuration.progress = .segments(count: 2, filled: 1) configuration.passportChecks = IDCPassportChecks(glare: false, fingers: true) configuration.copy[.passportFront] = IDCCaptureCopy( title: "Scan the photo page", subtitle: "Lay it flat inside the frame" )
- Per-piece title, subtitle, and eyebrow go on
-
Pass a verifier into
IDCFactory. The verifier runs after the user confirms a photo.let controller = IDCFactory.makeViewController( configuration: configuration, verifier: PassportUploadVerifier(), callbacks: IDCCallbacks( onComplete: { results, payloads in /* every piece verified */ }, onExit: { /* user backed out */ } ) )
IDCFactory.push(from:configuration:verifier:callbacks:)pushes that controller and dismisses it on exit.- A SwiftUI host builds an
IDCFlowViewModelwith the same three arguments and rendersIDCFlowView(viewModel:). IDCVerifier.verifyreturns.verified(payload:),.rejected(reason:), or.failed(message:). A rejection brings the failure sheet and lets the user scan again. A failure is the transport case, with retry. The payload from.verifiedis handed back ononCompletekeyed by piece.IDCFlowViewModel.dropPendingPieces(_:)(1.1.0) takes pieces not yet captured out of the run. A verifier that learns it no longer needs the next piece calls it before returning.verified, and the flow skips that piece (no front→back flip) and completes when nothing is left. KYC uses it for a passport with no back page.
- An
IDCModelRunner(above), and the TFLite runtime it uses. TFLite ships for iOS through CocoaPods only, so the package cannot depend on it. A host that already links TFLite for other models would also clash with a second copy. - An
IDCVerifierthat uploads or checks the confirmed image. The package does not talk to the network. NSCameraUsageDescriptionandNSPhotoLibraryUsageDescriptionin the host'sInfo.plist. The primer and the denied-camera panel are in the package; the usage strings are not.IDCAnalytics.reporter, if the host wants the events above. Leaving it unset drops them.
Image("reminder_bell") and the names in IDCTheme.Assets still read the main bundle. They build, and they render only when the host's asset catalog contains them.
IDCPiece,IDCCaptureResult,IDCCaptureSource,IDCVerificationOutcome,IDCCaptureFailure— the values a verifier and a callback see.IDCCaptureResult.fieldDetections(IDCFieldDetections) holds the passport photo, signature and MRZ boxes, normalised to the captured crop.IDCProgressStyle—.segments(count:filled:)(recomputed from the capture's own pieces),.steps(count:filled:)(host-driven, drawn exactly as given),.fraction, or.hidden.- The scan chrome, for a host that draws its own capture-adjacent screens in the same style (KYC v2's
passport, Gmail, ITR and confirm screens):
IDCScanTopBar(+idcScanTopBarTopSpacing),IDCCaptureHeader,IDCScanViewfinder,IDCSegmentedProgress,IDCTheme(theColors,LayoutandAssetsvalues those screens read),IDCReminderPlanner.customDate, and theIDCStringsthe reminder sheet and failure copy share. IDCModel,IDCTensor,IDCModelRunner,IDCLoadedModel,IDCInference— the model-runner seam.IDCModel.urlalso lets the host load the same bundled files elsewhere.IDCPassportChecks,IDCSelfieChecks(.standard,.none) — the check switches.
- The TFLite runtime. The host runs the models (see above).
- OCR and storage.
IDCDefaultOCRVerifier,IDCOCRVerificationMapper, andIDCVisaPassportVerifierlive inAppIntegrationand are not compiled here. A profile or KYC host passes its own upload closure as the verifier. - The visa funnel.
ApplicationCaptureLauncherand the passport and selfie host view controllers know aboutUserData,PassportData, and bouncer requirements. They call this package. They do not move into it. - KYC onboarding around the scan. Gmail connect, ITR connect, and the onboarding coordinator are not capture screens. A host that reaches this flow from KYC presents
IDCFlowViewfor the passport step and the selfie step and keeps the rest of that session itself.