One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

ImagePicker

Let people pick photos and videos from their library or take new ones with the system camera

One.ImagePicker opens the system’s own screens for choosing media: the photo picker for the library, and the camera app for a new photo or video. People pick or capture there, and your app gets back files it can read, upload, or show.

The library needs no permission on either platform, because the system picker runs outside your app and hands over only what people choose. The camera needs the camera permission, which the picker asks for the first time it opens. For a live preview inside your own layout, use Camera preview instead.

Usage

import { useState } from 'react'
import { Button, Image, View } from 'react-native'
import { One } from 'one'
function AvatarPicker() {
const [uri, setUri] = useState<string | null>(null)
return (
<View>
{uri && <Image source={{ uri }} style={{ width: 96, height: 96, borderRadius: 48 }} />}
<Button
title="Choose photo"
onPress={async () => {
const result = await One.ImagePicker.launchLibrary({ allowsEditing: true, quality: 0.8 })
if (!result.canceled) setUri(result.assets[0].uri)
}}
/>
</View>
)
}

To use the camera, declare why in your app config before prebuild. The microphone line is needed only to record video on iOS:

import { one } from 'one/vite'
export default {
plugins: [
one({
native: {
app: {
name: 'MyApp',
imagePicker: {
camera: 'Take a profile photo.',
microphone: 'Record videos with sound.',
},
},
},
}),
],
}

How it works

The library on iOS. launchLibrary presents PHPickerViewController filtered to images, videos, or both, keeping the order people tapped. On iOS 17 and later it asks for the most compatible version of each file, so HEIC photos arrive as JPEG. The picker closes first, then One copies each file, so a slow copy never looks like a frozen picker.

The library on Android. It opens the system photo picker. On devices without it, One falls back to the documents picker filtered to the same media types. A selectionLimit of 0 means the system maximum, which on Android 13 and later is the platform’s own limit.

The camera. launchCamera opens UIImagePickerController on iOS and the device’s camera app on Android. It takes a photo unless mediaTypes is only 'videos', in which case it records one video. The first launch asks for the camera permission. A denied permission, a device with no camera, and backing out all resolve { canceled: true, assets: null }, so a cancelled result does not tell you which happened; read getCameraPermissions when it matters.

Copies, not originals. Every result is a copy in the app’s cache folder, in a one-native-image-picker folder, returned as a file:// URI. You can read, upload, or move it without any photo library access, and the original stays untouched.

Cropping. allowsEditing shows a crop screen after one image is picked or taken. iOS uses the system’s square crop. Android uses One’s own crop screen, with a frame locked to aspect when you pass one.

Quality. quality below 1 re-encodes images as JPEG at that quality. On iOS, PNG and BMP picks keep their pixels unchanged; Android re-encodes them too, so a transparent PNG loses its transparency. Videos are never re-encoded by the library picker; for the camera, videoQuality picks the recording quality.

Web. Both calls open a hidden file input. launchCamera adds capture="environment", which opens the camera on phones and a file dialog on desktops. Results are blob URLs, and quality below 1 re-encodes through a canvas. Permission calls always read granted.

API reference

FunctionResolves
launchLibrary(options?)The picked assets, or canceled
launchCamera(options?)One captured asset, or canceled
getCameraPermissions()The status, without asking
requestCameraPermissions()The status, asking if undecided
ImagePickerOptionsTypeDescription
mediaTypesImagePickerMediaType | ImagePickerMediaType[] = "images"images, videos, or both.
ImagePickerMediaType | ImagePickerMediaType[] = "images"images, videos, or both.
selectionLimitnumber = 1The most assets to pick. 0 is the system maximum. The camera takes one.
number = 1The most assets to pick. 0 is the system maximum. The camera takes one.
allowsEditingboolean = falseShow a crop screen. One image only.
boolean = falseShow a crop screen. One image only.
aspect[number, number]Lock the Android crop frame to x by y.
[number, number]Lock the Android crop frame to x by y.
qualitynumber = 1JPEG quality from 0 to 1. Images only.
number = 1JPEG quality from 0 to 1. Images only.
videoMaxDurationnumber = 0Longest camera video, in seconds. 0 is no limit.
number = 0Longest camera video, in seconds. 0 is no limit.
videoQualityImagePickerVideoQuality = "high"high, medium, or low, for camera video.
ImagePickerVideoQuality = "high"high, medium, or low, for camera video.
ImagePickerAssetTypeDescription
uristringThe file URI of the copy.
stringThe file URI of the copy.
widthnumberWidth in pixels as displayed, or 0 if unknown.
numberWidth in pixels as displayed, or 0 if unknown.
heightnumberHeight in pixels as displayed, or 0 if unknown.
numberHeight in pixels as displayed, or 0 if unknown.
mimeTypestringThe file type, when known.
stringThe file type, when known.
fileNamestringThe original file name, when known.
stringThe original file name, when known.
fileSizenumberThe size of the copy in bytes.
numberThe size of the copy in bytes.
durationnumberVideo length in milliseconds.
numberVideo length in milliseconds.
typeImagePickerAssetTypeimage or video.
ImagePickerAssetTypeimage or video.

A result is { canceled: false, assets } or { canceled: true, assets: null }. A permission response has status (granted, denied, or undetermined), granted, and canAskAgain.

Platform differences

iOSAndroidWeb
LibraryPhoto pickerPhoto pickerFile dialog
CameraSystem cameraCamera appPhone camera
Crop shapeSquareFree or aspectNone
Camera video qualityHigh, medium, lowHigh or lowBrowser’s
ResultsCache filesCache filesBlob URLs

Limits and gotchas

Examples

Pick several photos for an upload

import { One } from 'one'
async function pickAndUpload(upload: (form: FormData) => Promise<void>) {
const result = await One.ImagePicker.launchLibrary({ selectionLimit: 5, quality: 0.7 })
if (result.canceled) return 0
const form = new FormData()
for (const asset of result.assets) {
const blob = await (await fetch(asset.uri)).blob()
form.append('photos', blob, asset.fileName ?? 'photo.jpg')
}
await upload(form)
return result.assets.length
}

fetch reads the cache file on iOS and Android and the blob URL on web, so one loop builds the upload on every platform.

Take a photo, explaining a denial

import { Alert } from 'react-native'
import { One } from 'one'
async function takePhoto() {
const permission = await One.ImagePicker.requestCameraPermissions()
if (!permission.granted) {
if (!permission.canAskAgain) {
Alert.alert('Camera is off', 'Turn on camera access in Settings.', [
{ text: 'Not now', style: 'cancel' },
{ text: 'Open Settings', onPress: () => One.openSettings() },
])
}
return null
}
const result = await One.ImagePicker.launchCamera({ quality: 0.8 })
return result.canceled ? null : result.assets[0]
}

Asking first separates a refusal from backing out of the camera, which launchCamera alone reports the same way.

Record a short clip

import { One } from 'one'
async function recordClip() {
const result = await One.ImagePicker.launchCamera({
mediaTypes: 'videos',
videoMaxDuration: 15,
videoQuality: 'medium',
})
if (result.canceled) return null
const [clip] = result.assets
return { uri: clip.uri, seconds: (clip.duration ?? 0) / 1000 }
}

The camera stops on its own at 15 seconds. duration is in milliseconds and can be missing when the platform does not report it.

Edit this page on GitHub.