これまで個人開発のアプリはFlutterで作ってきました。AI画像変換アプリも、写真から庭を育てるアプリも、Flutterです。

でも今回作った猫のアルバムアプリは、SwiftUIネイティブにしました。そして出来上がってみたら、これまで当たり前に用意していたものがまるごと要らなくなっていました。バックエンドも、ログインも、サーバー側の台帳も、課金基盤も。外部サービスはゼロです。

この記事では、ネイティブで作って実際に得たものと、その代わりに何を差し出したのかを書きます。

この記事で分かること

  • ネイティブにしたら消えたものの一覧(Flutter版2本との実比較)
  • SwiftData + CloudKitでバックエンドが要らなくなる条件
  • 実際に効いた4つの具体例(データ復元・写真アクセス・ウィジェット・画像圧縮)
  • 差し出したもの(iOS専用・CloudKit深依存)と、踏んだ罠

結論: 最大のメリットは速度でも見た目でもなく「借りるものが減る」こと

  • ネイティブの一番の価値は、OSが用意しているものをそのまま使えることでした。同期も認証もストレージも、Appleの中に最初から入っている
  • BaaSの記事で「BaaSは作らなくていいものリスト」と書きましたが、条件が揃うと借りること自体が要らなくなります
  • ただし成立条件があります。サーバー側で走らせる処理がなく、データがユーザー個人のもので完結するアプリであること
  • 代償はiOS専用になることCloudKitへの深い依存。承知の上で選ぶ性質のものです

今回の構成

領域 採用
UI SwiftUI
ローカルDB SwiftData
クラウド同期 CloudKit プライベートDB(SwiftDataの自動同期)
写真アクセス PhotoKit(自前の年別ピッカー)
課金 StoreKit 2を直接
最低OS iOS 17.0

外部サービスは1つも入っていません。 サーバーもデータベースも契約していません。

消えたもの一覧

Flutterで作った既存アプリと並べると、差がはっきりします。

既存アプリで必要だったもの 今回
BaaS(Firebase / Supabase) 不要 — CloudKitプライベートDB
認証(Sign in with Apple・匿名認証) 不要 — iCloudアカウントに自動で乗る
サーバー側の残高台帳・サーバーレス関数 不要 — 端末側で購入を検証
課金基盤(RevenueCat) 不要 — StoreKit 2を直接
Push通知基盤 不要 — ローカル通知で完結
プライバシーポリシーの第三者送信の記述 不要 — 「収集データなし」で申告

順に見ていきます。

同期とバックアップ: SwiftData + CloudKit

SwiftDataのモデルにCloudKit連携を設定すると、保存したデータがユーザー自身のiCloudプライベートDBへ自動で同期されます。コードとしては、ローカルDBに保存しているだけです。

これが効くのは「一生のアルバム」のようなアプリで、データを失わないこと自体が価値だからです。PoCで確認した実測はこうでした。

  • 保存後、数秒〜数十秒でサーバーへの反映が成功
  • アプリを削除して再インストール後、起動から約30秒で全データが復元
  • サーバー側に画像の実データが載っていることも直接確認

同じことをFlutterでやるなら、BaaSを契約し、スキーマを設計し、同期処理を書き、認証を用意することになります。その全部が、モデル定義とオプション設定に置き換わった格好です。

認証: そもそも要らなくなる

これがいちばん体験として大きいところです。アプリ独自のアカウント登録を作りませんでした。

ユーザーから見ると「何もしていないのに、機種変更したらデータが戻ってきた」になります。iCloudにサインインしていない人でも、ローカルで全機能が動きます。

花庭で消費型チケットを実装したときは、「残高が誰のものか」を決めるためだけにSign in with Appleを入れました。今回はその必要すらありません。持ち主はOSがすでに知っているからです。

課金: StoreKit 2を直接使った

前回RevenueCatの記事を書いておいて何ですが、このアプリではRevenueCatを使いませんでした。理由を正直に書きます。

RevenueCatの主な価値はサブスクリプションの複雑さ(更新失敗・猶予期間・トライアル・プラン変更)と複数ストアの同期です。今回は買い切り1点・サブスクなし・iOS専用なので、どちらも当てはまりません

代わりに使ったのがStoreKit 2のTransaction.currentEntitlementsで、これはAppleが署名した購入情報を端末側で検証できます。消費型チケットのように「残り何枚か」を管理する必要がなく、「買ったか買っていないか」だけなので、サーバー台帳も不要になりました。

つまり、課金の形が単純ならサーバーは要らない。前回の記事と矛盾しているように見えて、判断基準は同じです。サブスクを入れる日が来たら、そのときはRevenueCatに移します(既存の購入状態は引き継げます)。

プライバシー: 「収集データなし」で通せる

外部SDKを1つも入れていないので、App Privacyの申告は「データを収集しない」で通ります。写真も記録も、ユーザー自身のiCloudにしか保存されず、開発者は見られません。

これは審査の手間が減るだけでなく、ストアの製品ページに書ける言葉が変わります。「写真は、あなたのものです」と言い切れるのは、実装がそうなっているからです。

実際に効いた4つ

1. 写真ライブラリへのアクセス(PhotoKit)

「年ごとに写真を1枚選ぶ」という体験のために、標準のピッカーを捨てて自前のピッカーを作りました。PhotoKitなら撮影日でのフィルタが素直に書けます。

実機(iPhone SE)で14年分のライブラリを対象に検証して、年別グリッドの表示もスクロールも体感遅延なし。限定アクセス(選択した写真のみ)の状態でも、許可された写真だけで体験が崩れないことまで確認できました。

写真アプリを作るなら、OSのAPIに近い場所にいる強みがそのまま出ます。

2. ウィジェット(ホーム画面・ロック画面)

「一緒に暮らした日数」をホーム画面とロック画面に出しています。WidgetKitはSwiftUIで書けるので、アプリ本体と同じ書き方がそのまま通用します。

Flutterでもウィジェットは実現できますが、iOS側のウィジェット本体は結局SwiftUIで書くことになります。ネイティブなら、その境界が最初から存在しません。

3. 画像圧縮をその場で比較できる

CloudKitに保存する版をどの形式・品質にするか決めるとき、OSのエンコーダが手元にあるので実測が一瞬でした。

形式 サイズ(元459KB) エンコード時間
JPEG q0.8 456 KB 10ms
JPEG q0.5 233 KB 6ms
HEIC q0.8 300 KB 76ms
HEIC q0.5 153 KB 38ms

HEICはJPEGの約65%(同品質)で、エンコード時間の差は実用上無視できる範囲。最終的にHEICの長辺2048pxに決めました。数字を出してから決められると、後で迷いません。

4. 通知もローカルで完結

「一緒に暮らして100日」「N歳の誕生日」といった節目のお知らせは、すべてローカル通知です。サーバーからPushを送る基盤が要りません。年に数回しか鳴らないので、通知疲れも起きません。

差し出したもの

いいことばかり書いたので、代償も正直に。

  • iOS専用になる — Androidは最初から選択肢にありません。これは機能ではなく事業判断です
  • CloudKitへの深い依存 — 将来ほかのバックエンドへ移るのは簡単ではありません。承知の上で選んでいます
  • Productionへのスキーマデプロイを忘れると同期が動かない — 開発ビルドとTestFlight/App Storeでは環境が違い、CloudKit Consoleでのデプロイを忘れると配布版で同期が完全に沈黙します。今回モデルを変えるたびに3回デプロイしました
  • スキーマ変更はフィールド追加のみ — 本番環境では削除や型変更ができません。モデルは「後から足せる形」で設計する必要があります
  • iOS 17以上が必要 — SwiftDataの要件です

3つ目は特に危険で、エラーも出ずに黙って同期しなくなります。ここは手順書に太字で残しました。

Claude CodeでSwiftを書く

AI駆動開発の観点でも書いておきます。Swiftだから不利、ということはありませんでした。 SwiftUIもSwiftDataも情報が十分あり、いつもどおり開発できています。

むしろ効いたのは言語ではなく進め方で、今回は本番実装の前にPoCを2本作りました。「年フィルタ付きの写真ピッカーは実機で実用的か」「削除して再インストールしてもデータは戻るか」— この2つを先に検証し、その結果をレポートにしてから技術構成を確定させています。

未知の技術を採用するとき、AIに本番コードを書かせる前に「判断の根拠」を作らせる。この順番にすると、後戻りが激減します。

どちらに寄せるか

この3本(Flutter2本・ネイティブ1本)を作った現時点での判断基準です。

こういう条件なら 寄せ先
外部APIのキーを守る必要がある どちらでもサーバーが要る → クロスプラットフォームでも損しない
データがユーザー個人のもので完結する ネイティブ(CloudKitでバックエンドが消える)
Androidも出す・出すかもしれない クロスプラットフォーム
ウィジェット・写真・センサーなどOS機能が主役 ネイティブ
課金がサブスクや消費型で複雑 課金基盤を使う(どちらでも)
買い切り1点だけ ネイティブ(StoreKit 2直で完結)

まとめ

  • Swiftネイティブの最大のメリットは、速度でも見た目でもなく「借りるものが減る」こと。同期・認証・課金の受け皿がOSの中にある
  • 今回消えたのは、BaaS・認証・サーバー台帳・課金基盤・Push基盤・第三者送信のプライバシー表記。外部サービスはゼロになった
  • 成立条件は「サーバー側の処理がなく、データがユーザー個人のもので完結する」こと。AIを呼ぶアプリならどちらで作ってもサーバーは要る
  • 代償はiOS専用CloudKit依存。特に本番スキーマのデプロイ忘れは、無言で同期が止まるので要注意
  • 言語の選択よりも、PoCで根拠を作ってから本番に入る進め方の方が結果に効いた

道具は目的で選ぶもので、優劣ではありません。ただ、「このアプリなら外部サービスが1つも要らない」という選択肢があることは、個人開発では知っておく価値があると思います。維持するものが少ないほど、アプリは長く生きられます。