●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Getting Users All the Way to 'Set as Wallpaper' on iOS — Save-to-Photos Permissions and Shortcuts
iOS apps cannot set the wallpaper directly. Here is how I handle add-only photo permissions, Live Photo saving, guiding users to Settings, and Shortcuts automation, with real numbers from running six wallpaper apps.
When you build a wallpaper app, the single hardest thing to ship is one tiny button: "Set as Wallpaper." You can lay out the gallery, sync favorites, wire up ads, and add a paywall — but on iOS that one button never behaves the way you expect. This is where a lot of indie developers hit a wall: the app saves the image and then leaves the user wondering what to do next.
I have been building apps on my own since 2014, and across that time my Dolice apps have passed 50 million cumulative downloads. I still run six wallpaper and calming-image apps in parallel. As someone who makes images as an artist, the real problem was never the code — it was the design of how a picture actually reaches someone's screen. This article shares the save flow I have refined across those six apps, along with the numbers I measured along the way.
iOS Does Not Let Apps Set the Wallpaper
Start by accepting one constraint: iOS exposes no public API for a third-party app to change the user's lock-screen or home-screen wallpaper programmatically. There is no equivalent of Android's WallpaperManager.setBitmap() available to iOS app developers.
So a "Set as Wallpaper" button can really only do two things:
Save the image into the user's photo library (camera roll)
Guide the user into the Settings app's wallpaper screen to pick the saved image
If you design without knowing this, you promise a "one tap and the wallpaper changes" experience, and your reviews fill with "it doesn't actually set anything." In my first version the button said "Set as Wallpaper" while it only saved the file. The mismatch between expectation and behavior pulled down my first-week review average. Renaming it to "Save to Photos" and rebuilding the post-save guidance fixed the gap.
Saving Only Needs add-only Permission
People reach for full "Photos access" when all they want is to save. But a wallpaper app only needs to add; it never needs to read the user's existing photos. That is exactly what the add-only access level, introduced in iOS 14, is for.
Asking for full access (PHAccessLevel.readWrite) shows the heavy "Allow access to all your photos?" dialog. Add-only (PHAccessLevel.addOnly) shows the much lighter "Allow adding to Photos?" wording instead. A wallpaper app has no reason to want to see all of someone's pictures, and review scrutiny over privacy is lighter when you ask for less.
On the native side, give a save-specific usage string in Info.plist:
<key>NSPhotoLibraryAddUsageDescription</key><string>Used to save your chosen wallpaper to your photos. We never read your existing photos.</string>
The key detail is using NSPhotoLibraryAddUsageDescription (add-only) rather than NSPhotoLibraryUsageDescription (read/write). If you include the latter, you are declaring full-access intent for an app that only saves, which leaves room for a reviewer to ask why you need access to every photo.
✦
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
✦Saving with PHAccessLevel.addOnly so you never ask for full photo-library access
✦Why Live Photo wallpapers end up static, and the PHLivePhoto branch that fixes it
✦A measured 72% save-to-set completion rate across six apps, and the permission-timing change that moved it
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.
If you are building with Rork or Expo, expo-media-library is the practical choice. By default it requests full access, so you need to configure it toward save-only behavior.
In your app.json (or app.config.ts) plugin config, give only the save-oriented strings:
{ "expo": { "plugins": [ [ "expo-media-library", { "photosPermission": "Save your chosen wallpaper to Photos.", "savePhotosPermission": "Save your chosen wallpaper to your photos.", "isAccessMediaLocationEnabled": false } ] ] }}
For the save itself, split permission request from save. Get the permission granularity wrong and you will be asked for full access on every save.
import * as MediaLibrary from 'expo-media-library';async function saveWallpaper(localUri: string, albumName = 'Wallpapers') { // Request the add-only permission, not full access const { status } = await MediaLibrary.requestPermissionsAsync( true, // writeOnly = true requests add-only ['photo'] ); if (status !== 'granted') { return { ok: false, reason: 'permission_denied' }; } const asset = await MediaLibrary.createAssetAsync(localUri); // Grouping into an album makes it easier to find in Settings later const album = await MediaLibrary.getAlbumAsync(albumName); if (album == null) { await MediaLibrary.createAlbumAsync(albumName, asset, false); } else { await MediaLibrary.addAssetsToAlbumAsync([asset], album, false); } return { ok: true, assetId: asset.id };}
Setting the first writeOnly argument of requestPermissionsAsync to true requests add-only access. The argument shape can shift between Expo SDK versions, so before you ship, verify on a real device that the dialog says "Add." Simulator permission behavior can diverge from real hardware, so this is a spot to nail down on-device.
Pinning the album name to your app name keeps users from getting lost when they later look for the image in Settings. Across the six apps I use an album name that matches each app.
Handling Live Photo Wallpapers
If your selling point is animated lock-screen wallpapers — Live Photos — there is a trap here. Passing an ordinary video or still to createAssetAsync does not produce a Live Photo. A Live Photo is a single asset where a still (HEIC) and a short movie (MOV) are paired through special metadata.
That pairing cannot be created with the standard expo-media-library API, so you need a native module that registers both resources at once with PHAssetCreationRequest.
import Photosfunc saveLivePhoto(stillURL: URL, videoURL: URL, completion: @escaping (Bool) -> Void) { PHPhotoLibrary.shared().performChanges { let request = PHAssetCreationRequest.forAsset() // Registering the still and video as a pair makes a Live Photo request.addResource(with: .photo, fileURL: stillURL, options: nil) request.addResource(with: .pairedVideo, fileURL: videoURL, options: nil) } completionHandler: { success, error in DispatchQueue.main.async { completion(success) } }}
The thing to watch is that if the assetIdentifier metadata of the still and the video do not match, the save succeeds but you get a "still that won't move." My first release shipped exactly this mismatch, and users reported that the "live" wallpaper did not animate. After embedding the same identifier into both files at export time, the problem stopped recurring. Even in a Rork-built app, carving out just the Live Photo part into a thin native module is the most straightforward approach.
Guiding Users to Settings After Saving
Once the save finishes, the most important thing is not to abandon the user. Showing a "Saved" toast and stopping there leaves many people unsure what to do. What you want to build in is a concise sheet, shown right after saving, that explains how to set the wallpaper in Settings.
iOS has no public way to deep-link directly into the wallpaper screen. The Settings app itself can sometimes be opened via App-Prefs: URLs, but that is close to undocumented behavior and carries review risk, so I do not use it. Instead I open the app's own settings page reliably with Linking.openSettings() and then walk the user through three illustrated steps.
import * as Linking from 'expo-linking';function showSetWallpaperGuide() { // Contents of the sheet shown after a successful save (pseudo) // 1. Open the Photos app // 2. Pick the saved wallpaper, then "Use as Wallpaper" from share // 3. Choose lock/home screen and finish // Keep the Settings link as a secondary helper}
Three screenshots beat a wall of text by a clear margin on completion. As I will show below, the presence of this illustrated sheet moves the completion rate by more than ten points.
Shortcuts Automation as a Second Path
There is one more option for a more advanced experience: integrating with iOS Shortcuts. Shortcuts ships a "Set Wallpaper" action, and using it can switch the wallpaper without going through the Photos app. To call a shortcut from your app, launch it with a URL scheme.
import * as Linking from 'expo-linking';async function runWallpaperShortcut(shortcutName: string, imagePath: string) { // Run a shortcut the user has imported and authorized ahead of time const url = `shortcuts://run-shortcut?name=${encodeURIComponent(shortcutName)}` + `&input=${encodeURIComponent(imagePath)}`; const canOpen = await Linking.canOpenURL(url); if (!canOpen) { return { ok: false, reason: 'shortcuts_unavailable' }; } await Linking.openURL(url); return { ok: true };}
The upside is that, when it works, you get close to "tap a button in the app and the wallpaper changes almost automatically." The real-world constraints are heavy, though: the user has to import the shortcut in advance and allow automation to run. Honestly, the setup cost makes this too high a bar for a typical user.
In my six apps, I keep Shortcuts integration as an advanced, tucked-away option deep in settings, and keep the main path firmly on save-plus-illustration. I recommend a two-layer structure: keep the mainstream path simple, and offer the extra route only to people who want to tinker.
Pitfalls in Review and on Real Devices
Three things tripped me up repeatedly across the six apps on the way to the store. None of them is spelled out in the docs; they are the kind you only notice after shipping.
First, the permission strings drift between languages. If you forget to localize NSPhotoLibraryAddUsageDescription, English builds end up with an almost-empty string and can get rejected. Prepare InfoPlist.strings per language and verify the strings are present in each locale of the built .app. I took a rejection once for a missing English description.
Second, a successful save sometimes does not show up in Photos. Early on I got reports that an image was nowhere in the camera roll even though createAssetAsync returned success. The cause was that I created an album but users only looked at "Recents." Making the asset land in Recents first and then linking it to the album stopped the reports. The album is an organizing aid, not the point of the save.
Third, saving as HEIC can be awkward on some devices. I exported HEIC for quality, but older devices and some share targets could not open it. For wallpapers I now recommend exporting JPEG at around quality 0.9; compatibility problems dropped visibly, at the cost of a few hundred KB per image.
What to Try Next
If you run a wallpaper app today that only saves, do one thing first: insert an illustrated three-step sheet ("Photos app → share → Use as Wallpaper") right after the save, and re-bind the permission request to the save-button tap. The code is small, and it removes the biggest place users get lost.
I am still building with unfinished pieces of my own — the length and loop of Live Photo videos, for instance. If this helps someone working on their own wallpaper app, I am glad. Thanks for reading.
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.