78. Пример кастомной коллекции
Для создания коллекций в SwiftUI есть как минимум LazyVStack и LazyHStack. В этой статье покажу как можно сделать свою горизонтальную коллекцию с кастомной логикой распределения чипсов по строкам.
Постановка
Будем делать коллекцию с такими параметрами:
- Максимум 2 строки чипсов
- Если чипсов меньше 5, то располагаем их в одну строку
- Если чипсов больше 5, то располагаем их в 2 строки
- Если количество чипсов нечетное, то в первой строке их должно быть больше на 1, чем во второй
- Ожидается общее количество чипсов не больше 15
- При нажатии на чипс нужно вернуть в родительскую вьюху идентификатор нажатого чипса
Решение
Для начала нам понадобится модель чипсины, и сразу сделаем метод для удобного создания превью-заглушки с коллекцией чипсов:
/// Модель для чипсины в коллекции
struct ChipItem: Identifiable, Hashable {
let id: Int
let description: String
}
extension [ChipItem] {
/// Создает массив с заданным количеством чипсов
static func makeDemoList(count: Int) -> [ChipItem] {
(0..<count).map { i in
return .init(
id: i + 1,
description: "Тут чипс № \(i + 1)"
)
}
}
}
Теперь сделаем модель для коллекции, которая будет заниматься распределением чипсов по строкам. Для реализации такого поведения будем использовать структуру и словарь:
/// Модель для коллекции чипсов, создает словарь
/// для отображения коллекции в одну или две строки
struct ChipCollectionModel {
/// Итоговый словарь, где ключ - порядковый номер строки, а значение - чипсы для этой строки
let itemsDict: [Int: [ChipItem]]
/// Количество строк в коллекции
var rows: Int { itemsDict.keys.count }
init(items: [ChipItem]) {
guard !items.isEmpty else {
itemsDict = [:] // Если на вход не поступили элементы, словарь будет пустым
return
}
if items.count < 5 {
itemsDict = [1: items] // Если элементов меньше 5, будет 1 строка
} else {
let count = items.count
let isEven = count % 2 == 0
let midIndex = isEven ? count / 2 : (count + 1) / 2
itemsDict = [
1: Array(items[..<midIndex]),
2: Array(items[midIndex...])
]
}
}
}
struct ChipsCollectionView: View {
private let model: ChipCollectionModel
@Binding private var selection: Int?
/// Инициализатор
/// - Parameters:
/// - items: Список чипсов для коллекции
/// - selection: Идентификатор выбранного чипса
init(items: [ChipItem], selection: Binding<Int?>) {
self.model = .init(items: items)
self._selection = selection
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
VStack(alignment: .leading, spacing: 4) {
ForEach(0..<model.rows, id: \.self) { row in
if let items = model.itemsDict[row + 1] {
HStack(spacing: 4) {
ForEach(items) { item in
makeChipView(for: item)
.onTapGesture { selection = item.id }
}
}
}
}
}
.padding(.horizontal, 4)
}
}
private func makeChipView(for item: ChipItem) -> some View {
Text(item.description)
.foregroundStyle(.primary)
.padding(8)
.background {
RoundedRectangle(cornerRadius: 20)
.fill(.secondary.opacity(0.3))
}
}
}
Скриншоты
Бонус - тесты
Напишем unit-тесты на модель для коллекции:
import XCTest
@testable import DemoApp
final class ChipCollectionModelTests: XCTestCase {
func testEmptyCollection() {
let model = ChipCollectionModel(items: [])
XCTAssertEqual(model.rows, 0)
XCTAssertTrue(model.itemsDict.isEmpty)
}
func testSingleRowCollection() {
let items = [
ChipItem(id: 1, description: "Chips A"),
ChipItem(id: 2, description: "Chips B"),
ChipItem(id: 3, description: "Chips C"),
ChipItem(id: 4, description: "Chips D")
]
let model = ChipCollectionModel(items: items)
XCTAssertEqual(model.rows, 1)
XCTAssertEqual(model.itemsDict.count, 1)
XCTAssertEqual(model.itemsDict[1]?.count, 4)
XCTAssertEqual(model.itemsDict[1], items)
}
func testTwoRowCollectionEvenCount() {
let items = [
ChipItem(id: 1, description: "Chips A"),
ChipItem(id: 2, description: "Chips B"),
ChipItem(id: 3, description: "Chips C"),
ChipItem(id: 4, description: "Chips D"),
ChipItem(id: 5, description: "Chips E"),
ChipItem(id: 6, description: "Chips F")
]
let model = ChipCollectionModel(items: items)
XCTAssertEqual(model.rows, 2)
XCTAssertEqual(model.itemsDict.count, 2)
XCTAssertEqual(model.itemsDict[1]?.count, 3)
XCTAssertEqual(model.itemsDict[2]?.count, 3)
XCTAssertEqual(model.itemsDict[1], Array(items[0...2]))
XCTAssertEqual(model.itemsDict[2], Array(items[3...5]))
}
func testTwoRowCollectionOddCount() {
let items = [
ChipItem(id: 1, description: "Chips A"),
ChipItem(id: 2, description: "Chips B"),
ChipItem(id: 3, description: "Chips C"),
ChipItem(id: 4, description: "Chips D"),
ChipItem(id: 5, description: "Chips E")
]
let model = ChipCollectionModel(items: items)
XCTAssertEqual(model.rows, 2)
XCTAssertEqual(model.itemsDict.count, 2)
XCTAssertEqual(model.itemsDict[1]?.count, 3)
XCTAssertEqual(model.itemsDict[2]?.count, 2)
XCTAssertEqual(model.itemsDict[1], Array(items[0...2]))
XCTAssertEqual(model.itemsDict[2], Array(items[3...4]))
}
}
Заключение
Вот таким нехитрым способом можно сделать свою коллекцию в SwiftUI - на мой взгляд это намного удобнее, чем в UIKit 😁
Код для этой статьи можно посмотреть тут, другие статьи по разработке - тут, а про инвестиции - тут.