Impressia/Vernissage/Widgets/WaterfallGrid.swift

160 lines
5.2 KiB
Swift
Raw Normal View History

2023-05-25 17:33:04 +02:00
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
import WidgetsKit
2023-09-20 16:22:15 +02:00
import Semaphore
2023-05-25 17:33:04 +02:00
2023-05-26 16:06:38 +02:00
struct WaterfallGrid<Data, ID, Content>: View where Data: RandomAccessCollection, Data: Equatable, Content: View,
2023-09-19 18:43:00 +02:00
ID: Hashable, Data.Element: Equatable, Data.Element: Identifiable, Data.Element: Hashable, Data.Element: Sizable {
2023-05-25 17:33:04 +02:00
@Binding private var columns: Int
@Binding private var hideLoadMore: Bool
2023-05-26 16:06:38 +02:00
@Binding private var data: Data
2023-10-08 11:35:45 +02:00
@Binding private var refreshId: String
2023-09-19 18:43:00 +02:00
2023-05-26 16:06:38 +02:00
private let content: (Data.Element) -> Content
2023-09-19 18:43:00 +02:00
@State private var columnsData: [ColumnData<Data.Element>] = []
@State private var processedItems: [Data.Element.ID] = []
2023-10-08 11:35:45 +02:00
@State private var shouldRecalculate = false
2023-05-25 17:33:04 +02:00
private let onLoadMore: () async -> Void
2023-09-20 16:22:15 +02:00
private let semaphore = AsyncSemaphore(value: 1)
2023-05-25 17:33:04 +02:00
var body: some View {
2023-09-27 15:43:00 +02:00
HStack(alignment: .top, spacing: 8) {
2023-09-19 18:43:00 +02:00
ForEach(self.columnsData, id: \.id) { columnData in
2023-05-25 17:33:04 +02:00
LazyVStack(spacing: 8) {
2023-09-19 18:43:00 +02:00
ForEach(columnData.data, id: \.id) { item in
2023-05-25 17:33:04 +02:00
self.content(item)
}
2023-09-19 18:43:00 +02:00
if self.hideLoadMore == false {
2023-09-20 16:22:15 +02:00
// We can show multiple loading indicators. Each indicator can run loading feature in pararell.
// Thus we have to be sure that loading will exeute one by one.
2023-05-25 17:33:04 +02:00
LoadingIndicator()
.task {
2023-09-20 16:22:15 +02:00
Task { @MainActor in
await self.loadMoreData()
2023-09-19 18:43:00 +02:00
}
2023-05-25 17:33:04 +02:00
}
}
}
}
}
.onFirstAppear {
self.recalculateArrays()
}
2023-10-19 09:29:49 +02:00
.onChange(of: self.refreshId) {
2023-10-08 11:35:45 +02:00
self.shouldRecalculate = true
}
2023-10-19 09:29:49 +02:00
.onChange(of: self.data) {
2023-10-08 11:35:45 +02:00
if self.shouldRecalculate {
self.recalculateArrays()
self.shouldRecalculate = false
} else {
self.appendToArrays()
}
2023-05-25 17:33:04 +02:00
}
2023-10-19 09:29:49 +02:00
.onChange(of: self.columns) {
2023-05-25 17:33:04 +02:00
self.recalculateArrays()
}
}
2023-09-20 16:22:15 +02:00
private func loadMoreData() async {
await semaphore.wait()
defer { semaphore.signal() }
2023-05-25 17:33:04 +02:00
2023-09-20 16:22:15 +02:00
await self.onLoadMore()
}
2023-05-25 17:33:04 +02:00
2023-09-20 16:22:15 +02:00
private func recalculateArrays() {
Task { @MainActor in
await semaphore.wait()
defer { semaphore.signal() }
self.columnsData = []
self.processedItems = []
for _ in 0 ..< self.columns {
self.columnsData.append(ColumnData())
}
for item in self.data {
let index = self.minimumHeightIndex()
self.columnsData[index].data.append(item)
self.columnsData[index].height = self.columnsData[index].height + self.calculateHeight(item: item)
self.processedItems.append(item.id)
}
2023-09-19 18:43:00 +02:00
}
2023-05-25 17:33:04 +02:00
}
2023-09-19 18:43:00 +02:00
private func appendToArrays() {
2023-09-20 16:22:15 +02:00
Task { @MainActor in
await semaphore.wait()
defer { semaphore.signal() }
for item in self.data where self.processedItems.contains(where: { $0 == item.id }) == false {
let index = self.minimumHeightIndex()
self.columnsData[index].data.append(item)
self.columnsData[index].height = self.columnsData[index].height + self.calculateHeight(item: item)
self.processedItems.append(item.id)
}
2023-05-25 17:33:04 +02:00
}
2023-09-19 18:43:00 +02:00
}
private func calculateHeight(item: Sizable) -> Double {
return item.height / item.width
}
2023-05-25 17:33:04 +02:00
2023-09-19 18:43:00 +02:00
private func minimumHeight() -> Double {
return self.columnsData.map({ $0.height }).min() ?? .zero
2023-05-26 16:06:38 +02:00
}
2023-09-19 18:43:00 +02:00
private func minimumHeightIndex() -> Int {
let minimumHeight = self.minimumHeight()
2023-09-24 16:43:24 +02:00
return self.columnsData.firstIndex(where: { $0.height == minimumHeight }) ?? 0
2023-09-19 18:43:00 +02:00
}
2023-05-26 16:06:38 +02:00
}
extension WaterfallGrid {
2023-10-08 11:35:45 +02:00
init(_ data: Binding<Data>,
refreshId: Binding<String>,
columns: Binding<Int>,
hideLoadMore: Binding<Bool>,
content: @escaping (Data.Element) -> Content,
onLoadMore: @escaping () async -> Void) {
2023-05-26 16:06:38 +02:00
self.content = content
2023-09-19 18:43:00 +02:00
self.onLoadMore = onLoadMore
2023-05-26 16:06:38 +02:00
2023-09-19 18:43:00 +02:00
self._data = data
2023-05-26 16:06:38 +02:00
self._columns = columns
self._hideLoadMore = hideLoadMore
2023-10-08 11:35:45 +02:00
self._refreshId = refreshId
2023-05-26 16:06:38 +02:00
}
}
extension WaterfallGrid where ID == Data.Element.ID, Data.Element: Identifiable {
2023-10-08 11:35:45 +02:00
init(_ data: Binding<Data>,
refreshId: Binding<String>,
columns: Binding<Int>,
hideLoadMore: Binding<Bool>,
content: @escaping (Data.Element) -> Content,
onLoadMore: @escaping () async -> Void) {
2023-05-26 16:06:38 +02:00
self.content = content
2023-09-19 18:43:00 +02:00
self.onLoadMore = onLoadMore
2023-05-26 16:06:38 +02:00
2023-09-19 18:43:00 +02:00
self._data = data
2023-05-26 16:06:38 +02:00
self._columns = columns
self._hideLoadMore = hideLoadMore
2023-10-08 11:35:45 +02:00
self._refreshId = refreshId
2023-05-25 17:33:04 +02:00
}
}