diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index f3c2995..b636915 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -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 = ""; }; F8B3699A29D86EB600BE3808 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; F8B3699B29D86EBD00BE3808 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + F8B758DD2AB9DD85000C8068 /* ColumnData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnData.swift; sourceTree = ""; }; F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-001.xcdatamodel"; sourceTree = ""; }; F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-005.xcdatamodel"; sourceTree = ""; }; F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = ""; }; F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialsSectionView.swift; sourceTree = ""; }; F8DF38E729DDC3D20047F1AA /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; + F8E36E452AB8745300769C55 /* Sizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sizable.swift; sourceTree = ""; }; + F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusModel+Sizeable.swift"; sourceTree = ""; }; F8E6D03229CDD52500416CCA /* EditProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileView.swift; sourceTree = ""; }; F8EF371429C624DA00669F45 /* Vernissage-006.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-006.xcdatamodel"; sourceTree = ""; }; F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-012.xcdatamodel"; sourceTree = ""; }; @@ -535,6 +541,8 @@ F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */, F871F21C29EF0D7000A351EF /* NavigationMenuItemDetails.swift */, F8624D3C29F2D3AC00204986 /* SelectedMenuItemDetails.swift */, + F8E36E452AB8745300769C55 /* Sizable.swift */, + F8B758DD2AB9DD85000C8068 /* ColumnData.swift */, ); path = Models; sourceTree = ""; @@ -653,6 +661,7 @@ isa = PBXGroup; children = ( F858906A29E1CC7A00D4BDED /* UIApplication+Window.swift */, + F8E36E472AB874A500769C55 /* StatusModel+Sizeable.swift */, ); path = Extensions; sourceTree = ""; @@ -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", diff --git a/Vernissage/Extensions/StatusModel+Sizeable.swift b/Vernissage/Extensions/StatusModel+Sizeable.swift new file mode 100644 index 0000000..013b132 --- /dev/null +++ b/Vernissage/Extensions/StatusModel+Sizeable.swift @@ -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) + } +} diff --git a/Vernissage/Models/ColumnData.swift b/Vernissage/Models/ColumnData.swift new file mode 100644 index 0000000..fd40c4b --- /dev/null +++ b/Vernissage/Models/ColumnData.swift @@ -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: Identifiable where T: Identifiable, T: Hashable, T: Sizable { + public let id = UUID().uuidString + public var data: [T] = [] + public var height: Double = 0.0 +} diff --git a/Vernissage/Models/Sizable.swift b/Vernissage/Models/Sizable.swift new file mode 100644 index 0000000..e084785 --- /dev/null +++ b/Vernissage/Models/Sizable.swift @@ -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 } +} diff --git a/Vernissage/Widgets/WaterfallGrid.swift b/Vernissage/Widgets/WaterfallGrid.swift index 9d622ca..5cb732d 100644 --- a/Vernissage/Widgets/WaterfallGrid.swift +++ b/Vernissage/Widgets/WaterfallGrid.swift @@ -8,30 +8,35 @@ import SwiftUI import WidgetsKit struct WaterfallGrid: 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 + private let content: (Data.Element) -> Content - @State private var columnsData: [[Data.Element]] = [] + @State private var columnsData: [ColumnData] = [] + @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: 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: 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, id: KeyPath, columns: Binding, hideLoadMore: Binding, 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, columns: Binding, hideLoadMore: Binding, 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 } }