◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り27日。API 連携を足したアプリは移行先の確認を●SDK 58 — React Native 0.88 に更新されました。Android のリリース版で R8 が既定オンになり、iOS は SceneDelegate を生成します●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り27日。API 連携を足したアプリは移行先の確認を●SDK 58 — React Native 0.88 に更新されました。Android のリリース版で R8 が既定オンになり、iOS は SceneDelegate を生成します●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-06-17上級

Rork Max の SwiftData をオフライン優先で同期する — 衝突を起こさない差分マージの設計

Rork Max が生成する SwiftData アプリを、電波が不安定でも編集を失わないオフライン優先で同期させる設計をまとめます。行ごとの上書きではなく変更を単位にした差分マージ、トゥームストーンによる削除の伝播、再送できる送信キューまで、本番で詰まった判断とともに実装で示します。

Rork Max235SwiftDataオフライン同期衝突解決差分マージアーキテクチャ22

✦ プレミアム記事

iPad で壁紙のコレクションを並べ替えていた最中に地下鉄へ入ってしまい、改札を出てアプリを開き直したら並びが元に戻っていた——自分のアプリで、自分でそれを踏んだときの気まずさは忘れられません。保存は成功していたのに、オンラインに戻った瞬間にサーバー側の古い状態で上書きされていたのです。

Rork Max が生成するのはネイティブの Swift アプリなので、ローカルの保存は SwiftData で素直に書けます。けれど「ローカルに保存できた」と「同期しても消えない」は別物です。ここでは、電波が不安定でも編集を失わないオフライン優先の同期を、行の上書きではなく差分のマージとして組む設計を整理します。

「保存できた」がなぜ信用できないのか

多くの同期実装は、端末の現在状態をまるごとサーバーへ送り、サーバーの状態をまるごと端末へ書き戻します。これは片方向で一台しか触らない間は動きますが、二台目が現れた瞬間に壊れます。iPhone で付けたお気に入りと、iPad で変えた並び順が、後から同期したほうの「全体」で互いを潰し合うからです。

問題の本質は、同期の単位を「行(レコードの最終状態)」に置いていることにあります。最終状態だけを見ると、どちらが新しいかしか判定できません。実際に必要なのは「誰が・いつ・どのフィールドを変えたか」という変更の履歴です。単位を行から変更へ移すと、別々のフィールドへの編集は衝突せずに共存できます。

SwiftData モデルに同期メタデータを持たせる

まず、各モデルへ同期判断に使うメタデータを足します。更新時刻、論理削除フラグ(トゥームストーン)、そして同期済みかどうかを表すローカル専用の状態です。

import SwiftData
import Foundation
 
@Model
final class WallpaperItem {
    @Attribute(.unique) var id: UUID
    var title: String
    var sortIndex: Int
    var isFavorite: Bool
 
    // --- 同期メタデータ ---
    var updatedAt: Date          // 最後に「中身」を変えた時刻
    var isDeleted: Bool          // トゥームストーン(物理削除しない)
    var dirty: Bool              // ローカルの未送信変更があるか
    var revision: Int            // サーバーが採番する版番号
 
    init(id: UUID = UUID(), title: String, sortIndex: Int) {
        self.id = id
        self.title = title
        self.sortIndex = sortIndex
        self.isFavorite = false
        self.updatedAt = .now
        self.isDeleted = false
        self.dirty = true
        self.revision = 0
    }
}

ポイントは三つあります。id をサーバーと共有する UUID にして端末をまたいで同一性を保つこと。削除を isDeleted の論理削除にして、消えた事実そのものを同期できるようにすること。そして dirty で未送信の編集を覚えておき、電波が戻った時に何を送ればよいかを端末側が把握できるようにすることです。

✦

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

この記事の続きを読む

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

この記事で得られること
✦SwiftData モデルに同期メタデータを持たせ、最後の書き込みだけで上書きしない差分マージの実装が手に入る
✦削除を「行を消す」ではなくトゥームストーンで伝播させ、片方の端末で復活してしまう事故を防ぐ設計が分かる
✦電波が切れても編集を捨てない送信キュー(アウトボックス)の作り方と、二重送信を避ける冪等キーの置き方が分かる
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

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

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

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

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

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

関連記事

⬡ 開発ツール2026-08-01
実験の割り当てが同じ端末に偏る — 100万件のIDで確かめたハッシュ選定と salt の位置
端末側で実験の変種を決める割り当てが、実験を2本並べた途端に同じ端末へ集中しました。100万件のIDで4つのハッシュを測り、原因がハッシュの一様性ではなく salt の連結位置にあったこと、そして採用した実装までを実測値とともに残します。
⬡ 開発ツール2026-07-24
同じ通信が同時に何本も飛ぶのを1本に束ねる — 参照カウント付きシングルフライト層の設計
起動直後に同じ API へ何本ものリクエストが同時に飛ぶ多重フェッチを、進行中の Promise を共有して1本に束ねる設計です。束ねる範囲を決めるキーの組み立て方、失敗した Promise を配り続けてしまう罠、呼び出し側の中断が全体を巻き込む罠、そして束ね率を数えて回帰を止めるテストまで、参照カウント付きシングルフライト層の実装を実測の通信ログとともに残します。
⬡ 開発ツール2026-07-24
アプリの設定値を三層で解決する — 既定・ユーザー・リモートを境界付きにマージするレイヤー設計
既定値・ユーザー設定・リモート設定をひとつの型安全なレイヤーでマージする設計です。壊れたリモート値でもアプリが崩れないよう、キーごとに上書きの強さを変え、境界クランプと検証を通す実装を、実際の事故から起こしたコードで解説します。