SwiftUI теперь позволяет использовать привязки элементов в списках. Посмотрите, как это реализовано за кулисами

Списки, вероятно, являются одним из самых популярных элементов пользовательского интерфейса в приложениях для iOS, и мы прошли долгий путь с момента появления UITableViewController. Создание списков в UIKit было не совсем ракетной наукой, но для этого требовалась некоторая церемония.

SwiftUI упростил создание списков. Посмотрите этот фрагмент, который отображает список дел:

С помощью всего трех строк кода мы можем создать простой список.

Все становится немного сложнее, когда мы пытаемся сделать элементы в строках списка доступными для редактирования. Например, TextField требует привязки к элементу, который мы хотим редактировать:

Однако до сих пор SwiftUI не предоставлял простого способа доступа к изменяемым привязкам к элементам коллекции. Вот почему мы создали такой код:

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

Начиная с WWDC 2021, SwiftUI поддерживает привязки для элементов списка. Чтобы использовать эту функцию, все, что нам нужно сделать, это передать привязку к коллекции в список, и SwiftUI передаст нам привязку к текущему элементу в замыкании:

Этот код легче читать, и он похож на исходный код, с которого мы начали. И самое приятное: вы можете развернуть этот код обратно в любой выпуск iOS, поддерживающий SwiftUI.

За кулисами

Я уверен, что вы оцените повышенную простоту вашего кода. Более простой код означает меньше ошибок, а также упрощает чтение и запись.

Но как это работает?

Что ж, есть несколько вещей, которые объединяются, чтобы сделать это возможным. Давайте сначала посмотрим, что позволяет использовать привязки в ListForEach, кстати).

Давайте перейдем к определению инициализатора List (используя ⌃ + ⌘ + J или ⌃ + ⌘ + Click):

Беглый взгляд на документацию (с включенными API-различиями) показывает, что это дополнение с 12.5 до 13.0b1.

Давайте развернем то, что говорит нам эта новая сигнатура инициализатора:

  • Первый параметр (data: Binding<Data>) определяет, что инициализатор ожидает привязку, которая является общей для Data (например, массив, как в нашем случае).
  • Второй параметр (@ViewBuilder rowContent: @escaping (Binding<Data.Element>) -> RowContent) определяет завершающее замыкание, в котором мы определяем тело нашего списка. Эта сигнатура показывает нам, что закрытие получит привязку, которая является универсальной по типу элементов коллекции данных.

Это то, что позволяет нам передавать коллекцию привязок и получать их одну за другой в теле List.

Но это только часть уравнения! Почему мы можем использовать синтаксис знака $?

Чтобы понять это, нам нужно провести дополнительную археологию API - или получить подсказку от того, кто работал над этим. Спасибо, Холли.

SE-0293 (Расширение оболочек свойств до параметров функций и замыканий) обсуждает расширение оболочек свойств до параметров функций и замыканий. Вот почему мы в первую очередь можем использовать Binding для трейлинг-закрытия. Что делает знак $ возможным, обсуждается в разделе Закрытие предложения:

«Для замыканий, которые принимают прогнозируемое значение, атрибут property-wrapper не нужен, если оболочка поддерживающего свойства и проецируемое значение имеют один и тот же тип, например оболочку свойства из SwiftUI. Если Binding реализовано init(projectedValue:), его можно было бы использовать как атрибут-оболочку свойств для параметров закрытия без явной записи атрибута:

let useBinding: (Binding ‹Int›) - ›Void = {$ value in

...

}”

Конечно, если мы посмотрим на API-интерфейс для Binding, то вот оно: init(projectedValue: Binding<Value>) - дополнение от Xcode 12.5 к Xcode 13.0b1.

Вот и все: мы можем использовать привязки внутри List и ForEach благодаря работе, проделанной в SE-0293, и добавлению соответствующих инициализаторов в List (см. Документацию), ForEach (см. Документацию) и Binding (см. документацию).

Как упомянула Холли, новый синтаксис $ для параметров закрытия является аспектом языка во время компиляции и работает, пока вы компилируете с Swift 5.5.

Back-Deploying

Это подводит нас к следующему вопросу: почему это можно развернуть в более ранних версиях iOS? Новый синтаксис $ для параметров закрытия - это лишь часть головоломки, но у нас также есть новые API (дополнительные инициализаторы), которые должны быть доступны в предыдущих версиях ОС.

Как мы можем сделать новые API доступными для более ранних версий среды выполнения?

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

Документ немного расплывчато описывает @_alwaysEmitIntoClient, но кажется, что аннотирование функции, вычисляемого свойства или индекса с помощью этого атрибута приведет к передаче кода в двоичный файл клиента (то есть в наше приложение), что позволит использовать новые API. в предыдущих ABI-стабильных версиях среды выполнения Swift. Похоже, что это то, что команда сделала для этой конкретной функции.

Если вы хотите углубиться в эту тему, то в сообщении блога по эволюции библиотеки более подробно рассказывается об основных концепциях и о том, когда следует включить поддержку эволюции библиотеки (а когда нет).

Закрытие

Я всегда нахожу удивительным, насколько инженерные разработки вкладываются в кажущиеся мелкими особенности языка и фреймворка, которые мы часто принимаем как должное. Спасибо всем, кто над этим работал!

Как Мэтт упоминает в Что нового в SwiftUI (на отметке 7:37), эта функция поможет нам создавать больше безошибочных (и высокопроизводительных!) Приложений, так что сейчас хорошее время, чтобы пройтись по вашему коду и обновите все свои списки, чтобы использовать новые функции.

Спасибо за прочтение!

Первоначально опубликовано на https://peterfriese.dev.