Image picker
Let users choose a photo or video from their library, or take one with the camera.
@symbiote-native/image-picker wraps
expo-image-picker, which
opens the system picker UI, so every SymbioteNative adapter can reach it, not just React. The
launch and permission functions are free functions shared by every adapter. The two declarative
permission hooks (useCameraPermissions, useMediaLibraryPermissions) ship on all five, in each
framework’s own idiom.
| OS platform | Support |
|---|---|
| iOS | live |
| Android | live |
| Framework adapter | Support |
|---|---|
| React | live |
| Vue | live |
| Angular | live |
| Svelte | live |
| Solid | live |
Installation
Section titled “Installation”npm install @symbiote-native/image-pickerScaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --image-picker (or
add --image-picker in an existing app) installs and wires this for you - see
@symbiote-native/cli.
expo-image-picker and expo-modules-core come along as regular dependencies, pinned to exact
versions. Never install either yourself, and never add the expo meta-package to your project (it
bundles its own Metro/Babel pipeline, which conflicts with this project’s own).
Permission strings land automatically on install:
| Platform | Added | Why |
|---|---|---|
| iOS | NSPhotoLibraryUsageDescription |
Reading the photo library |
| iOS | NSCameraUsageDescription |
Taking a photo or video |
| iOS | NSMicrophoneUsageDescription |
Recording video with sound |
| Android | RECORD_AUDIO |
The camera picker can record video with sound |
CAMERA and the READ_MEDIA_* / pre-33 storage permissions ship in expo-image-picker’s own
manifest and merge in once the Gradle project is included. Reword the iOS strings in
native-link.json if the defaults do not fit your app.
Request the permission, launch the picker, and check canceled before reading assets. These
free functions are identical on every adapter:
import { launchImageLibraryAsync, requestMediaLibraryPermissionsAsync,} from '@symbiote-native/image-picker';
await requestMediaLibraryPermissionsAsync();const result = await launchImageLibraryAsync({ mediaTypes: 'images', quality: 0.8 });if (!result.canceled) { console.log(result.assets[0].uri);}Take a photo instead:
import { launchCameraAsync, requestCameraPermissionsAsync } from '@symbiote-native/image-picker';
await requestCameraPermissionsAsync();const result = await launchCameraAsync({ allowsEditing: true });Declarative permission state
Section titled “Declarative permission state”To render permission state, use the adapter’s own binding. Each is shown for the camera;
useMediaLibraryPermissions has the same shape and also accepts { writeOnly }.
import { useCameraPermissions } from '@symbiote-native/image-picker/react';
export default function CameraGate() { const [status, requestPermission] = useCameraPermissions();
if (!status?.granted) { return <button title="Allow camera" onPress={() => requestPermission()} />; } return <text>Camera allowed</text>;}<script setup lang="ts">import { useCameraPermissions } from '@symbiote-native/image-picker/vue';
const [status, requestPermission] = useCameraPermissions();</script>
<template> <text v-if="status?.granted">Camera allowed</text> <button v-else title="Allow camera" @press="requestPermission()" /></template>import { Component, inject } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { CameraPermissionsService } from '@symbiote-native/image-picker/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` @if (camera()?.granted) { <text>Camera allowed</text> } @else { <button title="Allow camera" (press)="request()" /> } `,})export class CameraGate { private readonly service = inject(CameraPermissionsService); readonly camera = this.service.connect();
request(): void { void this.service.request(); }}connect() returns a signal of the current status; get() and request() are imperative.
<script lang="ts"> import { useCameraPermissions } from '@symbiote-native/image-picker/svelte';
const camera = useCameraPermissions();</script>
{#if camera.status?.granted} <text>Camera allowed</text>{:else} <button title="Allow camera" onPress={() => camera.requestPermission()} />{/if}Svelte returns { status, requestPermission, getPermission } instead of a tuple, matching this
repo’s rune idiom.
import { useCameraPermissions } from '@symbiote-native/image-picker/solid';
export function CameraGate() { const [status, requestPermission] = useCameraPermissions();
return status()?.granted ? ( <text>Camera allowed</text> ) : ( <button title="Allow camera" onPress={() => requestPermission()} /> );}Functions
Section titled “Functions”| Signature | Description |
|---|---|
launchCameraAsync(options?) |
Opens the camera to take a photo or video. Resolves an IImagePickerResult |
launchImageLibraryAsync(options?) |
Opens the library picker. Resolves an IImagePickerResult |
getCameraPermissionsAsync() |
Reads the camera permission without prompting |
requestCameraPermissionsAsync() |
Prompts for the camera permission |
getMediaLibraryPermissionsAsync(writeOnly?) |
Reads the media library permission without prompting. writeOnly asks for add-only access |
requestMediaLibraryPermissionsAsync(writeOnly?) |
Prompts for the media library permission |
getPendingResultAsync() |
Android only. Recovers a result delivered after the app was killed. Resolves null on iOS |
IImagePickerOptions
Section titled “IImagePickerOptions”| Field | Type | Description |
|---|---|---|
mediaTypes |
IMediaType | IMediaType[] |
What to show: images, videos, or both. Defaults to 'images'. The deprecated MediaTypeOptions enum still works |
allowsEditing |
boolean |
Crop or rotate after picking. Exclusive with allowsMultipleSelection. Defaults to false |
aspect |
[number, number] |
Crop aspect ratio as [x, y]. Android only |
shape |
'rectangle' | 'oval' |
Crop shape. Android only. Defaults to 'rectangle' |
quality |
number |
Compression from 0 (smallest) to 1 (largest). Defaults to 1 |
exif |
boolean |
Include EXIF data on each asset |
base64 |
boolean |
Include the image as a base64 string on each asset |
allowsMultipleSelection |
boolean |
Let the user pick more than one item. Ignored when allowsEditing is set. Defaults to false |
selectionLimit |
number |
Maximum items when selecting several. 0 means the system maximum. Android and iOS 14+ |
orderedSelection |
boolean |
Number selected items in tap order. iOS 15+ only. Defaults to false |
defaultTab |
IDefaultTab |
Which tab the picker opens on. Android only. Defaults to 'photos' |
videoMaxDuration |
number |
Longest video in seconds. 0 means no limit |
videoQuality |
UIImagePickerControllerQualityType |
Video capture quality. iOS only. Defaults to High |
videoExportPreset |
VideoExportPreset |
Video export preset. Deprecated upstream. iOS only |
presentationStyle |
UIImagePickerPresentationStyle |
How the picker is presented. iOS only. Defaults to automatic |
cameraType |
CameraType |
Which camera opens first. Defaults to the back camera |
preferredAssetRepresentationMode |
UIImagePickerPreferredAssetRepresentationMode |
How a picked asset is exported. iOS 14+ only. Defaults to automatic |
legacy |
boolean |
Allow picking from outside the photo library. Android only. Defaults to false |
shouldDownloadFromNetwork |
boolean |
Download from iCloud when the original is not stored locally. iOS only. Defaults to false |
Permission hooks
Section titled “Permission hooks”| Adapter | useCameraPermissions / useMediaLibraryPermissions |
|---|---|
| React | [status, requestPermission, getPermission] |
| Vue | [status, requestPermission, getPermission] |
| Solid | [status, requestPermission, getPermission], with status an accessor |
| Svelte | { status, requestPermission, getPermission } |
| Angular | CameraPermissionsService / MediaLibraryPermissionsService |
- Check
canceledbefore readingassets. The result is a union:assetsisnullwhen the user backs out. allowsEditingwithallowsMultipleSelectiononly warns. Matching upstream, the native side ignoresallowsEditingin that case instead of throwing.MediaTypeOptionsis deprecated. It still works but logs a warning pointing at the string or array form ofmediaTypes.- Android can kill your app while the camera is open. Call
getPendingResultAsync()on launch to recover a result that arrived while the activity was destroyed. - Web-only fields are dropped.
ImagePickerAsset.file,ImagePickerResult.outputand the webbase64/captureoptions are not part of the port; this package targets iOS and Android. - The camera and pickers can only be verified on a device. The headless tests fake the native module, so they prove option validation and permission delegation, not the pickers.
Common questions
Section titled “Common questions”Do I need to ask for permission before opening the library? No. Launching the image library
needs no permission request. Ask for media library permission only when you need it, for example for
videos on iOS when allowsEditing is false and videoExportPreset is the default Passthrough,
ideally before the picker so users are not surprised by a dialog after picking.
The camera does not open on the iOS Simulator. The Simulator has no camera. Test
launchCameraAsync on a device.
The app restarts, or the result never arrives, after taking a photo on Android. Android can
destroy your activity while the camera is open. Call getPendingResultAsync() on launch to recover
the result.
The image is huge, or I want a smaller file. Lower quality (0 to 1), and resize or recompress
the result with the image manipulator.
I need the file as base64 or with EXIF. Set base64: true or exif: true; both are added to
each asset.
The crop rectangle is wrong for a high-resolution photo on iOS. This is a bug in the underlying
UIImagePickerController that upstream documents and cannot fix.
Why did Android ask for RECORD_AUDIO? The camera picker can record video with sound. The
package adds that permission for it.
Sources: Expo docs: ImagePicker, expo/expo#20482 Android 13 crash, expo/expo#11752 launchImageLibraryAsync crashes on Android.
How the wrapper works
Section titled “How the wrapper works”expo-image-picker’s JS is hand-ported into this package’s core/, resolving
ExponentImagePicker through expo-modules-core rather than the expo meta-package. The launch
and permission functions are framework-agnostic and re-exported by every adapter. The permission
hooks bind the shared createPermissionHook runtime from @symbiote-native/engine in each
framework’s own idiom, so the method-dispatch plumbing lives once. The native code is never
vendored: expo-modules-autolinking resolves it from node_modules (see
the native setup guide).