SmartComply iOS SDK
The SmartComply iOS SDK is a native Swift library that delivers a fully self-contained identity verification flow for iOS apps. Drop in one SwiftUI view and the SDK handles session management, country and ID-type selection, document capture, identity verification, and liveness detection automatically.Features
- Drop-in SwiftUI view —
SmartComplyFlowViewmanages the entire verification flow with no UI code required - Two verification modes — document photo capture or ID number data entry, configured from your Dashboard
- Guide-box document capture — crops exactly what falls inside the ID card frame so the image sent to the backend is always clean
- Liveness detection — a passive face scan (blink plus natural head movement) runs automatically after identity verification, with no step-by-step prompts
- Dynamic ID types — channels and fields are fetched live from your Dashboard configuration
- Multi-country support — renders a country picker automatically when more than one country is configured
- Dark and light mode — theme adapts to the system colour scheme; override with
preferredColorScheme - Automatic upload retry — transient upload failures are retried internally
- Your brand, not ours: name, description, colour and logo come from the Dashboard, and the font is passed by your app. See Branding
- Any backend host: production, staging, or your own, without a rebuild
Requirements
- iOS 16.0 or later
- Xcode 16 or later. The shipped XCFramework’s module interface names
SwiftUICorein public signatures, a module that first ships in the iOS 18 SDK, so Xcode 15 cannot compile it - A physical device that supports ARKit face tracking. Liveness gates on
ARFaceTrackingConfiguration.isSupported, which covers iPhone X and later as well as A12 and later devices without a TrueDepth camera. Liveness cannot run in the Simulator
Installation
The SDK is distributed via Swift Package Manager as a precompiled XCFramework. The package you add contains the Swift Package manifest; Xcode downloads the binary automatically and embeds it in your app.Xcode (recommended)
- Open your project in Xcode
- Go to File → Add Package Dependencies
- Enter the repository URL:
https://github.com/386konsult/smartcomply-ios-sdk - Select Up to Next Major Version and enter
1.1.0 - Click Add Package and select the SmartComplySDK library
Package.swift
1.1.0 reworks the liveness screen and changes when the selfie is taken.
Verification now happens in place, on a frozen blurred preview, instead of
cutting to a separate progress screen. The guide is a circle rather than an
oval. And the autoshot waits for a properly framed face rather than firing on a
timer, which adds up to 2.5 seconds for a user slow to get into frame and
nothing for one already there. Read Liveness before you upgrade.
Take 1.0.4, not 1.0.3. Up to 1.0.3 the front check required a face, and
rejected genuine IDs whose portrait is engraved rather than printed, Kenya’s
Maisha Namba card among them, with “No face detected”. 1.0.4 judges the front
on text instead. See Document images.
1.0.3 changes two behaviours.
FlowResult.status now holds the real verdict, and the success
screen no longer means a pass. Read FlowResult before upgrading from 1.0.2 or
below. It also adds Branding and a custom backend host.iOS and Android are versioned separately and the numbers have never lined up, because the two
have always been on their own tracks.The package URL is
smartcomply-ios-sdk, which is not the same as the SDK’s source repository.
It ships both device and simulator slices, so the same dependency builds for a physical device
and the Simulator without extra configuration.Platform Setup
Add the following key to your app’sInfo.plist:
PHPickerViewController, which runs out of process and requires no photo-library key.
Credentials
Both values come from your Adhere Dashboard and both are permanent. Reuse the same pair for every verification.Quick Start
1. Create the SDK instance
Create aSmartComply instance once, for example in your view model or app entry point.
2. Present the flow view
EmbedSmartComplyFlowView anywhere in your SwiftUI hierarchy. The SDK loads automatically when the view appears.
- Creates a secure session
- Displays a welcome screen with your logo and ID type cards
- Shows a country picker if multiple countries are configured
- User photographs the front of their ID inside the guide box
- Photographs the back, when the Dashboard’s
requires_back_sidefor that ID type isrequiredoroptional. Onoptionalthe capture screen also shows a This ID has no back side control; tapping it goes straight to the liveness scan and the entry is submitted with no back image. When the backend sends no value at all, the SDK falls back to matching the ID type’s name against national, driver, resident and voter - Runs the passive liveness scan
- Returns a
FlowResultto your completion handler
- Creates a secure session
- Displays a welcome screen with ID type selection
- Shows a country picker if multiple countries are configured
- User enters their ID number and any required fields
- Identity is verified against the national database
- Runs the passive liveness scan
- Returns a
FlowResultto your completion handler
Document images
Document photos are capped at 5 MB server-side. Both routes, the in-SDK camera and Upload from library, are downscaled to 1280px on the long edge and re-encoded as JPEG at quality 0.72 before upload, typically 150 to 350 KB, so a full-resolution phone photo does not fail the size limit. Before the review screen the SDK checks the front photo looks like a document: from 1.0.4 that means at least three regions of text. A failure is a warning, not a block. The user can still confirm, because a false negative would otherwise trap someone holding a genuine but unusual ID. The backend OCR is the check that actually rejects. The back photo is not checked.Environments
.sandbox targets a server running on the device itself, for local backend development. It is not a hosted test environment. Use .production for all integration work.
A different host
SDKEnvironment covers the two hosts you ship against, and one of them is
loopback on the handset. For anything else, a staging deployment or a QA build
that has to reach more than one backend without being rebuilt, override it. Added
in 1.0.3.
environment rather
than being treated as a host, so a build reading this off an empty field does not
post verifications to a relative URL.
Branding
The flow carries your brand, not Adhere’s. Colour and logo come from the SDK Config on the dashboard, so they change without an app release, and your name is shown while the session is being set up. The font is the one exception and is passed by the host.
The welcome heading and subheading are fixed SDK copy: “Verify Your Identity”, then one of two
lines keyed off the verification mode.
Logo
Upload it under Settings → Integrations → SDK → Brand Details. PNG, JPEG or WebP, up to 512 KB and 1024x1024 pixels. Requires 1.0.3. It is drawn on its own, with no box or border behind it, and bounded by height rather than fitted into a square so a wordmark stays legible. Only the SDK’s fallback shield sits in a tinted frame. The logo belongs to the SDK Config, not the company. A branch running several configs for different products gives each its own mark, and a config with no logo shows the SDK’s own shield rather than falling back to the company logo on your profile page. If you want the same mark everywhere, upload it to each config.A logo never delays or blocks a verification. The SDK fetches it after the
welcome screen is already on screen, on a three second budget, and keeps its own
shield if the fetch fails. It is also decoded through a pixel cap, so an
oversized image cannot exhaust memory on the handset.
Font
Passed by your app, because the SDK neither ships nor downloads font files: your loading, caching and licensing stay yours. Requires 1.0.3. A face listed underUIAppFonts, or registered at runtime, is available
process-wide, so the SDK needs only its PostScript name.
Font.custom(_:size:) selects a face by name and
takes no weight argument, so a single name would render every screen at one
weight. Only regular is required; anything omitted falls back to the nearest
name you did supply.
A name that is not registered logs a warning under the
com.smartcomply.sdk
subsystem and falls back to the system font rather than rendering nothing.
Dynamic Type still applies either way.Liveness
Two actions are drawn from.blink and .turnHead in random order and detected passively. Both are watched on every frame, neither is named to the user, and the HUD only reports how many are done: there is no prompted sequence.
Head yaw is measured against the phone, not against the world, and frames during which the device itself is rotating are not credited: turning the handset does not satisfy the challenge.
The scan has a 20 second cap.
The screen, from 1.1.0
The guide is a circle rather than an oval, and verification happens in place. Earlier versions cut to a separate progress screen the moment the capture was submitted, so the result poll began by throwing away the thing the user was looking at. The preview now freezes on its last frame, blurs, and the guide is replaced by a dotted ring while the poll runs underneath; the checkmark lands in the same circle. ARKit is paused as soon as the preview freezes. Nothing past.uploading reads
another frame, so the camera indicator no longer stays lit for the whole poll.
LivenessOrchestrator.stopCamera() is public if you drive the orchestrator
yourself.
When the autoshot is taken
Two stills leave the device. The autoshot is taken before the scan and uploaded with the entry; the snapshot is taken once both actions are done and uploaded with the video. From 1.1.0 the autoshot waits for a properly framed face. Up to 1.0.4 it fired 500ms after the session started, whatever ARKit happened to be pointed at, so the selfie on the dashboard was routinely of a ceiling. It now holds the shot until three consecutive frames put the face inside the guide, projecting the face anchor into the preview withARFrame.camera.projectPoint and checking it
against the same circle the user is being asked to fill.
- Timing. A 300ms settle, then up to 2.5 seconds waiting. On an iPhone 13 Pro a user moving into the guide cleared it in under half a second. A user who never frames themselves waits the full 2.5 seconds and then gets the old behaviour, so the fallback is never a worse image, only a later one.
- Presence alone is not the bar, and this is the part worth knowing. ARKit will happily track a face in the corner of the frame. Measured on device, a selfie taken that way scored 25.6% against the ID photo and was reported as a mismatch; the same person framed in the guide scored 67%. Bad framing does not just look wrong on the dashboard, it fails legitimate users.
- The HUD. A face that is tracked but outside the guide now reads “Move your face into the frame”. Earlier versions fell through to “Almost there, keep going…”, which told a user whose face was off the bottom of the screen that they were making progress.
Accessibility
From 1.0.3 the flow honours the user’s text size setting (Settings → Accessibility → Display & Text Size → Larger Text). Earlier versions used fixed point sizes throughout, so the setting had no effect on any screen. Scaling is capped at.accessibility1. The liveness guide, the document frame and the buttons are sized to fit their content, and the accessibility sizes above that cap overflow them: the bound keeps everything below it usable rather than shipping layouts that break at the top of the range.
What “Verification Complete” means
Nothing about the verdict. Every submitted entry reaches that screen, whether the backend passed or failed the check. The submit call returnsprocessing, because the backend resolves face matching on a worker moments later. The SDK polls for up to about 15 seconds and puts whatever it learns in FlowResult.status, then shows the success screen either way. A user who failed is not told so and is not offered a retry.
This is a product decision, not an oversight: tenants asked for the end user to see a submitted verification as accepted, and to act on the real verdict themselves from the dashboard and the webhook. It means you have to close the loop with a user who failed.
Pre-submission failures are unaffected and still show the failure screen with Try Again: a denied camera, a configuration or network error, and reaching the 20 second liveness cap. Nothing has been billed at that point and the user can recover on the spot.
Knowing the user gave up
onComplete only fires on a verification. Pass the optional onDismiss to learn that the flow ended without one: the user closed the failure screen, or an error left nothing to retry. Passing it also adds a Close control to the failure screen, which otherwise offers only Try Again.
SDK Configuration
FlowResult
Delivered to youronComplete closure when the user completes the flow in-app.
verifiedName and idNumber are populated from the poll as well as from submit, from 1.0.3.
In document mode they previously appeared only when OCR happened to finish before the user
completed liveness, so whether a user saw their own name came down to timing.From 1.0.2 both are populated. In document verification they come from the OCR of the captured
document; in data verification they come from the identity provider’s response.
verifiedName is
the full name where the provider sends one, otherwise it is composed from the first, middle and
last name fields.They are still only as good as the submission that produced them, so treat them as a receipt of
what was read, not as a verdict. The webhook remains the source of truth for the final outcome.The webhook is still the system of record. An entry’s status moves through
pending,
processing, and then passed, failed or expired, and the final status, the extracted fields
and any failure reason are delivered to the webhook URL configured on your SDK Config. See
Webhooks for the full payload.What changed in 1.0.3 is that onComplete no longer fires ahead of that verdict. It waits for
the backend to reach a terminal status, so status agrees with what the webhook will say. Reconcile
from the webhook for anything you persist: a run that has not resolved in about 15 seconds still
returns "processing".Error Handling
SmartComplyFlowView handles and displays error states automatically. For the headless API, all SDK methods throw on failure:
SDKError exposes message: String, statusCode: Int and errorCode: String?. AuthError, NetworkError and AlreadySubmittedError all subclass it, so a single catch let error as SDKError covers every case.
From 1.0.3 the error carries the backend’s own code.
errorCode was previously hardcoded to
AUTH_ERROR for every 401 and 403, so SDK_CONFIG_NOT_FOUND, an expired session and an invalid
key were indistinguishable and host logic keyed on the code could not work. Validation failures
likewise reported only the generic "One or more fields failed validation."; the per-field reason
is now surfaced.AlreadySubmittedError is raised when an upload retry is rejected as a duplicate, which means the
original attempt reached the server and the entry exists and was billed: the response was simply
lost coming back. The bundled flow treats it as such and reads the outcome rather than reporting a
failure. Handle it the same way if you drive the modules yourself.
