●MAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks AR/LiDAR scanning, Metal 3D, widgets, Live Activities, HealthKit, and more●FUNDING — Rork raised $2.8M from a16z, now drawing 743k+ monthly visits at an 85% growth rate●RN — Standard Rork generates iOS and Android apps together using React Native (Expo)●FOCUS — Rork focuses solely on native mobile apps, setting it apart from web-first Bolt and Lovable●PRICING — Free to start, paid plans from $25/mo, with Rork Max at $200/mo and two-click App Store publishing●MAX — Rork Max builds native Swift apps for iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●NATIVE — Rork Max unlocks AR/LiDAR scanning, Metal 3D, widgets, Live Activities, HealthKit, and more●FUNDING — Rork raised $2.8M from a16z, now drawing 743k+ monthly visits at an 85% growth rate●RN — Standard Rork generates iOS and Android apps together using React Native (Expo)●FOCUS — Rork focuses solely on native mobile apps, setting it apart from web-first Bolt and Lovable●PRICING — Free to start, paid plans from $25/mo, with Rork Max at $200/mo and two-click App Store publishing
Handling iOS Limited Photo Library Access in a Rork (Expo) App
Handle iOS limited photo library access (selected photos only) correctly in a Rork (Expo) app. Covers the three states of full / limited / denied, designing a screen that works from the selected subset, and a path to add more photos, all with working code.
I got a request on a wallpaper app — "I'd like to use my own photos as backgrounds" — and added photo library access. It worked fine on my test device, yet one user reported, "I selected photos but nothing shows up in the app." The cause: at the iOS permission dialog, they had chosen "Select Photos" rather than "Allow Access to All Photos." My app was built assuming full access, reading the whole library, so it could not correctly handle the few photos the user had picked.
iOS limited access (selected photos only) is exactly the option privacy-minded users tend to pick. Using a Rork-generated Expo app, this walkthrough lays out a design that builds limited access in as the assumption, not the exception.
Not "was it granted" but "how much was granted"
Treat the photo permission as a granted boolean and you drop limited access. The iOS photo permission effectively has three states.
State
User's choice
What the app can do
Correct path
Full access
All Photos
Read the whole library
Normal grid
Limited access
Select Photos
Read only the chosen photos
Show the selection + an "add more" path
Denied
Don't Allow
Read nothing
Guide to the Settings app
The pitfall: even limited access succeeds as a permission. Look only at granted === true and you cannot tell full from limited. Try to read the whole library under limited access and only the few chosen photos come back — which, depending on the implementation, looks "empty." Mistake that for denial and you show the user an off-target "please allow in Settings," confusing them.
Receive the three states correctly
Expo's expo-media-library returns information that distinguishes full from limited. Read accessPrivileges (iOS) to decide all / limited / none.
// photos/permission.tsimport * as MediaLibrary from "expo-media-library";export type PhotoAccess = "all" | "limited" | "denied";export async function requestPhotoAccess(): Promise<PhotoAccess> { // Pass false for writeOnly to request read permission const res = await MediaLibrary.requestPermissionsAsync(false); return normalize(res);}export async function getPhotoAccess(): Promise<PhotoAccess> { const res = await MediaLibrary.getPermissionsAsync(false); return normalize(res);}function normalize(res: MediaLibrary.PermissionResponse): PhotoAccess { if (res.status !== "granted") return "denied"; // iOS: accessPrivileges is "limited" under limited access // On Android or full-access iOS it is "all" (or undefined) const priv = (res as any).accessPrivileges as string | undefined; if (priv === "limited") return "limited"; return "all";}
The false argument to requestPermissionsAsync(false) means "not write-only" — that is, request read access too. Get this wrong and you cannot read, producing a hard-to-diagnose bug.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Switch to a screen design that works from the selected subset, treating limited access as the assumption rather than full access
✦Receive permission not as a granted boolean but as three states (all / limited / denied), with the right path for each
✦Build a path to re-pick more photos under limited access, and a design that never mistakes an empty selection for a denied permission
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Make the screen work from "only the selected photos"
Under limited access, getAssetsAsync returns only the photos the user selected. Accept that as the assumption, not a flaw, and design the screen to work from the selection. Ideally the same code runs for both full and limited.
// photos/usePhotos.tsimport { useCallback, useEffect, useState } from "react";import * as MediaLibrary from "expo-media-library";import { getPhotoAccess, PhotoAccess } from "./permission";export function usePhotos() { const [access, setAccess] = useState<PhotoAccess>("denied"); const [assets, setAssets] = useState<MediaLibrary.Asset[]>([]); const load = useCallback(async () => { const a = await getPhotoAccess(); setAccess(a); if (a === "denied") { setAssets([]); return; } // Same call for full and limited. Under limited, only the chosen ones return const page = await MediaLibrary.getAssetsAsync({ mediaType: "photo", sortBy: [["creationTime", false]], first: 200, }); setAssets(page.assets); }, []); useEffect(() => { load(); }, [load]); return { access, assets, reload: load };}
The screen varies what it shows by access. The important thing: when limited returns an empty assets, do not treat it as denied. Empty means nothing has been selected yet, so show an "add photos" path.
Give limited-access users a way to re-pick photos later. iOS has a mechanism to re-present the limited-library selection screen, callable from Expo via presentPermissionsPickerAsync.
// photos/presentLimitedPicker.tsimport * as MediaLibrary from "expo-media-library";import { Platform } from "react-native";export async function presentLimitedPicker(): Promise<void> { if (Platform.OS !== "ios") return; // this path is for iOS limited access // Show the screen to re-pick photos for the limited library if (typeof (MediaLibrary as any).presentPermissionsPickerAsync === "function") { await (MediaLibrary as any).presentPermissionsPickerAsync(); }}
The key is to always call reload() right after re-picking. iOS updates the accessible set the moment the picker closes, but the assets your app holds are stale. Skip the reload and the photos the user just added don't appear, making the feature feel "broken."
Also, prompting to add photos on every launch is annoying and runs against the wishes of a user who chose limited access. A modest, always-present banner, with the picker shown only when they actively tap "add photos," strikes the right distance.
Rollout steps
When retrofitting this into existing photo features, this order is safe. I followed it myself when adding "use your own photo as a background" to my indie wallpaper app.
Centralize permission acquisition in requestPhotoAccess, returning the three states all / limited / denied.
Replace loading with usePhotos, rebuilding the screen around the assumption that limited access returns only the chosen photos. Confirm that limited plus empty is not treated as denied.
Show the "add photos" path (presentLimitedPicker then reload) only under limited access, and verify on a real device that re-picks are reflected.
This way you can verify behavior at each step and avoid the pitfall of mistaking limited access for denial. Photo handling is scrutinized in App Store privacy review, and as with ad SDKs like AdMob, careful permission handling pays off in your rating.
Pitfalls and notes from production
A few things from doing this for real. First, if you develop with full access on your test device, you basically never reproduce this. I didn't at first either. Limited access doesn't switch back until you explicitly set it to "Select Photos" again in the Settings app, so during testing you must deliberately set it to limited and verify. Just making that a habit kills bugs like the reported one before shipping.
Second, the wording. A one-size message — "please grant access" — rings off-target for a limited-access user. I changed the limited-access banner to a state-aware note: "Only the photos you chose are available. Tap 'Add Photos' to include more." When users understand the result of their own choice, dissatisfaction is less likely to turn into a support ticket.
Finally, a privacy-respecting design comes back as trust over the long run. A user who goes out of their way to choose limited access watches how the app behaves. Honor that choice and create a state where "it feels good with just what I picked," and reviews receive it warmly. Permissions are an unglamorous area, but the more carefully you face them, the more it pays off.
The crux of photo permission is not to think in a granted binary. Put the three states — full, limited, denied — in your assumptions from the start, and limited access stops being an exception and becomes an ordinary option you handle gracefully.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.