На основе дизайна 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. Большое вам спасибо за ваше время.