Scan multipage documents with the iOS system camera
One.iOS.VNDocumentCameraViewController presents VisionKit’s document
camera, including its multipage capture, crop, and review UI. Reach for
it for receipt, contract, and whiteboard scanning where Apple’s capture
experience beats a custom camera screen. For live text and barcode
scanning in the viewfinder, see the data scanner instead.
This needs an iOS build, a supported physical device, and
ios.infoPlist.NSCameraUsageDescription. isSupported is the SDK
capability check, not a camera permission grant. Simulators cannot scan
documents.
import { One } from 'one'
if (One.iOS.VNDocumentCameraViewController.isSupported) { const camera = new One.iOS.VNDocumentCameraViewController() camera.delegate = { documentCameraViewController: async (controller, result) => { try { if ('didFinishWith' in result) { const scan = result.didFinishWith for (let at = 0; at < scan.pageCount; at++) { const image = scan.imageOfPage(at) const bytes = image.pngData() // consume the encoded page bytes here. } } else { const error = result.didFailWith console.error(error.domain, error.code, error.localizedDescription) } } finally { await controller.dismiss() } }, documentCameraViewControllerDidCancel: (controller) => { void controller.dismiss() }, } await camera.present()}The delegate owns dismissal, as in VisionKit. Completion delivers the scan or the failure; cancellation arrives on its own callback and never fabricates a scan.
Retain the controller while using it. Each controller has its own
delegate and presentation state. A concurrent presentation on the same
controller rejects, and overlapping transitions reject too. Delegate
errors preserve the native NSError domain, code, and localized
description. Presentation errors reject the promise before any delegate
result: unsupported hardware, a missing camera usage description, no
presenter, or an already-presented controller.
The delegate adapts Swift’s labeled overloads to JavaScript:
documentCameraViewController receives { didFinishWith: scan } or
{ didFailWith: error }, and documentCameraViewControllerDidCancel
receives the controller. present and dismiss adapt view controller
presentation to the current app presenter; the native camera content is
unchanged.
The scan retains the native result. The title is a string, the page
count a number, and imageOfPage returns the native page image for an
integer index within range; anything else throws a page error. The
image subset exposes the point size, the scale, and PNG and JPEG
encoders. Encoding returns an ArrayBuffer, or undefined when UIKit
cannot encode the image. JPEG quality must be finite within 0 to 1.
Pages encode only when requested; results are never replaced by
synthetic images or cached file lists.
| Class availability | Type | Description |
|---|---|---|
isSupportedbooleanStatic SDK device support check. False on web; native getter requires an iOS build. | boolean | Static SDK device support check. False on web; native getter requires an iOS build. |
| Controller | Type | Description |
|---|---|---|
delegateVNDocumentCameraViewControllerDelegate<VNDocumentCameraViewControllerInstance> | nullReceives completion, cancellation or failure. Defaults to null. | VNDocumentCameraViewControllerDelegate<VNDocumentCameraViewControllerInstance> | null | Receives completion, cancellation or failure. Defaults to null. |
present(animated?: boolean) => Promise<void>Presents from the current app controller. Resolves after presentation; animated defaults to true. | (animated?: boolean) => Promise<void> | Presents from the current app controller. Resolves after presentation; animated defaults to true. |
dismiss(animated?: boolean) => Promise<void>Dismisses the camera. Resolves after dismissal; animated defaults to true. | (animated?: boolean) => Promise<void> | Dismisses the camera. Resolves after dismissal; animated defaults to true. |
| Scan | Type | Description |
|---|---|---|
titlestringNative scan title. | string | Native scan title. |
pageCountnumberNumber of captured pages. | number | Number of captured pages. |
imageOfPage(at: number) => DocumentCameraImageNative page image at a valid zero-based index. | (at: number) => DocumentCameraImage | Native page image at a valid zero-based index. |
| Page image | Type | Description |
|---|---|---|
sizeDocumentCameraImageSizeWidth and height in points. | DocumentCameraImageSize | Width and height in points. |
scalenumberPixels per point. | number | Pixels per point. |
pngData() => ArrayBuffer | undefinedEncodes the page as PNG. | () => ArrayBuffer | undefined | Encodes the page as PNG. |
jpegData(compressionQuality: number) => ArrayBuffer | undefinedEncodes the page as JPEG. | (compressionQuality: number) => ArrayBuffer | undefined | Encodes the page as JPEG. |
| iOS | Android | Web | |
|---|---|---|---|
| Camera | VisionKit UI | Throws | Throws |
| Support | Per device | Never | Never |
| Pages | Native scan | None | None |
isSupported checks hardware only.
The camera prompt still gates the first presentation.import { One } from 'one'
async function scanReceipts(): Promise<ArrayBuffer[]> { if (!One.iOS.VNDocumentCameraViewController.isSupported) return [] const camera = new One.iOS.VNDocumentCameraViewController() return new Promise((resolve, reject) => { camera.delegate = { documentCameraViewController: async (controller, result) => { try { if ('didFailWith' in result) { reject(new Error(result.didFailWith.localizedDescription)) return } const scan = result.didFinishWith const pages: ArrayBuffer[] = [] for (let at = 0; at < scan.pageCount; at++) { const bytes = scan.imageOfPage(at).jpegData(0.85) if (bytes) pages.push(bytes) } resolve(pages) } finally { await controller.dismiss() } }, documentCameraViewControllerDidCancel: (controller) => { void controller.dismiss().then(() => resolve([])) }, } void camera.present() })}The promise bridges the delegate into async flow: JPEG bytes on finish,
an empty list on cancel, a throw on failure. The dismiss in finally
covers every path.
import { One } from 'one'
async function countPages(): Promise<number> { if (!One.iOS.VNDocumentCameraViewController.isSupported) return 0 const camera = new One.iOS.VNDocumentCameraViewController() return new Promise((resolve) => { camera.delegate = { documentCameraViewController: async (controller, result) => { try { resolve('didFinishWith' in result ? result.didFinishWith.pageCount : 0) } finally { await controller.dismiss() } }, documentCameraViewControllerDidCancel: (controller) => { void controller.dismiss().then(() => resolve(0)) }, } void camera.present() })}Page counts read straight off the scan without encoding a single image, which keeps a counting flow light on memory.
Edit this page on GitHub.