Сложность в UIKit, легкий ветерок с SwiftUI
Отображение простого списка значений в SwiftUI чрезвычайно просто по сравнению с созданием UITableView в UIKit. Здесь нет делегатов настройки, соответствия протоколам или добавления табличного представления к контроллеру представления.
В этой статье я собираюсь сделать еще один шаг вперед. Я собираюсь показать вам, как создать расширяющийся список с возможностью выбора нескольких значений. Не было простого способа создать расширяющийся список в UIKit. На самом деле для этого существовали даже библиотеки.
Вместо того, чтобы идти по коду шаг за шагом, я кратко объясню каждую часть и покажу весь код в конце, а также ссылку на завершенный проект.
Введение в проект
Предположим, мы собираемся создать представление для приложения для отслеживания расходов. Вы хотите отобразить список категорий, которые пользователь может назначить для записи о расходах. Для простоты предоставленный код позволит выбрать подкатегорию и ее родительский элемент. Я оставлю это на ваше усмотрение, чтобы найти умный способ исключить родительский выбор, если выбрана какая-либо из его подкатегорий 😃.
Компоненты
Модель данных
Чтобы не усложнять этот пример, у нас будет только один тип для нашей модели данных. Фактически, чтобы это работало, дети должны быть того же типа, что и родитель. Позвольте мне объяснить с помощью кода:
struct Category: Identifiable { | |
let id = UUID().uuidString | |
var name: String | |
var subcategories: [Category]? //this type MUST be the same as the parent | |
var sortedSubcategories: [Category]? { | |
subcategories?.sorted(by: { $0.name < $1.name }) | |
} | |
} |
Чтобы удовлетворить List
требованиям SwiftUI, мы также должны соответствовать Identifiable
. Кроме того, наша коллекция выбранных категорий будет Set
, поэтому у нас должен быть способ однозначно идентифицировать каждую категорию.
Обратите внимание, что подкатегории того же типа, что и родительские. Это обязательное требование для использования функции расширяющегося списка SwiftUI.
Посмотреть модель
Опять же, чтобы не усложнять этот пример, мы не будем извлекать JSON из сетевого вызова для отображения списка данных. Вместо этого мы создадим список по умолчанию из нескольких родительских категорий и их потомков.
class CategoryViewModel: ObservableObject { | |
@Published var categories = [Category]() | |
init() { | |
categories = [ | |
Category(name: "Bills", subcategories: [ | |
Category(name: "Internet"), | |
Category(name: "Electric"), | |
Category(name: "Gas"), | |
]), | |
Category(name: "Car", subcategories: [ | |
Category(name: "Gas"), | |
Category(name: "Payment"), | |
Category(name: "Maintenance"), | |
]), | |
Category(name: "Food", subcategories: [ | |
Category(name: "Eating Out"), | |
Category(name: "Groceries"), | |
Category(name: "Coffee"), | |
]), | |
].sorted(by: { $0.name < $1.name }) | |
} | |
} |
Когда объект соответствует ObservableObject
, каждый раз, когда свойство категорий изменяется, событие публикуется через своего издателя. Чтобы узнать больше о ObservableObject
, ознакомьтесь с документацией Apple. Вы сможете увидеть это в действии к концу проекта, когда мы добавим в список новую подкатегорию.
Просмотр списка
Наконец, мы подошли к главному событию. Вот полный код представления. Я объясню ключевые ингредиенты ниже.
struct ContentView: View { | |
@StateObject private var categoryViewModel = CategoryViewModel() | |
@State private var selectedCategories: Set<String> = [] | |
var body: some View { | |
NavigationView { | |
List(categoryViewModel.categories, children: \.sortedSubcategories, rowContent: { row in | |
Image(systemName: selectedCategories.contains(row.id) ? "checkmark.square.fill" : "square") | |
.resizable() | |
.frame(width: 20, height: 20, alignment: .center) | |
.foregroundColor(Color(UIColor.systemPink)) | |
Label(row.name, systemImage: categoryViewModel.icons[row.name] ?? "") | |
.frame(width: UIScreen.main.bounds.width - 90, height: 30, alignment: .leading) | |
.background(Color.init(.systemGray5)) //just to show how far the label stretches | |
.foregroundColor(.blue) | |
.font(.headline) | |
.onTapGesture { | |
if selectedCategories.contains(row.id) { | |
selectedCategories.remove(row.id) | |
} else { | |
selectedCategories.insert(row.id) | |
} | |
} | |
}) | |
.toolbar(content: { | |
Button(action: { | |
let randomIndex = Int.random(in: 0...2) | |
categoryViewModel.categories[randomIndex].subcategories?.append(Category(name: "NEW")) | |
}, label: { | |
Image(systemName: "plus") | |
}) | |
}) | |
.navigationTitle("Categories") | |
} | |
} | |
} |
- Мы создаем экземпляр модели представления и удерживаем его, отмечая его с помощью
@StateObject
. - Нам нужна коллекция для хранения строкового значения UUID выбранных категорий. Поскольку мы хотим убедиться, что у нас есть только одна копия выбранной категории и порядок не важен, мы будем использовать
Set
. - Мы создаем
List
, передаваемый в массив категорий, и используем путь ключа свойства subcategories. Помните, что дети должны быть того же типа, что и родитель. - Для каждой ячейки (жаргон UIKit) или строки у нас есть
Image
для отображения, если категория выбрана, иLabel
, отображающий значок и имя категории. Стрелка индикатора раскрытия информации справа предоставляется SwiftUI для этого конкретного типаList
. Метка растягивается на всю ширину строки, чтобы дать больше места для распознавания жестов. Подробнее об этом дальше. - В
Label
каждой строки естьonTapGesture
, чтобы определить, когда пользователь выбрал категорию. Считайте это эквивалентомdidSelectRowAt
метода UIKit. Когда категория выбрана, она либо добавит ее в набор, либо удалит, если она уже существует. - Наконец, я добавил кнопку на панели инструментов, чтобы добавить категорию по умолчанию со случайным индексом, чтобы продемонстрировать анимацию при ответе на добавления.
Заключение
Как видите, эта функциональность относительно проста с небольшими базовыми знаниями о том, как работает SwiftUI. Было бы сложно объяснить это в UIKit без встроенной функциональности от Apple.
Кстати, я прекрасно понимаю, что никаких дизайнерских наград этот проект не выиграет. 😀
Вы можете получить полную версию проекта здесь.
Удачного кодирования!