RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-05-28中級

Rork で作った壁紙アプリの「お気に入り」を NSUbiquitousKeyValueStore に寄せた6本並行運用の実装ノート

Rork が出した iOS アプリで「お気に入り壁紙」をデバイス間同期する場合、CloudKit より NSUbiquitousKeyValueStore が個人開発の実情に合います。壁紙アプリ6本を並行運用する立場から、しきい値設計・競合解消・初回起動の戻し順序まで実装ノートとしてまとめます。

iCloud3NSUbiquitousKeyValueStoreStoreKit8Rork515個人開発186壁紙アプリ28

プレミアム記事

機種変更直後に「お気に入りした壁紙が全部消えていた」というレビューを 2 件続けて受けた週があって、それまで AsyncStorage に保存していたお気に入りリストを 6 本ある壁紙アプリ全部で iCloud に寄せ直すことに決めました。アーティスト・クリエイターの廣川政樹です。2014 年から個人で iOS/Android アプリを作っており、累計 5,000 万 DL を超えた壁紙系アプリの保守を一人で続けています。

「iCloud で同期」と聞くと反射的に CloudKit を選びがちですが、お気に入り壁紙のような小さな配列データに対しては、CloudKit はオーバースペックでした。実際に選んだのは NSUbiquitousKeyValueStore(以下 KV ストア)です。本稿は、Rork が出した Expo / React Native プロジェクトに KV ストアを後付けし、6 本のアプリで 3 週間運用した実装ノートです。よくある「サンプルコードはあるが本番で破綻する」パターンを避けるための判断軸を、実数値と一緒に残しておきます。

なぜ CloudKit ではなく NSUbiquitousKeyValueStore か

お気に入り壁紙のデータ構造は、突き詰めれば「壁紙 ID の配列」と「カテゴリ別の数件のメモ」だけです。これを CloudKit に乗せるとスキーマ管理・コンテナ初期化・サブスクリプション設定が必要になり、6 本のアプリで重複作業が走ります。私の場合、もう一つ大きな理由がありました。CloudKit はコンテナ単位での課金や運用上のクォータ管理が将来発生する可能性があり、月収が AdMob 中心の個人開発では、メンテナンスコストを上げてまで採用する根拠が弱かったのです。

NSUbiquitousKeyValueStore は Apple ID に紐づく単純な KV ストレージで、容量は 1 MB、キー数は 1024 個、値あたり 1 MB という制約があります。お気に入り壁紙の ID を文字列で持つなら、平均 180 件のリストでも 5 KB 前後に収まります。6 本のアプリで個別の Apple ID 配下に独立した KV ストアを持つ設計で、容量は十分でした。

判断軸を整理すると、以下のようになります。

  • ユーザーごとの構造化データ(コメント・編集履歴・大容量メディア)を扱う → CloudKit
  • 端末間で「最新の選好を一致させたい小さな状態」を扱う → KV ストア
  • ユーザー間で共有が必要な状態を扱う → CloudKit のパブリックデータベース or 自前サーバー

お気に入り壁紙は 2 番目に該当します。ここを CloudKit にすると、課金・運用・更新通知の三方向で考えることが増えて、6 アプリ並行運用が破綻します。私が壁紙アプリで KV ストアに寄せた理由は「小さく寄せきれる確信」ともう一つ、「Apple 側にバックエンドを完全に預けて自分の運用責任を縮める」という指針です。

Expo / React Native プロジェクトに KV ストアを生やす最短ルート

Rork が出力する Expo プロジェクトでは、KV ストアを扱うネイティブモジュールが既定で入っていません。expo-modules-core を使って Swift 側の薄いブリッジを自作するのが、保守しやすくて 6 アプリで使い回せる形でした。

modules/iCloudKVSync/ios/ICloudKVSync.swift に以下のような実装を置きます。

import ExpoModulesCore
import Foundation
 
public class ICloudKVSyncModule: Module {
    private var observer: NSObjectProtocol?
 
    public func definition() -> ModuleDefinition {
        Name("ICloudKVSync")
 
        Events("onRemoteChange")
 
        OnCreate {
            let store = NSUbiquitousKeyValueStore.default
            // 最初の synchronize は失敗してもログだけ残す
            _ = store.synchronize()
            self.observer = NotificationCenter.default.addObserver(
                forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
                object: store,
                queue: .main
            ) { [weak self] notification in
                guard let self = self else { return }
                let reason = notification.userInfo?[NSUbiquitousKeyValueStoreChangeReasonKey] as? Int ?? -1
                let keys = notification.userInfo?[NSUbiquitousKeyValueStoreChangedKeysKey] as? [String] ?? []
                self.sendEvent("onRemoteChange", [
                    "reason": reason,
                    "keys": keys
                ])
            }
        }
 
        OnDestroy {
            if let observer = self.observer {
                NotificationCenter.default.removeObserver(observer)
            }
        }
 
        AsyncFunction("setString") { (key: String, value: String) -> Bool in
            let store = NSUbiquitousKeyValueStore.default
            store.set(value, forKey: key)
            return store.synchronize()
        }
 
        AsyncFunction("getString") { (key: String) -> String? in
            return NSUbiquitousKeyValueStore.default.string(forKey: key)
        }
 
        AsyncFunction("removeKey") { (key: String) -> Bool in
            let store = NSUbiquitousKeyValueStore.default
            store.removeObject(forKey: key)
            return store.synchronize()
        }
 
        AsyncFunction("synchronize") { () -> Bool in
            return NSUbiquitousKeyValueStore.default.synchronize()
        }
    }
}

ポイントは 3 つあります。1 つ目は、synchronize() の戻り値はあくまで「ローカルキャッシュへの書き込みが成功したか」であって、クラウドへの伝搬完了ではないという点です。2 つ目は、didChangeExternallyNotification を購読してリモート更新の reason / changed keys を JS 側へイベントとして流す設計にしておくと、後から競合解消ロジックを足しやすくなる点です。3 つ目は、OnDestroy で observer を解除しないと、Expo の開発ビルドでホットリロード時に多重発火するので忘れずに書きます。

JS 側は型を最小に保ち、ストアを直接いじる箇所を 1 ファイルに閉じ込めます。

import { NativeModule, requireNativeModule } from 'expo-modules-core';
 
type ChangeEvent = { reason: number; keys: string[] };
 
declare class ICloudKVSyncModule extends NativeModule<{
  onRemoteChange: (event: ChangeEvent) => void;
}> {
  setString(key: string, value: string): Promise<boolean>;
  getString(key: string): Promise<string | null>;
  removeKey(key: string): Promise<boolean>;
  synchronize(): Promise<boolean>;
}
 
export default requireNativeModule<ICloudKVSyncModule>('ICloudKVSync');

Rork が初期生成するプロジェクトに app.jsonios.entitlementscom.apple.developer.ubiquity-kvstore-identifier を追加するのも忘れずに行います。これを忘れると Xcode のビルドは通るのに、本番の TestFlight ビルドで読み書きが完全に no-op になるという、地味で見つけづらいバグになります。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
1MB / 1024キーの制約下で6アプリ分のお気に入り(平均180件)を破綻なく同期する設計
CloudKit を見送って NSUbiquitousKeyValueStore に寄せた根拠と運用コスト比較
デバイス時計のズレを前提にした last-writer-wins の競合解消とロールバック手順
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

開発ツール2026-05-28
Rork で書いた 6 つの壁紙アプリで MMKV に移し替えた 3 週間の所感
壁紙アプリ 6 本で AsyncStorage から react-native-mmkv へ少しずつ移し替えてみた 3 週間分の手順と所感を、つまずいた点も含めて書き残します。2014年からの個人開発の現場感を踏まえて、判断のラインを共有します。
開発ツール2026-05-26
Rork で生成した壁紙アプリを iPad へ対応させた2週間の所感
2014年から壁紙アプリを iPhone 中心で運用してきた個人開発者として、Rork で立ち上げた新規壁紙アプリを iPad に対応させる作業を2週間続けた記録です。AdMob アダプティブバナーの暴れ方、Safe Area の盲点、グリッド表示の解像感まで、運用の温度のまま書き残しました。
開発ツール2026-05-25
Rork iOSアプリのメモリプレッシャー対策 — 壁紙アプリ運用で磨いた5段階解放アーキテクチャ
Rork経由でビルドしたiOSアプリのメモリプレッシャー対策を、5段階解放という考え方で整理しました。壁紙アプリ群を12年運用してきた中で見えた優先順位と、Instrumentsでの計測手順を実装込みで紹介します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →