SwiftUI — это современный UI framework, который позволяет разработчикам быстро и легко создавать собственные приложения на всех платформах Apple. Используя простой, понятный декларативный стиль, разработчики могут создавать потрясающие пользовательские интерфейсы с плавной анимацией. SwiftUI экономит время разработчиков, предоставляя огромное количество готовых решений, включая Interface Layout, Dark Mode, Accessibility, интернационализацию и многое другое. Приложения SwiftUI работают нативно и невероятно быстро. А поскольку SwiftUI — это один и тот же API, встроенный в iOS, iPadOS, macOS, watchOS и tvOS, разработчики могут быстрее и проще создавать отличные нативные приложения для всех платформ Apple. Звучит amazing, не правда ли? SwiftUI был анонсирован на WWDC2019 и за последний год было написано множество статей, посвященных этому фреймворку. Поэтому в данной статье мы не будем заострять внимание на таких вещах, как а сразу перейдем к практике и сделаем достаточно стандартную в повседневной жизни задачу — создание горизонтального списка. Будет очень много кода и мало комментариев, впрочем, все как мы любим. Горизонтальный список можно создать достаточно просто. Для этого необходимо поместить HStack в ScrollView и заполнить HStack нашими элементами: Для практической наглядности создадим View, которая будет отображать список из 100 карточек. Каждая карточка будет отображать случайно сгенерированный смайлик и индекс самой карточки. Запускаем и вуаля, как и обещала Apple — все нативно и невероятно быстро.   Но есть одна проблема — если количество данных увеличится, то мы столкнемся с проблемой. Запускаем и ...  Для решения этой проблемы в UIKit мы бы использовали UICollectionView. Но, к сожалению, не всё, что было возможно при использовании UIKit, имеет аналог в SwiftUI.  Конечно, можно было бы использовать UICollectionView напрямую в SwiftUI. Как это сделать можно прочитать здесь. Но это уже не SwiftUI и определенно не наш путь. На данный момент единственная структура в SwiftUI, которая загружает и отображает данные только по необходимости (on demand) это List. Возьмем это решение от Apple и создадим схожее API для нашей структуры HorizontalList: Обновим наш пример с карточками и будем использовать собственное решение: Запускаем и (через какое-то время..) видим, что карточки успешно загрузились и отобразились:  Большую часть времени SwiftUI будет сам делать свою магию по расположению элементов и жизнь будет великолепной. Однако бывают случаи, когда нам требуется больше контроля для расположения наших собственных Views. Для этого у нас есть несколько инструментов. Одним из них является GeometryReader, его мы и будем использовать для получения информации о размерах и позициях элементов внутри него. GeometryReader - A container view that defines its content as a function of its own size and coordinate space. Вместо тысячи слов про GeometryReader, я бы посоветовал один раз перейти по ссылке и прочитать очень хорошую статью по теме. Помимо расположения элементов в нашем HorizontalList нам необходимо знать размеры этих элементов. Естественно, мы могли бы задать статический размер, но это не то, как работает List и соответственно не то, как будем работать мы.  В SwiftUI есть механизм, который позволяет добавлять некоторые атрибуты к View. Эти атрибуты называются "Preferences". При изменении этих атрибутов мы будем получать callback. Первым делом нам необходимо создать структуру данных для атрибута. В ней мы будем хранить индекс элемента и его Rect. Структура должна поддерживать протокол Equatable. Следующим шагом создадим сам аттрибут, поддерживающий протокол PreferenceKey и содержащий в себе массив значений ViewRectPreferenceData. Так как нам нужно будет знать размеры элементов, а сделать это можно только с помощью GeometryReader, мы создадим специальную View, которая будет добавлена как child к нашим элементам и, как результат, иметь GeometryReader с размерами нашего элемента. Добавим созданный PreferenceSetterView к нашим элементам, как background: При изменении Preference вызывается метод onPreferenceChange, в нем то мы и получим размеры отображенных на экране элементов и сохраним их в наш массив. Так как у нас горизонтальный список, также мы будем высчитывать отступ элементов согласно отступу и размеру предыдущего элемента. Про Preferences eсть хорошая серия статей из 3-х частей:
Часть 1
Часть 2
Часть 3 Теперь, когда мы имеем размеры элементов и размеры экрана, мы легко можем посчитать какие элементы должны быть видимыми. Ниже реализован метод updateVisibleIndices и места откуда он будет вызываться: (пытливый критический взгляд может заметить, что количество visibleIndices не может быть меньше 1 и правильней было бы иметь это значение опциональным, но для простоты оставим как есть) Протестируем, используя метод onAppear(), который вызывается при появлении элемента на экране: Отлично, как видно из лога после запуска появились на экран только 3 элемента. Следующим шагом развития нашего компонента будет поддержка Drag Gestures для скроллинга данных. Так как изменение позиции скролла влияет на отображаемые элементы, переменные offset и dragOffset будут State переменными. Добавим к нашему компоненту DragGesture и его обработчики: В результате у нас появится вычисляемый contentOffset, который мы будем применять для калькуляции видимого фрейма и позиций элементов: Запускаем приложение:  Вот мы и реализовали основную логику для горизонтального скролла с большим количеством данных и загрузкой их по необходимости. Текущая реализация скролла элементов с помощью drag gesture не учитывает velocity. Настало время улучшить логику скролла и исправить этот пробел. Для того, чтобы учесть скорость прокрутки нам необходима анимация. Любая анимация строится на изменении значений за какой-то период времени. Для того чтобы фиксировать периоды времени нам необходим таймер: Теперь когда у нас есть таймер, надо знать, что анимировать. Список получается довольно простой: startPosition, endPosition и scrollDuration. Единственное, так как нам не надо перегружать View при изменении какого-либо из этих значений, мы их упакуем в модель класса: И завершающим шагом интегрируем модель анимации с таймером в наше View: В нашем рабочем компоненте есть одна проблема — можно спокойно проскроллить туда где нет данных. Поэтому следует добавить логику, которая будет проверять не вышли ли мы за границы. Реализуем метод safeOffset, возвращающий безопасные значения для отступа. У CGFloat нет метода clamped, но его можно легко добавить с помощью расширения: Ниже представлен полный код с получением возможного максимального отступа и использованием метода safeOffset: Для увеличения производительности, чтобы не создавать каждый раз элементы, создадим простой кэш, который будет чиститься в случае нехватки памяти:  Готовый компонент вы можете найти по адресу
Предыдущая наша статья на тему SwiftUI доступна здесь Статья написана моим коллегой Денисом Шалагиным и опубликована для сообщества по его просьбе. Всем счастливого WWDC2020!