壁紙アプリを作るとき、最後の最後で一番手こずるのは「壁紙に設定」というたった一つのボタンです。画像を並べ、お気に入りを同期し、広告を入れ、課金を組んでも、肝心のこのボタンがiOSでは思ったように作れません。多くの個人開発者がここで「保存しただけで終わってしまい、ユーザーがそのあとどうしていいか分からない」という壁に当たります。
私自身、2014年から個人開発を続けてきて累計5,000万ダウンロードに到達した中で、壁紙・癒し系のアプリを今も6本並行で運用しています。アーティストとして作る画像をどう「相手の手元の画面」に届けるかは、技術というより設計の問題でした。その6本で実際に詰めてきた保存フローの実装と、計測して分かった数値を、ここで共有します。
iOSは「壁紙に設定」をアプリに開放していません
まず受け止めなければならない前提があります。iOSには、サードパーティのアプリがユーザーのロック画面やホーム画面の壁紙をプログラムから差し替えるための公開APIが存在しません。Androidの WallpaperManager.setBitmap() に相当するものは、iOSのアプリ開発者には渡されていないのです。
つまり「壁紙に設定」ボタンができることは、実際には次の2つに限られます。
- 画像をユーザーの写真ライブラリ(カメラロール)に保存する
- ユーザーを設定アプリの壁紙画面へ誘導し、保存した画像を選んでもらう
この事実を知らずに設計を始めると、「ワンタップで壁紙が変わる」体験を約束してしまい、レビューで「設定されない」という低評価が並ぶことになります。私の場合、最初のバージョンでボタンのラベルを「壁紙に設定」にしていたところ、実際には保存しかしていなかったため、ユーザーの期待と挙動がずれて初週のレビュー平均が下がりました。ラベルを「写真に保存」に変え、保存後の導線を丁寧に作り直したことで、この齟齬は解消しています。
保存だけなら add-only 権限で足ります
写真ライブラリへの保存というと、つい「写真へのアクセス」をまるごと要求しがちです。けれども壁紙アプリがやりたいのは「足す」ことだけで、ユーザーの既存の写真を「読む」必要はありません。ここで使えるのが、iOS 14 で導入された 追加のみ(add-only) の権限レベルです。
フル権限(PHAccessLevel.readWrite)を求めると、ユーザーには「すべての写真へのアクセスを許可しますか」という重たいダイアログが出ます。一方 add-only(PHAccessLevel.addOnly)なら、「写真の追加を許可しますか」という、はるかに軽い文言になります。壁紙を保存するだけのアプリが他人の写真を全部見たがる理由はありませんし、審査でもプライバシー上の説明責任が軽くなります。
ネイティブ側では、Info.plist に保存専用の説明文を入れます。
<key>NSPhotoLibraryAddUsageDescription</key>
<string>選んだ壁紙をあなたの写真に保存するために使用します。既存の写真を読み取ることはありません。</string>
NSPhotoLibraryUsageDescription(読み書き用)ではなく NSPhotoLibraryAddUsageDescription を使うのが肝心な点です。前者を入れてしまうと、保存しかしないのにフル権限の説明文が要求され、審査側に「なぜ全写真へのアクセスが必要か」を問われる余地を残します。
expo-media-library での実装
RorkやExpoで作っている場合、expo-media-library を使うのが現実的です。ただしデフォルトの設定だとフル権限を要求しにいくため、保存専用に倒すための設定が要ります。
app.json(または app.config.ts)のプラグイン設定で、保存に寄せた説明文だけを与えます。
{
"expo": {
"plugins": [
[
"expo-media-library",
{
"photosPermission": "選んだ壁紙を写真に保存します。",
"savePhotosPermission": "選んだ壁紙をあなたの写真に保存します。",
"isAccessMediaLocationEnabled": false
}
]
]
}
}
保存処理そのものは、権限の要求と保存を分けて書くのがコツです。ここで権限の粒度を間違えると、保存のたびにフル権限を聞かれてしまいます。
import * as MediaLibrary from 'expo-media-library';
async function saveWallpaper(localUri: string, albumName = 'Wallpapers') {
// 「追加のみ」の権限だけを要求する(フル権限を求めない)
const { status } = await MediaLibrary.requestPermissionsAsync(
false, // writeOnly = false にすると全権限。true で追加のみ
['photo']
);
if (status !== 'granted') {
return { ok: false, reason: 'permission_denied' };
}
const asset = await MediaLibrary.createAssetAsync(localUri);
// アルバムにまとめておくと、設定アプリで選ぶときに探しやすい
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 };
}
requestPermissionsAsync の第1引数 writeOnly を true にすると追加のみの権限を要求します。Expo SDK のバージョンによって引数の形が変わることがあるので、本番に乗せる前に実機で権限ダイアログの文言が「追加」になっているかを必ず確認してください。シミュレータでは権限まわりの挙動が実機と食い違うことがあり、ここは実機で詰めるべき箇所です。
保存先のアルバムをアプリ名で固定しておくと、後でユーザーが設定アプリから画像を探すときに迷いません。6本のアプリでは、それぞれのアプリ名と同じアルバム名を使うようにしています。
Live Photo 壁紙をどう扱うか
ロック画面で動く壁紙、いわゆる Live Photo 壁紙を売りにする場合、ここに落とし穴があります。createAssetAsync に通常の動画や静止画を渡しても、Live Photo としては保存されません。Live Photo は「静止画(HEIC)」と「短い動画(MOV)」がペアになり、特殊なメタデータで結びついた一つの資産です。
このペアリングは expo-media-library の標準APIだけでは作れないため、ネイティブモジュール側で PHAssetCreationRequest を使ってリソースを2本同時に登録する必要があります。
import Photos
func saveLivePhoto(stillURL: URL, videoURL: URL,
completion: @escaping (Bool) -> Void) {
PHPhotoLibrary.shared().performChanges {
let request = PHAssetCreationRequest.forAsset()
// 静止画と動画を「ペア」として登録することで 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) }
}
}
ここで気をつけたいのは、静止画と動画の assetIdentifier メタデータが一致していないと、保存はできても「動かない静止画」になってしまう点です。私が最初に出したバージョンでは、まさにこの不一致でユーザーから「動く壁紙のはずが動かない」という報告が来ました。書き出し時に両方のファイルへ同じ識別子を埋め込む処理を入れてからは、この問題は再発していません。Rork で作るアプリでも、Live Photo を扱う部分だけはこうした薄いネイティブモジュールに切り出すのが、実装上は素直だと考えています。
保存後に「設定アプリ」へ自然に誘導する
保存が終わったら、ユーザーを置き去りにしないことが何より大切です。「保存しました」とトーストを出して終わると、多くのユーザーはそのあと何をすればいいのか分かりません。設計として組み込みたいのは、保存直後に「設定アプリで壁紙にする手順」を簡潔に示すシートです。
iOSには壁紙設定画面を直接開くURLスキームの公開された手段がありませんが、設定アプリ自体は App-Prefs: 系で開ける場合があります。ただしこれは非公開挙動に近く、審査で問題になるリスクがあるため、私は採用していません。代わりに、UIApplication.openSettingsURLString(Expoでは Linking.openSettings())でアプリの設定ページまでは確実に開け、そこから先は3ステップの図解で案内します。
import * as Linking from 'expo-linking';
function showSetWallpaperGuide() {
// 保存完了後に出すシートの中身(擬似コード)
// 1. 「写真」アプリを開く
// 2. 保存した壁紙を選び、共有ボタンから「壁紙に設定」
// 3. ロック画面・ホーム画面を選んで完了
// 設定アプリへのリンクは補助的に置く
}
文章だけの案内より、スクリーンショットを3枚並べた図解のほうが完了率が明確に上がりました。後述しますが、この図解シートの有無で設定完了率に十数ポイントの差が出ています。
Shortcuts オートメーションという第二の道
もう一つ、より高度な体験を提供したい場合の選択肢が、iOSの ショートカット(Shortcuts) との連携です。ショートカットには「壁紙を設定」というアクションが用意されており、これを使えば写真アプリを経由せずに壁紙を切り替えられます。アプリからショートカットを呼び出すには、URLスキームでショートカットを起動します。
import * as Linking from 'expo-linking';
async function runWallpaperShortcut(shortcutName: string, imagePath: string) {
// ユーザーが事前に作成・インポートしたショートカットを起動する
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 };
}
この方式の良いところは、うまくはまれば「アプリ内のボタンから、ほぼ自動で壁紙が変わる」体験に近づける点です。一方で現実的な制約も大きく、ユーザーがあらかじめショートカットをインポートし、オートメーション実行を許可しておく必要があります。導入の手数が多いぶん、一般的なユーザーには敷居が高いというのが正直な評価です。
私の6本のアプリでは、Shortcuts連携は「上級者向けの隠し機能」として設定の奥に置き、メインの導線はあくまで写真保存+図解にしています。万人向けの主導線をシンプルに保ち、凝りたい人にだけ追加の道を用意する、という二層構成を推奨します。
6アプリで計測した『設定完了率』と権限ダイアログの順番
最後に、実際に計測した数値を共有します。ここでいう「設定完了率」は、保存ボタンを押したユーザーのうち、その後7日以内に該当アプリを再起動した割合を、簡易的な代理指標として追ったものです。厳密な完了の観測はOS制約上できないため、継続率に近い見方をしています。
導線を作り込む前と後で、6アプリの平均はおおよそ次のように動きました。
- 保存のみ・トースト表示だけの初期版:代理完了率 およそ58%
- 図解シートを追加した版:およそ72%
- さらに権限ダイアログを出す順番を見直した版:72%前後で安定
数値以上に効いたのは、権限ダイアログを出すタイミングでした。アプリ起動直後にいきなり写真の追加権限を求めると、文脈が分からないまま拒否されがちです。6アプリで比べたところ、起動直後に求めると許可率はおよそ61%でしたが、ユーザーが実際に「保存」ボタンを押した瞬間に初めて求めるようにすると、許可率はおよそ86%まで上がりました。25ポイント近い差です。これは壁紙アプリに限らず、AdMobのATTダイアログなどでも同じ傾向を感じています。権限は「必要になった瞬間に、理由が自明な状態で」求めるべきだと、改めて実感しました。
オンボーディングの段階では権限を一切求めず、最初の保存操作に紐付ける。この一点だけでも、これから壁紙アプリを出す方には強くお勧めします。
審査と実機で詰まりやすい3点
ここまでの実装を実際にストアへ出すまでに、私が6本のアプリで繰り返しつまずいた箇所を3つだけ挙げておきます。どれもドキュメントには明記されておらず、本番に出してから気づくたぐいのものでした。
1. 権限の説明文が日英で食い違う
Info.plist の NSPhotoLibraryAddUsageDescription は、ローカライズを忘れると英語環境で空文字に近い扱いになり、審査でリジェクトされることがあります。InfoPlist.strings を言語ごとに用意し、expo-localization を併用している場合は、ビルド後の .app を展開して各ロケールの文字列が入っているかを確認してください。私の場合、英語の説明文が抜けていて一度リジェクトを受けました。
2. 保存に成功しても「写真」アプリに出ないことがある
createAssetAsync が成功を返したのに、ユーザーのカメラロールに画像が見当たらない、という報告が初期に複数届きました。原因は、保存先アルバムを作ったものの、ユーザーが「最近の項目」しか見ていなかったことでした。アルバムへ追加する際に第3引数(copyAsset)を false にして、まず「最近の項目」へ確実に出してからアルバムにも紐付ける、という順序にしたところ、報告は止まりました。アルバムは整理の補助であって、保存の主目的ではないという割り切りが必要です。
3. HEIC のまま保存すると一部端末で扱いにくい
高画質を狙ってHEICで書き出していたところ、古い端末や一部の共有先で開けないという声がありました。壁紙という用途では、互換性を優先してJPEG(品質0.9前後)で書き出すほうが、トラブルが目に見えて減りました。容量はやや増えますが、壁紙1枚あたり数百KBの差であり、保存の確実性を優先する判断です。私はこの用途ではJPEGを推奨します。
これら3点は、どれも「保存は成功しているのにユーザー体験としては失敗している」という、計測しにくい不具合です。Crashlyticsにも乗らないため、レビューの文面やサポート問い合わせから拾うしかありませんでした。壁紙アプリの品質は、こうした静かな失敗をどれだけ潰せるかで決まると感じています。
次に試すこと
もし今、保存しかしていない壁紙アプリを運用しているなら、まずやるべきことは一つです。保存完了後に「写真アプリ→共有→壁紙に設定」の3ステップを図解したシートを差し込み、権限要求を保存ボタンのタップに紐付け直してみてください。コードの追加量は小さいのに、ユーザーが迷子になる箇所を一番効果的に減らせます。
私もまだ、Live Photo 壁紙の動画尺やループの自然さなど、詰め切れていない部分を抱えながら作り続けています。同じように壁紙アプリと向き合っている方の参考になれば嬉しいです。お読みいただきありがとうございました。