旅行計画アプリ「たびくる」の共同編集を2台の端末で確かめると、ビルドや単一端末での操作だけでは見えなかった問題が見つかった。未同期のメモが競合時に消え、削除した予定が別端末から復活し、所有者が消した旅行が参加者には残る。今回は、それぞれの原因を追って修正した過程を記録する。
競合時に未同期メモを残す
端末Aで旅行名、端末Bでメモを編集し、Aを先に同期してからBをオンラインへ戻したところ、Bで競合が表示された一方、入力したメモが消えた。競合を検出する前に非同期の取得結果が画面の旅行データを置き換える経路があった。
そこで、端末で編集した旅行全体を旅行ごとのlocalDraftとして保持し、競合を検出したときに復元してから確認画面を出すようにした。共有側の候補も別に保持する。
所有者Aが旅程を追加し、編集者Bがメモを追記する2端末の再確認では、Bで端末側を優先した後、メモがAへ反映された。続いてAで共有側を採用すると、Aが追加した旅程は旅行全体の採用に伴って失われた。これは当時の明示的な旅行全体採用の結果であり、項目ごとの自動マージが完成したことを示すものではない。
保存と取得の重なりを整理する
自動マージの実装後には「共有に失敗しました」と表示され、手動更新や再起動でも相手端末へ変更が届かないケースが出た。診断ログではCKError.operationCancelledに加え、通信不能を示すNSURLError -1009、接続断のNSURLError -1005を確認した。
調査すると、前面復帰やバックグラウンド保存、同期予約が重なったとき、保存中の処理が取り消され得た。また、保存中に同じ旅行の取得が始まる経路もあった。保存中の処理を新しい予約で取り消さず、取得要求は保存完了後にまとめて実行するよう修正した。一時的な通信エラーには、CloudKitが示す待機時間を起点とした段階的な再試行を追加した。
修正後の実機確認では、Aの保存完了に続き、起動中のBが再起動せずに変更を取得できた。双方向・同時編集を含む最終受入は引き続き必要である。
削除した予定が復活する経路をふさぐ
端末Bで予定を削除してもAに反映されず、後にAが保存するとBにも予定が復活した。原因は一つではなかった。自動マージが「片方で削除、もう片方は未変更」を編集との競合として扱い、項目を残していた。画面の一部では、配列内の予定を直接変更しても旅行全体の編集通知と同期予約を通っていなかった。起動時には、古い端末内データをCloudKitの取得より先に再送する順序も残っていた。
マージでは相手が未変更なら削除を反映し、相手が編集済みのときだけ項目を削除候補として残すようにした。予定などの追加・削除は旅行全体の変更としてStoreへ渡し、起動時はCloudKitを先に取得して端末側との差分をマージしてから保存する。
2端末では、予定の削除と追加をAからB、BからAの計4方向で確認し、いずれも反映された。同じスポットを片方が削除し、もう片方が時刻を変更した競合では、編集後のスポットが残ることも確認した。削除候補の画面表示を含む最終受入は残っている。
所有者が削除した旅行を参加者側でも整理する
所有者が共有旅行を削除した後、参加者側ではCloudKitのnotFoundを一般的な共有エラーとして扱い、旅行を端末に残していた。共有用Zoneが消えた場合にはCKError.zoneNotFound(code 26)も発生した。実機では独自のnotFoundがNSErrorとして渡され、当初の削除判定から漏れることも分かった。
削除済みの共有旅行を取得した場合は、その旅行と関連する同期予約・保留データを端末側から整理するようにした。通信障害は削除と区別し、再試行の対象にする。実機で所有者の削除後に参加者側から旅行が消え、参加者のアプリを再起動しても復活しないことを確認した。別の新規共有旅行でも、作成後の削除を確認している。
確認できた範囲と残る作業
上記の局所修正後、XcodeのDebugビルドは成功した。2端末で確認できたのは、競合時の明示的な採用操作、起動中の片方向同期、予定の追加・削除の両方向反映、所有者による旅行削除の反映である。
共同編集全体の完了判定はまだしていない。保存中の追加編集や同時編集の最終受入、削除候補の表示確認などが残る。共有同期のシェル回帰テストも、この作業環境ではSwiftのswift-plugin-serverがサンドボックスで拒否され、修正後には実行できていない。今回の結果を、すべての競合条件や配布環境での動作確認としては扱わない。