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
|
|
|
}
|
|
|
|
}
|