Проверка изменений списков iOS на облачном Mac
Когда из списка одновременно удаляют элементы, добавляют новые и меняют порядок существующих, самые трудновоспроизводимые ошибки обычно связаны не с неверными данными, а с путаницей между старыми и новыми позициями. На облачном Mac от OSXFlow можно сначала запустить проверочный скрипт на чистом Swift: без запуска симулятора проверить прямое и обратное применение изменений, а затем подключить проверенные структурные изменения к слою обновления интерфейса.
Сначала определите, какие два состояния описывает набор изменений
Назовём массив стабильных ID до обновления старым снимком, а массив после обновления — новым. Правильное направление вызова — new.difference(from: old), а не наоборот. Смещения удаляемых элементов относятся к старому снимку, а вставляемых — к новому. Их нельзя рассматривать в одной постоянно меняющейся системе координат.
Например, старый список — [a,b,c], а новый — [c,a,d]. Если сначала вставить элементы по новым позициям, а затем удалить по старым, можно удалить только что вставленный элемент. Для применения изменений средствами стандартной библиотеки используйте old.applying(diff), а не корректируйте каждое смещение вручную.
Сразу зафиксируйте три условия: ID внутри одного снимка уникальны; изменение содержимого не меняет ID; во время фонового расчёта изменений исходный снимок нельзя изменять на месте. Повторяющиеся ID не обязательно вызовут ошибку в стандартной библиотеке, но лишат строки интерфейса однозначной идентичности.
Набор изменений — не универсальная заплатка для любого списка. Он относится к конкретной паре снимков. Даже успешное применение не означает, что версия исходных данных была правильной.
Проверьте прямое и обратное применение исполняемым скриптом
В удалённом терминале убедитесь, что 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"]))
При успешном выполнении скрипт ничего не выводит, а процесс завершается с нулевым кодом. Нарушение любого предусловия прерывает выполнение, и конвейер фиксирует сбой. Не используйте режим компиляции -Ounchecked: он удаляет эти проверки.
Здесь проверяются исходный набор изменений, набор с выявленными перемещениями и обратное преобразование. Эти проверки отвечают на три разных вопроса: «Получается ли целевое состояние?», «Сохраняется ли смысл преобразования после связывания перемещений?», «Можно ли вернуться к исходному состоянию?». Одного сравнения количества элементов недостаточно, чтобы обнаружить ошибки в позициях: нужно сравнивать массивы целиком.
Переберите все небольшие входные данные, а не только несколько удобных примеров
Фиксированные примеры хорошо объясняют сбои, но легко упускают сочетания вроде «удалить первый элемент и одновременно переместить последний в середину». Для четырёх разных 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 к исходным и целевым позициям, а также к моменту обновления источника данных. Успешная проверка набора изменений стандартной библиотеки подтверждает лишь корректность преобразования массива, но не произвольного порядка вызовов интерфейса.
Обновляйте содержимое отдельно
Допустим, у строки с ID a изменился только заголовок. Массив ID остался прежним, поэтому набор изменений будет пустым. При этом всё равно нужно сравнить отображаемые поля старых и новых моделей и собрать ID строк, требующих обновления. Не создавайте временно новый ID ради запуска обновления: так изменение содержимого превратится в удаление и вставку, а состояние выделения или раскрытия будет потеряно.
Отсекайте устаревшие изменения проверкой версии
В момент начала фонового расчёта зафиксируйте и старый снимок, и монотонно возрастающий номер версии. Когда результат вернётся в домен изоляции интерфейса, сначала убедитесь, что текущая версия всё ещё совпадает с зафиксированной, и только затем обновляйте модель и интерфейс. Между проверкой и применением результата не должно быть асинхронного ожидания.
Если версия уже изменилась, отбросьте результат и повторите расчёт на основе текущего снимка. Не считайте ненулевой результат стандартной библиотеки проверкой версии: из-за повторяющихся значений или случайно совместимых позиций даже устаревший набор изменений может успешно породить массив.
Перед выпуском проверьте четыре пункта: повторяющиеся ID явно отклоняются; структурные изменения отделены от обновления содержимого; результаты для устаревших версий не применяются; адаптер интерфейса проверен хотя бы на одном сценарии с одновременным удалением, перемещением и вставкой. Для поиска причины достаточно сохранить входные снимки, номер версии и сработавшее утверждение — записывать конфиденциальное содержимое строк не нужно.
Часто задаваемые вопросы
Можно ли отбросить связанные удаление и вставку после inferringMoves()?
Нет. Перемещение по-прежнему представлено связанными удалением и вставкой. Для applying(_:) нужна полная разница; преобразование в операции интерфейса требует отдельной проверки индексов.
Выявит ли сравнение стабильных ID изменение заголовка?
Нет. Разница идентификаторов описывает только структуру. Содержимое нужно сравнивать отдельно и обновлять по актуальной модели после структурных изменений.
Подключите выделенный Mac к своему рабочему процессу
Собирайте проекты, запускайте тесты и проводите эксперименты в оригинальной macOS. Сравните конфигурации и периоды оплаты, а затем выберите подходящий физический узел.