Узнайте, как вычислять смещения в реальном времени в SwiftUI ScrollView.

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

В UIKit каждый UIScrollView имеет свойство, которое позволяет нам легко считывать смещение самого представления:

var contentOffset: CGPoint { get set }

Он возвращает структуру со значениями x и y. Супер просто, супер удобно!

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

В конце этого урока вы сможете создать что-то похожее на это:

Фреймворк SwiftUI часто позволяет (или заставляет) нам мыслить нестандартно для решения задач, и это отличный повод для этого.

Давайте сначала начнем с создания очень простого пользовательского интерфейса с длинным списком и меткой Text, которая не сможет показать реальное значение смещения (конечно, мы добавим эту функцию позже), поскольку вы можете видеть, что переменная verticalOffset никогда не изменяется:

Чтобы добиться результата, показанного выше, мы напишем View, который будет вести себя точно так же, как SwiftUI ScrollView, но каким-то образом транслирует значение своего смещения в реальном времени.

Во-первых, нам нужно создать новую структуру, которая будет соответствовать протоколу PreferenceKey.

Официальная документация Apple дает нам следующее определение этого протокола:

Именованное значение, созданное представлением.
Представление с несколькими дочерними элементами автоматически объединяет свои значения для заданного предпочтения в одно значение, видимое для его предков.

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

Чтобы соответствовать этому протоколу, должны быть реализованы свойство и функция, обе статические:

static var defaultValue: Self.Value { get }
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)

Значением по умолчанию будет наше смещение, CGPoint, с начальным значением 0,0.

Давайте создадим структуру OffsetPreferenceKey вот так:

Отлично, теперь пришло время создать нашу версию scrollView.
Давайте создадим структуру с теми же свойствами, что и SwiftUI ScrollView, но с дополнительным элементом, onOffsetChangedзамыканием, которое срабатывает, когда scrollview изменяет свою позицию:

Как видите, я использовал общий var content для передачи всего содержимого файла ScrollView. Как указано в определении структуры T будет иметь тип View.

Давайте теперь посмотрим на реализацию свойства body:

  • В строке 2 я создал ScrollView
  • В строке 3 GeometryReader содержит пустое представление, Color.Clear без измерений. Нам нужно отслеживать положение представления, и это умная идея использовать безразмерное представление. Внутри него я установил ключ OffsetPreferenceKey, передавая в качестве значения источник самого фрейма. Я использовал coordinateSpace(name:), чтобы позволить другой функции находить и работать с нашим представлением Color, а также работать с измерениями относительно этого представления.
  • Я передаю исходную позицию внешнему миру, запуская замыкание при изменении этого значения в строке 15.
  • В строке 12 я использую content, переданный инициализатору.

Это вся структура:

Готово! У нас есть новый OffsettableScrollView, который сможет передавать значение своего смещения в любое время при его изменении!

Настало время изменить наше исходное представление содержимого с помощью, наконец, подключенного Text:

Все кончено! Как видите, теперь это супер просто и супер чисто!

Надеюсь, вам понравилась эта статья. Если вас интересует видеоверсия, это руководство также есть на моем канале YouTube:

Удачного кодирования!