Проверка изменений списков iOS на облачном Mac

Проверка изменений списков iOS на облачном Mac

Проверка изменений списков 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. Сравните конфигурации и периоды оплаты, а затем выберите подходящий физический узел.

Арендовать облачный Mac Сравнить конфигурации

Передайте следующую сборку облачному Mac

Выделенный физический компьютер, а не виртуальная машина. Выберите конфигурацию и срок аренды под свой проект.