iOS-Listenänderungen auf dem Cloud-Mac prüfen

iOS-Listenänderungen auf dem Cloud-Mac prüfen

iOS-Listenänderungen auf dem Cloud-Mac prüfen

Wenn in einer Liste gleichzeitig Elemente gelöscht, eingefügt und umgeordnet werden, entstehen die am schwersten reproduzierbaren Fehler oft nicht durch falsch berechnete Daten, sondern dadurch, dass alte Positionen als neue interpretiert werden. Auf einem Cloud-Mac von OSXFlow kannst du zunächst mit einem reinen Swift-Prüfskript testen, ob sich eine Differenz vorwärts und rückwärts anwenden lässt – ohne einen Simulator zu starten. Erst danach bindest du die geprüften Strukturänderungen an die UI-Aktualisierung an.

Zuerst klären, welche beiden Zustände die Differenz beschreibt

Das Array stabiler IDs vor der Aktualisierung bezeichnen wir als alten Snapshot, das Array danach als neuen Snapshot. Die Aufrufrichtung lautet new.difference(from: old), nicht umgekehrt. Die Offsets für Löschungen beziehen sich auf den alten Snapshot, die Offsets für Einfügungen auf den neuen. Sie dürfen nicht als Positionen in einem einzigen, laufend veränderten Koordinatensystem verstanden werden.

Angenommen, die alte Liste lautet [a,b,c] und die neue [c,a,d]. Wenn du zuerst an den neuen Positionen einfügst und anschließend an den alten Positionen löschst, entfernst du möglicherweise ein gerade eingefügtes Element. Wenn die Standardbibliothek die Transformation übernehmen soll, verwende old.applying(diff), statt die einzelnen Offsets selbst anzupassen.

Dafür gelten zunächst drei Bedingungen: IDs sind innerhalb eines Snapshots eindeutig; Inhaltsänderungen verändern die ID nicht; während die Differenz im Hintergrund berechnet wird, darf der Ausgangs-Snapshot nicht direkt verändert werden. Doppelte IDs führen zwar nicht zwingend zu einem Fehler in der Standardbibliothek, machen die Identität der UI-Zeilen aber uneindeutig.

Eine Differenz ist kein Patch, der auf jede beliebige Liste passt. Sie gehört zu einem bestimmten Paar von Snapshots. Dass sie erfolgreich angewendet werden kann, bedeutet noch nicht, dass die Version der Ausgangsdaten stimmt.

Hin- und Rücktransformation mit einem ausführbaren Skript prüfen

Prüfe im Remote-Terminal, ob xcrun --find swift den Compiler findet. Speichere den folgenden Code unter /tmp/list-diff-check.swift und führe anschließend xcrun swift /tmp/list-diff-check.swift aus. Das Beispiel verwendet ausschließlich die Standardbibliothek. Du brauchst weder ein Projekt noch Downloads von Abhängigkeiten oder eine Signierungskonfiguration.

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"]))

Bei erfolgreicher Ausführung gibt das Skript nichts aus und der Prozess endet mit Exitcode null. Schlägt eine der Vorbedingungen fehl, bricht die Ausführung ab, sodass die Pipeline den Lauf als fehlgeschlagen wertet. Verwende nicht den Compilermodus -Ounchecked, da er diese Prüfungen entfernt.

Geprüft werden sowohl die ursprüngliche Differenz als auch die Differenz nach dem Ermitteln von Verschiebungen und deren inverse Transformation. Damit werden drei Fragen beantwortet: „Wird der Zielzustand erreicht?“, „Bleibt die Semantik durch die Zuordnung von Verschiebungen erhalten?“ und „Lässt sich der Ausgangszustand wiederherstellen?“. Ein Vergleich der Elementanzahl allein erkennt keine falsch platzierten Elemente. Dafür müssen die vollständigen Arrays verglichen werden.

Kleine Eingabemengen vollständig durchlaufen statt nur naheliegende Fälle testen

Feste Testfälle eignen sich gut, um Fehler zu erklären. Kombinationen wie „erstes Element löschen und gleichzeitig das letzte in die Mitte verschieben“ werden dabei jedoch leicht übersehen. Aus vier unterschiedlichen IDs lassen sich insgesamt 65 geordnete Teilmengen ohne Wiederholungen bilden. Alle paarweisen Übergänge ergeben 4225 Kombinationen. Diese Größenordnung lässt sich vollständig durchlaufen; ein Zufalls-Seed ist nicht nötig.

Hänge den folgenden Code an dieselbe Datei an und führe sie erneut aus:

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)
    }
}

Damit sind leere Listen, reine Einfügungen, reine Löschungen, eine vollständig umgekehrte Reihenfolge und gemischte Änderungen abgedeckt. Es ist jedoch kein Beweis für die Korrektheit bei beliebiger Listenlänge. Reduziere Fehler aus dem Produktivbetrieb bei der Integration auf ein minimales Eingabepaar und ergänze damit die festen Testfälle. Miss Laufzeit und Speicherverbrauch großer Listen separat; die Ergebnisse dieses Skripts sind keine Leistungszusage.

Eine Zuordnung als Verschiebung ist noch keine UI-Verschiebeanweisung

inferringMoves() verknüpft erkennbare Löschungen und Einfügungen miteinander, erzeugt daraus aber keinen neuen Enum-Fall. Übergib an applying(_:) weiterhin die vollständige Differenz. Überspringe nicht eine der beiden Änderungen, nur weil sie miteinander verknüpft sind.

Wenn die UI eine eigene Schnittstelle für Batch-Updates verwendet, implementiere dafür einen separaten Adapter. Prüfe, welche Anforderungen diese Schnittstelle an Ausgangspositionen, Zielpositionen und den Zeitpunkt der Datenquellenaktualisierung stellt. Dass eine Differenz der Standardbibliothek die Prüfungen besteht, belegt nur die korrekte Array-Transformation – nicht die Korrektheit einer beliebigen Reihenfolge von UI-Aufrufen.

Inhaltsaktualisierungen separat behandeln

Angenommen, bei der Zeile mit der ID a ändert sich nur der Titel. Das ID-Array bleibt dann unverändert und die Differenz ist leer. Trotzdem musst du die sichtbaren Felder des alten und neuen Modells vergleichen und die IDs der zu aktualisierenden Zeilen sammeln. Erzeuge nicht vorübergehend eine neue ID, nur um eine Aktualisierung auszulösen. Das würde aus einer Inhaltsänderung eine Löschung mit anschließender Einfügung machen und den Auswahl- oder Aufklappzustand verlieren lassen.

Veraltete Differenzen durch Versionsprüfungen abweisen

Erfasse beim Start der Hintergrundberechnung sowohl den alten Snapshot als auch eine monoton steigende Versionsnummer. Sobald das Ergebnis im Isolationsbereich der UI ankommt, prüfst du zunächst, ob die aktuelle Version noch der erfassten Version entspricht. Erst dann aktualisierst du Modell und UI. Zwischen Prüfung und Übernahme darf kein asynchrones Warten liegen.

Hat sich die Version inzwischen geändert, verwirf das Ergebnis und berechne die Differenz ausgehend vom aktuellen Snapshot neu. Verwende „die Standardbibliothek liefert ein Ergebnis ungleich nil“ nicht als Versionsprüfung: Doppelte Werte oder zufällig passende Positionen können dazu führen, dass selbst eine veraltete Differenz noch ein Array erzeugt.

Prüfe vor dem Produktiveinsatz abschließend vier Punkte: Doppelte IDs werden ausdrücklich abgewiesen; Strukturänderungen und Inhaltsaktualisierungen sind getrennt; Ergebnisse für veraltete Versionen werden nicht übernommen; und der UI-Adapter ist mindestens mit einem Fall getestet, bei dem Löschungen, Verschiebungen und Einfügungen gleichzeitig auftreten. Um Fehler einzugrenzen, genügen die Eingabe-Snapshots, die Versionsnummer und die fehlgeschlagene Assertion. Sensible Inhalte innerhalb der Zeilen musst du dafür nicht protokollieren.

Häufig gestellte Fragen

Darf man nach inferringMoves() verknüpfte Löschungen und Einfügungen weglassen?

Nein. Eine Verschiebung bleibt als verknüpfte Löschung und Einfügung dargestellt. applying(_:) benötigt die vollständige Differenz. UI-Verschiebungen brauchen einen separat geprüften Adapter.

Erkennt ein Vergleich stabiler IDs einen geänderten Titel?

Nein. ID-Differenzen beschreiben nur die Listenstruktur. Inhaltsänderungen müssen separat verglichen und nach dem Strukturupdate aus dem aktuellen Modell angezeigt werden.

Ein eigener Mac für deinen Workflow

Builds, Tests und Experimente mit nativem macOS durchführen. Vergleiche zuerst Ausstattung und Abrechnungszeiträume und wähle dann den passenden physischen Mac.

Cloud-Mac jetzt mieten Mac-Modelle ansehen

Den nächsten Build auf einem Cloud-Mac ausführen

Ein exklusiver physischer Mac, keine virtuelle Maschine. Wähle Ausstattung und Mietdauer passend zu deinem Projekt.