クラウドMacでiOSリストの差分更新を検証する

クラウドMacでiOSリストの差分更新を検証する

クラウドMacでiOSリストの差分更新を検証する

リストで削除・挿入・並べ替えが同時に起きるとき、再現が難しい不具合の原因は、データの計算ミスよりも、更新前の位置を更新後の位置として扱ってしまうことにあります。OSXFlowのクラウドMacでは、まずSwiftだけで動く検証スクリプトを実行し、シミュレータを起動せずに差分の適用と逆変換を確認できます。検証を通過した構造変更を、その後で画面の更新処理につなぎましょう。

差分がどの2つの状態を表すのかを明確にする

更新前の安定したIDの配列を旧スナップショット、更新後の配列を新スナップショットとします。呼び出しの向きは new.difference(from: old) であり、その逆ではありません。削除のオフセットは旧スナップショット、挿入のオフセットは新スナップショットを基準にしています。両者を、逐次変化する1つの座標系の位置として扱ってはいけません。

たとえば、旧リストが [a,b,c]、新リストが [c,a,d] だとします。先に新しい位置へ挿入し、その後で古い位置を基準に削除すると、挿入したばかりの要素を消してしまう可能性があります。標準ライブラリに変換を任せる場合は old.applying(diff) を使い、個々のオフセットを自分で調整しないでください。

ここで、3つの前提を決めておきます。同一スナップショット内のIDは一意であること、内容を変更してもIDは変わらないこと、バックグラウンドで差分を計算している間は元のスナップショットをその場で書き換えないことです。IDが重複していても標準ライブラリが必ずエラーを出すとは限りませんが、画面上の行を一意に識別できなくなります。

差分は、どのリストにでも適用できるパッチではありません。特定の2つのスナップショットに対応するものです。適用に成功しても、元データのバージョンが正しいとは限りません。

実行可能なスクリプトで適用と逆変換を検証する

リモートのターミナルで、xcrun --find swift がコンパイラを見つけられることを確認します。次のコードを /tmp/list-diff-check.swift に保存し、xcrun swift /tmp/list-diff-check.swift を実行してください。この例で使うのは標準ライブラリだけです。プロジェクトの作成、依存ライブラリのダウンロード、署名設定は必要ありません。

func hasUniqueIDs(_ values: [String]) -> Bool {
    Set(values).count == values.count
}

func verify(_ old: [String], _ new: [String]) {
    precondition(hasUniqueIDs(old))
    precondition(hasUniqueIDs(new))

    let raw = new.difference(from: old)
    let moved = raw.inferringMoves()

    precondition(old.applying(raw) == new)
    precondition(old.applying(moved) == new)
    precondition(new.applying(moved.inverse()) == old)
}

verify([], [])
verify([], ["a"])
verify(["a"], [])
verify(["a", "b", "c"], ["c", "a", "d"])
verify(["a", "b", "c"], ["c", "b", "a"])
precondition(!hasUniqueIDs(["a", "a"]))

正常に実行されると、スクリプトは何も出力せず、プロセスは終了コード0で終了します。いずれかの事前条件を満たさなければ実行が中断され、パイプラインで失敗として扱えます。これらのチェックが取り除かれるコンパイルモード -Ounchecked は使わないでください。

ここでは、元の差分、移動を推論した後の差分、そして逆変換を検証しています。それぞれ「目的の状態に到達できるか」「移動の関連付けによって意味が変わらないか」「元の状態に戻せるか」を確認するものです。要素数だけを比べても位置のずれは見つかりません。配列全体を比較する必要があります。

手近な数例だけでなく、小さな集合を全探索する

固定のテストケースは不具合の説明には向いていますが、「先頭の要素を削除しながら、末尾の要素を途中へ移動する」といった組み合わせを見落としがちです。異なる4つのIDから作れる、重複のない順序付き部分集合は全部で65個あります。そのすべての組み合わせで変換すると4225組です。この規模なら乱数シードを使わず、そのまま全探索できます。

同じファイルに次のコードを追記し、もう一度実行してください。

func snapshots(
    prefix: [String],
    remaining: [String]
) -> [[String]] {
    var result = [prefix]
    for index in remaining.indices {
        var rest = remaining
        let next = rest.remove(at: index)
        result += snapshots(
            prefix: prefix + [next],
            remaining: rest
        )
    }
    return result
}

let cases = snapshots(
    prefix: [],
    remaining: ["a", "b", "c", "d"]
)
precondition(cases.count == 65)

for old in cases {
    for new in cases {
        verify(old, new)
    }
}

これで、空のリスト、挿入のみ、削除のみ、順序の完全な反転、複数種類の編集が混在するケースを網羅できます。ただし、任意の長さで正しく動作することの証明にはなりません。実際に組み込む際は、本番で発生した不具合を最小の入力ペアに絞り込み、固定のテストケースに追加してください。大きなリストの処理時間とメモリ使用量は別途測定し、このスクリプトの実行結果を性能の保証として扱わないようにします。

移動の関連付けは画面への移動指示ではない

inferringMoves() は、移動として認識できる削除と挿入に関連付けを追加しますが、それらを新しい列挙ケースに変えるわけではありません。applying(_:) に渡すときは差分全体を保持し、関連付けがあるからといって片方を省略しないでください。

画面側で独自の一括更新APIを使う場合は、専用のアダプタを実装し、移動元の位置、移動先の位置、データソースを更新するタイミングについて、そのAPIが求める条件を確認します。標準ライブラリの差分が検証を通過しても、確認できたのは配列の変換が正しいことだけです。画面更新APIをどの順序で呼び出しても正しく動くと保証されたわけではありません。

内容の再表示は別の経路で処理する

IDが a の行でタイトルだけを変更した場合、IDの配列は変わらないため、差分は空になります。それでも、新旧モデルの表示対象フィールドを比較し、再表示が必要なIDを集める必要があります。再表示を起こすために一時的な新しいIDを生成してはいけません。単なる内容変更が削除と挿入に変わり、選択状態や展開状態が失われてしまいます。

バージョンチェックで古い差分の適用を防ぐ

バックグラウンドで計算を始めるときに、旧スナップショットと単調増加するバージョン番号を一緒に取得します。結果がUIの分離領域に戻ったら、まず現在のバージョンが取得時のバージョンと一致することを確認し、その後でモデルと画面を更新します。この確認から更新の確定までの間に、非同期の待機を挟んではいけません。

すでにバージョンが変わっていれば結果を破棄し、現在のスナップショットを基に計算し直します。「標準ライブラリがnilではない結果を返した」ことをバージョン確認の代わりにしてはいけません。値の重複や、位置が偶然適合することによって、古い差分からでも配列が生成される可能性があります。

本番投入前に、最後に4点を確認してください。重複IDを明示的に拒否していること、構造変更と内容の再表示を分離していること、古いバージョンの結果を反映しないこと、そしてUIアダプタで削除・移動・挿入が同時に起きるケースを少なくとも1つ検証していることです。入力スナップショット、バージョン番号、失敗したアサーションを残しておけば、問題の切り分けができます。行の中にある機密情報まで記録する必要はありません。

よくある質問

inferringMoves()の後で関連する削除と挿入を省略できますか?

できません。移動は関連付けられた削除と挿入として表現されます。applying(_:)には差分全体を渡してください。画面の移動操作に変換する場合は、別途インデックス規則の検証が必要です。

安定したIDの比較だけでタイトル変更も検出できますか?

できません。IDの差分が表すのはリスト構造です。内容は別に比較して再表示を要求し、構造更新後に最新のモデルを参照します。

ワークフローに専用Macを追加

macOSをそのまま使って、ビルドやテスト、検証を実行できます。構成と利用期間を比較して、用途に合った物理マシンをお選びください。

クラウドMacをレンタル プランを見る

次のビルドはクラウドMacで

仮想マシンではなく、専用の物理マシンをご利用いただけます。プロジェクトに合わせて構成と利用期間をお選びください。