ある朝、自分の壁紙アプリの App Store Connect を開いたら、ダウンロードサイズが 180MB を超えていました。Rork Max に頼んで作った新しい画面を足しただけのつもりだったのに、いつの間にか初回ダウンロードがセルラー回線の警告ラインに迫っていたのです。
原因を追うと、生成されたコードが高解像度の画像と複数のカスタムフォントをそのままバンドル直下に置いていました。Rork Max は「見た目が完成した画面」を最短で出すことには長けていますが、配信容量まで面倒を見てくれるわけではない、というのがこの朝の実感でした。
個人開発でアプリを長く運用していると、容量は静かに効いてくる指標です。ここからは、Rork Max が吐いた Swift ネイティブのコードを土台に、App Thinning と On-Demand Resources を使って初回ダウンロードを軽くするまでの工程を、実際に触った数字とともに残しておきます。
アプリ容量はどこで効いてくるのか
容量を気にする理由は、見栄えの問題ではありません。インストール直前の離脱に直結するからです。
App Store はセルラー回線でのダウンロードに上限を設けていて、これを超えると「Wi-Fi に接続してください」という警告が出ます。せっかく広告や紹介でストアまで来てくれた人が、この一枚の警告で去っていきます。私自身、容量を 90MB 台まで落としたアプリで、初回インストールの完了率が体感ではっきり改善した経験があります。
もうひとつは更新のたびにかかる通信です。毎週更新する運用をしていると、利用者は何度もダウンロードを繰り返します。1 回あたり数十 MB の差は、積もると無視できません。
App Thinning は何を自動でやってくれるのか
App Thinning は Apple が用意した仕組みで、大きく三つの要素に分かれます。まず Slicing が、端末ごとに必要な解像度のアセットだけを抜き出して配信します。次に On-Demand Resources が、初回には含めないリソースを後から取りに行けるようにします。そして以前あった Bitcode は現在は役割を終えています。
ここで誤解しやすいのは、Slicing は「アセットカタログに正しく入れていれば自動で効く」という点です。逆に言えば、生成コードのようにバンドル直下へ画像を置いてしまうと、Slicing の対象から外れて全解像度が全端末に配られます。Rork Max の出力で容量が膨らむ典型は、まさにここにあります。この落とし穴は見た目には現れないため、レポートを見るまで気づきにくいのが厄介なところです。
アセットカタログを正しく使う
最初の一手は、バンドル直下に散っている画像をアセットカタログへ移すことです。Assets.xcassets の中で各画像を 1x / 2x / 3x のスロットに割り当てると、Slicing が端末解像度に応じた 1 種類だけを配信するようになります。
// ❌ 生成直後にありがちな読み込み(バンドル直下・Slicing 対象外)
Image ( uiImage : UIImage ( named : "hero_background.png" ) ! )
// ✅ アセットカタログ経由(拡張子なし・Slicing が効く)
Image ( "hero_background" )
拡張子つきのファイル名で読み込んでいるコードを見つけたら、それはバンドル直下を指している合図です。アセットカタログに登録し直し、コードからは拡張子を外して名前だけで参照します。これだけで、3x 端末向けの巨大画像が 1x や 2x の端末へ無駄に配られる事態を回避できます。
フォントも同じ発想で見直します。本文用と見出し用に同じファミリーを別ファイルで二重同梱していたり、結局使っていないウェイトが残っていたりすることがよくあります。実際に画面で使っているウェイトだけに絞ると、それだけで数 MB が消えます。私の場合、見直しの最初にフォントから手を付けることが多く、効果のわりに作業が軽いので個人的に好んでいる順序です。
On-Demand Resources で初回ダウンロードを軽くする
Slicing で削りきれない大きなリソースは、On-Demand Resources(ODR)に逃がします。チュートリアル用の動画、追加のテーマパック、初回には不要な高解像度の壁紙群などが候補です。
ODR ではアセットに「タグ」を付け、必要になった時点で取りに行きます。Xcode のアセットカタログでリソースにタグを設定したうえで、コードからは次のように要求します。
import Foundation
final class ThemePackLoader {
private var request: NSBundleResourceRequest ?
func loadThemePack ( tag : String ) async throws {
let req = NSBundleResourceRequest ( tags : [tag])
// 進捗を UI に出したい場合は req.progress を監視します
self .request = req
let available = await req. conditionallyBeginAccessingResources ()
if available {
return // すでに端末に存在します
}
try await req. beginAccessingResources ()
}
func release () {
request ? . endAccessingResources ()
request = nil
}
}
実装でつまずきやすい点が三つあるので、順に分けて書いておきます。
取得済みかを先に確認する
conditionallyBeginAccessingResources で「すでに取得済みか」を先に確認し、無駄な再ダウンロードを回避します。これを省くと、同じパックを何度も取りに行ってしまい、ODR にした意味が薄れます。
使い終わったら解放する
使い終わったら endAccessingResources を必ず呼びます。解放しないと端末に居座り続け、容量を後から取りに行く設計の利点が消えてしまいます。ここを忘れるのは典型的なハマりどころです。
取得失敗への対処を用意する
ダウンロード中はネットワークの失敗が起こり得るので、取得失敗時に同梱の低解像度版へフォールバックする経路を必ず用意します。本番では、地下鉄の中で取得に失敗した利用者からのレビューが、この対処の有無をそのまま映し出します。
私の場合、初回には標準テーマだけを同梱し、追加の壁紙パック(合計でおよそ 40MB)を ODR に逃がす構成を推奨します。初回ダウンロードが軽くなり、追加パックは「気に入った人だけが」取りに行くので、平均の通信量も下がります。
容量予算を決めて運用する
削る作業は一度やって終わりにすると、また少しずつ膨らみます。そこで私は、アプリごとに容量の予算を決めて運用しています。下の表は、私が個人開発の壁紙アプリで使っている目安です。
区分 目安 理由
初回ダウンロード(Slicing 後) 100MB 未満 セルラー回線の警告を避け、初回完了率を守るため
同梱アセット合計 30MB 未満 UI とアイコン、最小限のフォントに絞る
ODR で配信 1 タグあたり 50MB 未満 地下や電波の弱い場所でも取得を完了させるため
インストール後サイズ 端末空き容量の負担にならない範囲 アンインストールの動機を作らない
新しい画面を足すときは、この予算に照らして次の順で判断しています。
その画像は端末解像度に依存するか。依存するならアセットカタログへ入れて Slicing に任せます。
初回起動の体験に必要か。不要で大きいなら ODR のタグに振り分けます。
同じ役割のアセットがすでに同梱されていないか。重複していれば古い方を消します。
この三段の判断を毎回その場で通すと、容量がじわじわ増える事故を防げます。判断が属人化しないので、共同作業でもぶれません。
計測しながら削る
感覚で削るのは危険なので、必ず数字を見ます。Xcode の Organizer から App Size Report を開くと、端末ごとの配信サイズと、何がどれだけ占めているかを確認できます。
# アーカイブから App Thinning 適用後のサイズを書き出す
xcodebuild -exportArchive \
-archivePath MyApp.xcarchive \
-exportPath ./thinned \
-exportOptionsPlist thinning.plist
# thinning.plist に <key>thinning</key><string><all-variants></string> を指定します
書き出した結果の App Thinning Size Report.txt を開くと、端末ごとの圧縮後・展開後サイズが並びます。私はリリースのたびにこの数字を記録し、前回からどれだけ増減したかを追っています。増えていたら、直近で足した画面のアセットを真っ先に疑います。
計測して初めて分かることもあります。あるアプリでは、未使用のローカライズ用画像が 12MB も残っていました。コード上はどこからも参照されていないのに、アセットカタログには居座っていたのです。レポートがなければ、おそらく気づかないままでした。
どこまでやるかの線引き
最後に、力の入れどころについて触れておきます。容量削減は、突き詰めれば際限がありません。けれど、利用者が体感する境目はおおむね決まっています。初回ダウンロードがセルラーの警告を越えるかどうか、起動後すぐに使い始められるかどうか。この二点を守れていれば、それ以上は費用対効果が落ちていきます。
私は、Slicing を確実に効かせること、明らかに大きいリソースだけ ODR に逃がすこと、そして容量予算を記録し続けること、この三つを最優先にしています。残りは時間に余裕があるときの磨き込みと割り切っています。
まずは手元の Rork Max プロジェクトで、拡張子つきで読み込んでいる画像が残っていないかを探してみてください。そこが、容量を取り戻す最初の一歩になります。お読みいただきありがとうございました。