CameraView
The device camera inside the app: a live preview and still photos, each a JPEG
File. A photo taken in the app comes from CameraView; a photo
the user already has in the library, from openFilePicker({ accept: 'image/*' })
(Files & share).
- take a photo with the camera
- open a full-screen camera with a shutter and a close button
- show a live camera preview inside a card on a screen
- switch between the front and the back camera for a selfie
- show, upload or share the photo just taken
- offer the camera only on devices that have one
- send the user to the settings when the camera permission was denied
Platforms: iOS, Android, the web and the web player (https or localhost only); not on the desktop — check CameraView.isSupported.
At a glance
const camera = CameraView() // the back camera
const photos = UIRow().style({ gap: 8, flexWrap: 'wrap' })
camera.widgets( // the controls, over the preview
UIWidget(UIButton(UIText('Close')).style({ bgColor: '#00000080', p: 10, borderRadius: 20 })
.onClick(() => Router.pop())).style({ top: 'safe-top', left: 16 }),
UIWidget(UIButton(UIText('Shoot')).style({ bgColor: '#00000080', p: 16, borderRadius: 32 })
.onClick(async () => {
try {
const photo = await camera.takePhoto() // a JPEG File
Router.pop() // back to the home screen
photos.append(UIImage(photo).style({ width: 96, height: 128, borderRadius: 8 }))
} catch (e) { toast(e.message) } // "failed": no frame yet
})).style({ bottom: 'safe-bottom', left: 0, right: 0, alignItems: 'center' }),
)
Router.init(UIScreen(
CameraView.isSupported
? UIButton(UIText('Take a photo')).onClick(async () => {
try { await Router.push(camera) } // asks for the permission, then shows it
catch (e) { toast(e.message) } // denied, or another screen came first
})
: UIText('No camera on this device'),
photos,
).style({ p: 16, pt: 'safe-top', gap: 12 }))Add the camera to the app: app.json
The camera is a plugin: list it in the project's app.json, or it is not there — in a shell
built from the project, on the web and in the web player alike, where CameraView.isSupported is
then false. The compile warns about a camera the app uses and does not list.
{ "plugins": { "camera": "*" } }lecodes app plugin add camera writes the same entry. A shell built from the project takes the
camera permission from the plugin (NSCameraUsageDescription on iOS, CAMERA on Android). The
le.codes viewer app has the camera whatever the list says.
Full-screen camera with a shutter: Router.push
A camera that fills the screen is a destination of its own, as a screen is: CameraView() with
its controls — a shutter, a close button, a flip — in .widgets(...), pushed with
Router.push(camera); the At a glance is the whole flow. A preview goes among a
screen's children only when it is a part of that screen
(inside a screen).
CameraView(options?: { facingMode?: 'front' | 'back' }): CameraView // default 'back'
camera.widgets(...widgets: UIWidget[]): CameraView // the controls, laid out over the preview
Router.push(camera): Promise<void> // asks for the permission, then shows the preview
Router.pop(): void // back: the preview leaves
camera.onOpen(cb) / camera.onClose(cb) // shown / left (popped or covered)
camera.open(opts?: { transition }): Promise<void> // the same without the Router: replaces what is shown
camera.close(opts?: { transition }): void // after open(): leaves nothing on screenRouter.push(camera)asks for the camera permission while the current screen stays visible, then shows the preview. It rejects, and nothing is pushed, when the permission is denied, the host has no camera view or another navigation came first — catch it.- Back: a close button in the widgets calls
Router.pop(); the system's back pops the camera too, as any pushed destination. The sameCameraView()can be pushed again after a pop. - The widgets show while the camera is the destination on screen, positioned in its whole box:
top: 'safe-top'andbottom: 'safe-bottom'keep them clear of the notch and the home bar. camera.open()shows the camera without the Router, in place of what is shown; itscamera.close()then leaves nothing on screen, andRouter.restore()brings the router's screen back. With the Router, go back withRouter.pop(), neverclose().
Live preview inside a screen or a card
A preview that is a part of a screen — a viewfinder in a card, beside a form — is an element
among the screen's children, sized by its styles like any other; its buttons are ordinary
children next to it (its widgets show only while it is a destination, pushed or opened). It does not ask for the
permission itself: ask first with permissions.request('camera') — without it, Android shows a
black preview.
const preview = CameraView().style({ height: 240, borderRadius: 16 }) // sized like any element
const shot = UIColumn()
const main = async () => {
const allowed = CameraView.isSupported && (await permissions.request('camera')) === 'granted'
UIScreen(
allowed
? UIColumn(
preview, // the live viewfinder in the card
UIButton(UIText('Capture')).onClick(() => preview.takePhoto().then(
photo => shot.setContent([UIImage(photo).style({ height: 160, borderRadius: 8 })]),
e => toast(e.message))),
).style({ p: 12, gap: 8, borderRadius: 16, bgColor: '#1c1c1e' })
: UIText('Allow the camera to photograph the receipt'),
shot,
).style({ p: 16, pt: 'safe-top', gap: 12 }).open()
}
main()Front or back camera (selfie): setFacingMode
A selfie takes the front camera: create the view with CameraView({ facingMode: 'front' }), or
switch a preview on screen with setFacingMode.
CameraView({ facingMode: 'front' | 'back' }) // the camera it starts on; default 'back'
camera.setFacingMode(mode: 'front' | 'back'): Promise<void> // switch the preview on screenlet facing: CameraFacing = 'front'
const camera = CameraView({ facingMode: facing }) // a selfie camera
camera.widgets(
UIWidget(UIButton(UIText('Close')).onClick(() => Router.pop())).style({ top: 'safe-top', left: 16 }),
UIWidget(UIButton(UIText('Flip')).onClick(() => {
const next = facing === 'front' ? 'back' : 'front'
camera.setFacingMode(next).then(() => { facing = next }, e => toast(e.message)) // switch the preview
})).style({ top: 'safe-top', right: 16 }),
)
Router.init(UIScreen(UIButton(UIText('Selfie')).onClick(async () => {
try { await Router.push(camera) } catch (e) { toast(e.message) }
})))setFacingMode switches a preview that is on screen; to start on the front camera, pass
facingMode when you create the view.
Show, upload or share the photo: takePhoto()
takePhoto() resolves with the current frame as a JPEG File
named image.jpg — a handle to bytes the host holds in memory; nothing saves it to the photo
library. Hand it to what takes a File: UIImage(photo) shows it, a
FormData part uploads it,
share(photo) opens the OS share sheet (where the user can save it), Texture2D.load(photo)
makes a 2D texture, and a server function keeps it on the project's backend
(Stored files).
camera.takePhoto(): Promise<File> // rejects with "failed" when the camera has no frame to giveconst camera = CameraView()
const send = async (photo: File) => {
Router.pop()
const form = new FormData()
form.append('photo', photo) // a file part named image.jpg
const res = await fetch('https://api.example.com/photos', { method: 'POST', body: form })
toast(res.status === 200 ? 'Sent' : `Upload failed: ${res.status}`)
}
camera.widgets(UIWidget(
UIButton(UIText('Send')).onClick(() => camera.takePhoto().then(send, e => toast(e.message))),
UIButton(UIText('Share')).onClick(() =>
camera.takePhoto().then(photo => share(photo, 'My shot'), e => toast(e.message))),
).style({ bottom: 'safe-bottom', left: 0, right: 0, flexDirection: 'row', justifyContent: 'center', gap: 16 }))
Router.init(UIScreen(UIButton(UIText('Send a photo')).onClick(async () => {
try { await Router.push(camera) } catch (e) { toast(e.message) }
})))photos only — there is no video recording; openFilePicker({ accept: 'video/*' })
picks a video the user already has.
there is no flash, flashlight (torch) or zoom control — a CameraView is the
preview, takePhoto() and the facing mode.
No camera or permission denied: isSupported
Offer the camera only where there is one, and tell the user why it did not open.
CameraView.isSupported: boolean // static — does this host have the camera view?
camera.on('error', (e: { message: string }) => void) // the browser's preview could not startisSupportedisfalsewhere the host has no camera view: on the desktop, on a web page that is not https or localhost, in an app whoseapp.jsondoes not list the plugin, and in headless tests (below).Router.push(camera)andcamera.open()reject, and nothing is shown, with the reason as the message: the permission denied (The camera permission was denied), no camera view on this host (CameraView isn't available on this host — …), another navigation first (… superseded …).- After a denial
permissions.query('camera')is'denied', andpermissions.openSettings()opens the app's page of the system settings, where the user turns the camera back on: iOS never asks twice, and the web has no such page (Permissions). - The
errorevent comes from the browser only (the web and the web player): no camera, or the browser refused the stream. iOS and Android send none. - Headless tests (
lecodes render/lecodes test) have no camera:isSupportedisfalseunless the run names the view,--native-view camera. The preview is then a labelled placeholder,Router.pushworks, andtakePhoto()/setFacingMode()reject.
const camera = CameraView()
camera.widgets(UIWidget(UIButton(UIText('Close')).onClick(() => Router.pop()))
.style({ top: 'safe-top', left: 16 }))
const openCamera = async () => {
try {
await Router.push(camera) // asks for the permission first
} catch (e) {
if ((await permissions.query('camera')) === 'denied') {
toast('Turn the camera on in Settings')
permissions.openSettings() // the app's page of the settings
} else toast(e.message)
}
}
Router.init(UIScreen(
CameraView.isSupported
? UIButton(UIText('Open the camera')).onClick(openCamera)
: UIText('This device has no camera'),
))When the camera turns on and off
The camera turns on when its preview is shown. On iOS and Android it turns off when the preview leaves the screen — popped, closed, covered by another screen, or gone with the screen that embeds it — and on again when the preview comes back: there is nothing to release by hand.
on the web the camera, once on, stays on after the preview leaves — the browser's
camera indicator stays lit — until the page is closed (in the web player: until the
CameraView is freed).
- Make one
CameraView()and push it again each time: on the web every instance keeps its own camera stream. - The device lends its camera to one preview: show one
CameraViewat a time.
Pitfalls
-
A full-screen preview among a screen's children, with buttons positioned absolutely over it, is a web habit; the camera is a destination with its controls in its widgets:
UIScreen(camera.style({ flexGrow: 1 }), shutter.style({ position: 'absolute', bottom: 40 })).open() // ✗ Router.push(camera.widgets(UIWidget(shutter).style({ bottom: 'safe-bottom' }))) // ✓ -
camera.close()on a pushed camera leaves an empty screen; go back withRouter.pop():camera.close() // ✗ after Router.push: nothing is on screen Router.pop() // ✓
See also
- Files & share —
openFilePickerfor a photo from the library;share() - Networking — the
Filehandle; uploading withFormData - Stored files — keeping photos on the project's backend
- UIWidget — the controls over the preview
- Screens & Router —
Router.push/pop, transitions - Permissions — asking earlier; a denied camera
- QRScanner — the camera that reads codes
- NativeView — the channel under every view plugin