SwiftUI теперь позволяет использовать привязки элементов в списках. Посмотрите, как это реализовано за кулисами
Списки, вероятно, являются одним из самых популярных элементов пользовательского интерфейса в приложениях для iOS, и мы прошли долгий путь с момента появления UITableViewController
. Создание списков в UIKit было не совсем ракетной наукой, но для этого требовалась некоторая церемония.
SwiftUI упростил создание списков. Посмотрите этот фрагмент, который отображает список дел:
С помощью всего трех строк кода мы можем создать простой список.
Все становится немного сложнее, когда мы пытаемся сделать элементы в строках списка доступными для редактирования. Например, TextField
требует привязки к элементу, который мы хотим редактировать:
Однако до сих пор SwiftUI не предоставлял простого способа доступа к изменяемым привязкам к элементам коллекции. Вот почему мы создали такой код:
Этот код не только выглядит намного сложнее, чем нужно, но и заставляет SwiftUI повторно отображать весь список, даже если мы изменим только один элемент в списке. Это может привести к медленным обновлениям пользовательского интерфейса и мерцанию.
Начиная с WWDC 2021, SwiftUI поддерживает привязки для элементов списка. Чтобы использовать эту функцию, все, что нам нужно сделать, это передать привязку к коллекции в список, и SwiftUI передаст нам привязку к текущему элементу в замыкании:
Этот код легче читать, и он похож на исходный код, с которого мы начали. И самое приятное: вы можете развернуть этот код обратно в любой выпуск iOS, поддерживающий SwiftUI.
За кулисами
Я уверен, что вы оцените повышенную простоту вашего кода. Более простой код означает меньше ошибок, а также упрощает чтение и запись.
Но как это работает?
Что ж, есть несколько вещей, которые объединяются, чтобы сделать это возможным. Давайте сначала посмотрим, что позволяет использовать привязки в List
(и ForEach
, кстати).
Давайте перейдем к определению инициализатора 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.