На основе дизайна Dribbble

Недавно, когда я работал над реализацией SwiftUI дизайна, найденного на Dribbble, мне пришла в голову идея расширить этот проект несколькими классными фильтрами, чтобы сузить список результатов.
Я решил, что представление фильтра будет состоять из двух отдельных параметров фильтра, в каждом из них будет несколько параметров для выбора. Но потом я столкнулся с проблемой. Когда я работал с UIKit, я всегда реализовывал такое представление как UICollectionView с определенным UICollectionViewFlowLayout. Но как я могу сделать это в SwiftUI?
Давайте посмотрим на мою реализацию гибкого средства выбора со SwiftUI!
Выбираемый протокол
Самая важная часть средства выбора заключается в том, что мы можем выбрать некоторые необходимые параметры благодаря этому компоненту представления. Вот почему в начале я создал протокол Selectable.
Все объекты, соответствующие этому протоколу, должны реализовать два свойства: displayedName — имя, которое будет отображаться в средстве выбора, и isSelected — логическое значение, указывающее, выбрана ли данная опция или нет.
Более того, чтобы можно было создавать объекты Selectable, просто сопоставляя массив значений String, пользовательское init с displayedName в качестве аргумента должно быть предоставлено объектом, соответствующим Selectable.
Протоколы Identifiable и Hashable гарантируют, что мы сможем легко создать представление SwiftUI с циклом ForEach. Кроме того, все объекты, соответствующие протоколу Selectable, будут реализовывать постоянное значение id, хранящее значение UUID.
Я намеренно опускаю реализацию объекта, соответствующего протоколу Selectable, так как считаю это очевидным. Если нет, то вы сможете проверить это в моем репозитории GitHub, упомянутом в конце этой статьи.
Настройка
Моей целью было не только создать реализацию гибкого средства выбора, но и сделать его максимально настраиваемым.
Вот почему FlexiblePicker будет создан с использованием универсального типа T, который соответствует протоколу Selectable. Таким образом, впоследствии будет легче повторно использовать этот компонент, поскольку он будет независимым от типа.
Перед реализацией самого сборщика я записал все настраиваемые свойства. Следующим шагом было создание расширений String для вычисления ширины и высоты определенного строкового значения.
Поскольку моя реализация дает возможность изменить размер и вес шрифта, оба расширения, упомянутые ранее, принимают в качестве аргумента UIFont, используемый гибким средством выбора.
Поскольку мои строковые расширения для вычисления размера заданной строки принимают в качестве входных данных UIFont, мне нужно было преобразовать все веса UIFont в эквиваленты SwiftUI.
Вот почему я ввел перечисление FontWeight, состоящее из всех возможных случаев, названных в честь UIFont весов.
Кроме того, это перечисление имеет два свойства: одно возвращает вес UIFont, а второе — вес шрифта SwiftUI. Таким образом, мы просто предоставляем конкретный случай перечисления FontWeight нашему FlexiblePicker.
Логика гибкого подборщика
После этого я наконец был готов начать работу над реализацией FlexiblePicker.
Прежде всего, мне нужна была функция для вычисления и возврата ширины всех данных, переданных на вход. Я сделал это, сопоставив все входные значения с кортежами, состоящими из входного значения и самой ширины.
Внутри карты я использовал функцию уменьшения, чтобы суммировать все ширины, связанные с заданным входным значением (ширина текста, ширина границы, отступы текста, интервалы).
Теперь, когда функция для расчета ширины готова, мы можем перебрать все входные данные и разбить их на отдельные массивы. Каждый массив состоит из элементов, которые могут поместиться в одно и то же HStack. Логика проста. У нас есть массивы:
singleLineResultмассив — отвечает за хранение элементов, которые помещаются в определенную строкуallLinesResultмассив — отвечает за хранение всех массивов элементов (каждый массив является эквивалентом строки элементов)
Во-первых, мы проверяем, больше ли результат вычитания ширины элемента из ширины строки HStack 0.
Если условие выполнено, мы добавляем текущий элемент к singleLineResult, обновляем доступную ширину строки HStack и переходим к следующему элементу.
Если результат меньше 0, это означает, что мы не можем поместить следующий элемент в заданную строку, поэтому мы добавляем singleLineResult к allLinesResult, устанавливаем singleLineResult как массив, состоящий только из текущего элемента (тот, который не может поместиться в строку). предыдущая строка) и обновить ширину строки HStack, вычитая ширину текущего элемента.
После прохождения всех элементов мы должны обработать конкретный угловой случай. Возможно, что singleLineResult не будет пустым и не будет добавлено к allLinesResult — так как мы добавляем singleLineResult только тогда, когда результат вычитания ширины элемента будет меньше 0. В этом случае мы должны проверить, является ли singleLineResult пустой, если это правда, мы возвращаем allLinesResult, если нет, мы должны сначала добавить singleLineResult, а затем вернуть allLinesResult.
И последнее, но не менее важное: мы должны вычислить высоту VStack, чтобы облегчить SwiftUI интерпретацию нашего компонента представления. Высота VStack рассчитывается на основе двух значений:
- высота любого элемента внутри входных данных (вычисляется аналогично ширине с помощью функции уменьшения, чтобы суммировать все высоты, связанные с элементом)
- количество строк, которые будут отображаться в
VStack
Результат умножения этих двух чисел будет нашим ростом VStack. Просто, да?
Гибкий вид выбора
Наконец, когда вся логика готова, нам нужно реализовать тело представления. Как я упоминал ранее, представление будет создано с использованием ForEach циклов, один из которых вложен в другой.
Важно иметь в виду, что циклы ForEach требуют, чтобы каждый элемент повторяемой коллекции соответствовал протоколу Identifiable или имел уникальный идентификатор.
Вот почему я сопоставил результаты разделенных строк на кортежи, состоящие из каждой строки и идентификатора, который является значением UUID.
Благодаря этому я могу предоставить аргумент id для цикла ForEach. Второе, что нужно помнить, это то, что цикл ForEach ожидает получить какое-то представление в качестве возвращаемого значения.
Если мы вставим туда еще один цикл ForEach, у нас возникнут проблемы с правильной функциональностью нашего представления, так как ForEach не является видом представления.
Именно поэтому я завернул весь цикл ForEach сначала в HStack, а затем в Group, чтобы убедиться, что компилятор все правильно интерпретирует.
Почти все готово, осталось добавить функцию, которая будет обрабатывать взаимодействие пользователя с кнопками. Функция просто переключает свойство isSelected для конкретных данных.
Остальной код прост, в основном это конфигурация всех свойств, таких как шрифт, цвета или граница. Более того, в конце VStack мы ставим рамку, ширина которой берется из GeometryReader, а высота — вычисляется ранее созданной функцией.

И вот мы готовы! Наш FlexiblePicker готов и готов к использованию!
Надеюсь, вам понравилось! Как всегда, вся реализация доступна в моем репозитории GitHub. Большое вам спасибо за ваше время.