Fix issues with waterfall widget

This commit is contained in:
Marcin Czachurski 2023-09-19 18:43:00 +02:00
parent e92226f4fd
commit 80b2d2f4e9
5 changed files with 119 additions and 43 deletions

View File

@ -191,9 +191,12 @@
F8B0885E29942E31002AB40A /* ThirdPartyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885D29942E31002AB40A /* ThirdPartyView.swift */; };
F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B0885F29943498002AB40A /* OtherSectionView.swift */; };
F8B08862299435C9002AB40A /* SupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B08861299435C9002AB40A /* SupportView.swift */; };
F8B758DE2AB9DD85000C8068 /* ColumnData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B758DD2AB9DD85000C8068 /* ColumnData.swift */; };
F8D5444329D4066C002225D6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D5444229D4066C002225D6 /* AppDelegate.swift */; };
F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */; };
F8DF38E629DDB98A0047F1AA /* SocialsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */; };
F8E36E462AB8745300769C55 /* Sizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E452AB8745300769C55 /* Sizable.swift */; };
F8E36E482AB874A500769C55 /* StatusModel+Sizeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */; };
F8E6D03329CDD52500416CCA /* EditProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E6D03229CDD52500416CCA /* EditProfileView.swift */; };
F8F6E44229BC58F20004795E /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; };
F8F6E44C29BCC1F70004795E /* PhotoSmallWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F6E44629BCC0DC0004795E /* PhotoSmallWidgetView.swift */; };
@ -393,12 +396,15 @@
F8B08861299435C9002AB40A /* SupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportView.swift; sourceTree = "<group>"; };
F8B3699A29D86EB600BE3808 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
F8B3699B29D86EBD00BE3808 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
F8B758DD2AB9DD85000C8068 /* ColumnData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnData.swift; sourceTree = "<group>"; };
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-001.xcdatamodel"; sourceTree = "<group>"; };
F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-005.xcdatamodel"; sourceTree = "<group>"; };
F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = "<group>"; };
F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialsSectionView.swift; sourceTree = "<group>"; };
F8DF38E729DDC3D20047F1AA /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = "<group>"; };
F8E36E452AB8745300769C55 /* Sizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizable.swift; sourceTree = "<group>"; };
F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusModel+Sizeable.swift"; sourceTree = "<group>"; };
F8E6D03229CDD52500416CCA /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = "<group>"; };
F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-006.xcdatamodel"; sourceTree = "<group>"; };
F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-012.xcdatamodel"; sourceTree = "<group>"; };
@ -535,6 +541,8 @@
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */,
F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */,
F8624D3C29F2D3AC00204986 /* SelectedMenuItemDetails.swift */,
F8E36E452AB8745300769C55 /* Sizable.swift */,
F8B758DD2AB9DD85000C8068 /* ColumnData.swift */,
);
path = Models;
sourceTree = "<group>";
@ -653,6 +661,7 @@
isa = PBXGroup;
children = (
F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */,
F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1185,6 +1194,7 @@
F87AEB922986C44E00434FB6 /* AuthorizationSession.swift in Sources */,
F86A4301299A97F500DF7645 /* ProductIdentifiers.swift in Sources */,
F89D6C4229717FDC001DA3D4 /* AccountsSectionView.swift in Sources */,
F8E36E482AB874A500769C55 /* StatusModel+Sizeable.swift in Sources */,
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */,
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */,
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */,
@ -1201,6 +1211,7 @@
F883402029B62AE900C3E096 /* SearchView.swift in Sources */,
F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */,
F871F21D29EF0D7000A351EF /* NavigationMenuItemDetails.swift in Sources */,
F8E36E462AB8745300769C55 /* Sizable.swift in Sources */,
F85DBF8F296732E20069BF89 /* AccountsView.swift in Sources */,
F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */,
F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */,
@ -1220,6 +1231,7 @@
F891E7CE29C35BF50022C449 /* ImageRowItem.swift in Sources */,
F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */,
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */,
F8B758DE2AB9DD85000C8068 /* ColumnData.swift in Sources */,
F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */,
F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */,
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
@ -1325,18 +1337,19 @@
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 143;
CURRENT_PROJECT_VERSION = 204;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = VernissageWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.9.0;
MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1353,18 +1366,19 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 143;
CURRENT_PROJECT_VERSION = 204;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageWidget/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = VernissageWidget;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.9.0;
MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1380,19 +1394,19 @@
CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 143;
CURRENT_PROJECT_VERSION = 204;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageShare/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = VernissageShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.9.0;
MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1407,19 +1421,19 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 143;
CURRENT_PROJECT_VERSION = 204;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageShare/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = VernissageShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.9.0;
MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1480,7 +1494,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -1535,7 +1549,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
@ -1556,7 +1570,7 @@
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 201;
CURRENT_PROJECT_VERSION = 204;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;
@ -1570,7 +1584,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1598,7 +1612,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 201;
CURRENT_PROJECT_VERSION = 204;
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
DEVELOPMENT_TEAM = B2U9FEKYP8;
ENABLE_PREVIEWS = YES;
@ -1612,7 +1626,7 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -0,0 +1,18 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import ClientKit
extension StatusModel: Sizable {
public var height: Double {
return Double(self.mediaAttachments.first?.metaImageHeight ?? 500)
}
public var width: Double {
return Double(self.mediaAttachments.first?.metaImageWidth ?? 500)
}
}

View File

@ -0,0 +1,13 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
class ColumnData<T>: Identifiable where T: Identifiable, T: Hashable, T: Sizable {
public let id = UUID().uuidString
public var data: [T] = []
public var height: Double = 0.0
}

View File

@ -0,0 +1,12 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
public protocol Sizable {
var height: Double { get }
var width: Double { get }
}

View File

@ -8,30 +8,35 @@ import SwiftUI
import WidgetsKit
struct WaterfallGrid<Data, ID, Content>: View where Data: RandomAccessCollection, Data: Equatable, Content: View,
ID: Hashable, Data.Element: Equatable, Data.Element: Identifiable, Data.Element: Hashable {
ID: Hashable, Data.Element: Equatable, Data.Element: Identifiable, Data.Element: Hashable, Data.Element: Sizable {
@Binding private var columns: Int
@Binding private var hideLoadMore: Bool
@Binding private var data: Data
private let dataId: KeyPath<Data.Element, ID>
private let content: (Data.Element) -> Content
@State private var columnsData: [[Data.Element]] = []
@State private var columnsData: [ColumnData<Data.Element>] = []
@State private var processedItems: [Data.Element.ID] = []
@State private var isDuringLoading: Bool = false
private let onLoadMore: () async -> Void
var body: some View {
HStack(alignment: .top, spacing: 20) {
ForEach(self.columnsData, id: \.self) { array in
ForEach(self.columnsData, id: \.id) { columnData in
LazyVStack(spacing: 8) {
ForEach(array, id: \.id) { item in
ForEach(columnData.data, id: \.id) { item in
self.content(item)
}
if self.shouldShowSpinner(array: array) {
if self.hideLoadMore == false {
LoadingIndicator()
.task {
await self.onLoadMore()
if isDuringLoading == false {
isDuringLoading = true
await self.onLoadMore()
isDuringLoading = false
}
}
}
}
@ -41,7 +46,7 @@ struct WaterfallGrid<Data, ID, Content>: View where Data: RandomAccessCollection
self.recalculateArrays()
}
.onChange(of: self.data) { _ in
self.recalculateArrays()
self.appendToArrays()
}
.onChange(of: self.columns) { _ in
self.recalculateArrays()
@ -49,52 +54,66 @@ struct WaterfallGrid<Data, ID, Content>: View where Data: RandomAccessCollection
}
private func recalculateArrays() {
var internalArray: [[Data.Element]] = []
self.columnsData = []
self.processedItems = []
for _ in 0 ..< self.columns {
internalArray.append([])
self.columnsData.append(ColumnData())
}
for (index, item) in self.data.enumerated() {
let arrayIndex = index % self.columns
internalArray[arrayIndex].append(item)
}
for item in self.data {
let index = self.minimumHeightIndex()
self.columnsData = internalArray
self.columnsData[index].data.append(item)
self.columnsData[index].height = self.columnsData[index].height + self.calculateHeight(item: item)
self.processedItems.append(item.id)
}
}
private func shouldShowSpinner(array: [Data.Element]) -> Bool {
if self.hideLoadMore {
return false
}
private func appendToArrays() {
for item in self.data where self.processedItems.contains(where: { $0 == item.id }) == false {
let index = self.minimumHeightIndex()
return self.columnsData[1].first == array.first
self.columnsData[index].data.append(item)
self.columnsData[index].height = self.columnsData[index].height + self.calculateHeight(item: item)
self.processedItems.append(item.id)
}
}
private func calculateHeight(item: Sizable) -> Double {
return item.height / item.width
}
private func minimumHeight() -> Double {
return self.columnsData.map({ $0.height }).min() ?? .zero
}
private func minimumHeightIndex() -> Int {
let minimumHeight = self.minimumHeight()
return self.columnsData.lastIndex(where: { $0.height == minimumHeight }) ?? 0
}
}
extension WaterfallGrid {
init(_ data: Binding<Data>, id: KeyPath<Data.Element, ID>, columns: Binding<Int>,
hideLoadMore: Binding<Bool>, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) {
self._data = data
self.dataId = id
self.content = content
self.onLoadMore = onLoadMore
self._data = data
self._columns = columns
self._hideLoadMore = hideLoadMore
self.onLoadMore = onLoadMore
}
}
extension WaterfallGrid where ID == Data.Element.ID, Data.Element: Identifiable {
init(_ data: Binding<Data>, columns: Binding<Int>,
hideLoadMore: Binding<Bool>, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) {
self._data = data
self.dataId = \Data.Element.id
self.content = content
self.onLoadMore = onLoadMore
self._data = data
self._columns = columns
self._hideLoadMore = hideLoadMore
self.onLoadMore = onLoadMore
}
}