From 2fc1cda2c4a4d05a88fa3fbc7e3af8ae9935bb87 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 5 Apr 2025 15:59:48 -0400 Subject: [PATCH 1/6] Initial commit: Sharing sizes across measurements --- ListableUI/Sources/Internal/Cache.swift | 29 ++++++++ ListableUI/Sources/Internal/Metatype.swift | 35 ++++++++++ .../PresentationState.HeaderFooterState.swift | 12 ++-- .../PresentationState.ItemState.swift | 10 +-- ListableUI/Sources/Item/ItemContent.swift | 12 ++++ ListableUI/Sources/SizingCacheKey.swift | 67 +++++++++++++++++++ Podfile.lock | 12 ++-- 7 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 ListableUI/Sources/Internal/Cache.swift create mode 100644 ListableUI/Sources/Internal/Metatype.swift create mode 100644 ListableUI/Sources/SizingCacheKey.swift diff --git a/ListableUI/Sources/Internal/Cache.swift b/ListableUI/Sources/Internal/Cache.swift new file mode 100644 index 000000000..df77a672b --- /dev/null +++ b/ListableUI/Sources/Internal/Cache.swift @@ -0,0 +1,29 @@ +// +// Cache.swift +// ListableUI +// +// Created by Kyle Van Essen on 4/5/25. +// + +import Foundation + + +final class Cache { + + private var values : [Key:Value] = [:] + + func clear() { + values.removeAll() + } + + func get(_ key:Key, create : () -> Value) -> Value { + + if let value = values[key] { + return value + } else { + let value = create() + values[key] = value + return value + } + } +} diff --git a/ListableUI/Sources/Internal/Metatype.swift b/ListableUI/Sources/Internal/Metatype.swift new file mode 100644 index 000000000..8e4e54563 --- /dev/null +++ b/ListableUI/Sources/Internal/Metatype.swift @@ -0,0 +1,35 @@ +// +// Metatype.swift +// ListableUI +// +// Created by Kyle Van Essen on 4/5/25. +// + +import Foundation + + +/// A wrapper to make metatypes easier to work with, providing `Equatable`, `Hashable`, and `CustomStringConvertible`. +/// +/// This is copied from Blueprint: +/// https://github.com/square/Blueprint/blob/main/BlueprintUI/Sources/Internal/Metatype.swift +/// +struct Metatype: Hashable, CustomStringConvertible { + + var type: Any.Type + + init(_ type: Any.Type) { + self.type = type + } + + var description: String { + "\(type)" + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(type)) + } + + static func == (lhs: Metatype, rhs: Metatype) -> Bool { + lhs.type == rhs.type + } +} diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift index f2e904610..7f5d15f4b 100644 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift @@ -232,11 +232,11 @@ extension PresentationState } } - private var cachedSizes : [SizeKey:CGSize] = [:] + private var cachedSizes : Cache = .init() func resetCachedSizes() { - self.cachedSizes.removeAll() + self.cachedSizes.clear() } func size( @@ -256,9 +256,7 @@ extension PresentationState sizing: self.model.sizing ) - if let size = self.cachedSizes[key] { - return size - } else { + return self.cachedSizes.get(key) { SignpostLogger.log(.begin, log: .updateContent, name: "Measure HeaderFooter", for: self.model) let size : CGSize = cache.use( @@ -276,9 +274,7 @@ extension PresentationState return self.model.sizing.measure(with: view, info: info) }) - - self.cachedSizes[key] = size - + SignpostLogger.log(.end, log: .updateContent, name: "Measure HeaderFooter", for: self.model) return size diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift index 891687e75..979fd2452 100644 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift @@ -501,11 +501,11 @@ extension PresentationState } } - private var cachedSizes : [SizeKey:CGSize] = [:] + private var cachedSizes : Cache = .init() func resetCachedSizes() { - self.cachedSizes.removeAll() + self.cachedSizes.clear() } func size( @@ -525,9 +525,7 @@ extension PresentationState sizing: self.model.sizing ) - if let size = self.cachedSizes[key] { - return size - } else { + return self.cachedSizes.get(key) { SignpostLogger.log(.begin, log: .updateContent, name: "Measure ItemContent", for: self.model) let size : CGSize = cache.use( @@ -547,8 +545,6 @@ extension PresentationState return self.model.sizing.measure(with: cell, info: info) }) - self.cachedSizes[key] = size - SignpostLogger.log(.end, log: .updateContent, name: "Measure ItemContent", for: self.model) return size diff --git a/ListableUI/Sources/Item/ItemContent.swift b/ListableUI/Sources/Item/ItemContent.swift index 93b8937b9..7f74a599c 100644 --- a/ListableUI/Sources/Item/ItemContent.swift +++ b/ListableUI/Sources/Item/ItemContent.swift @@ -314,6 +314,10 @@ public protocol ItemContent : AnyItemConvertible where Coordinator.ItemContentTy /// func wasMoved(comparedTo other : Self) -> Bool + associatedtype ContentSizingSharingKey : SizingSharingKey = DefaultSizingSharingKey + + var sizingSharingKey : ContentSizingSharingKey { get } + // // MARK: Default Item Properties // @@ -658,6 +662,14 @@ public extension ItemContent } +public extension ItemContent where ContentSizingSharingKey == DefaultSizingSharingKey +{ + var sizingSharingKey : ContentSizingSharingKey { + DefaultSizingSharingKey() + } +} + + /// Provides a default coordinator for items without a specified coordinator. public extension ItemContent where Coordinator == DefaultItemContentCoordinator { diff --git a/ListableUI/Sources/SizingCacheKey.swift b/ListableUI/Sources/SizingCacheKey.swift new file mode 100644 index 000000000..adebf5a83 --- /dev/null +++ b/ListableUI/Sources/SizingCacheKey.swift @@ -0,0 +1,67 @@ +// +// SizingCacheKey.swift +// ListableUI +// +// Created by Kyle Van Essen on 4/5/25. +// + +import Foundation + + +public protocol SizingSharingKey: Hashable { + + +} + + +public struct DefaultSizingSharingKey : SizingSharingKey {} + + + +final class SizingSharingCache { + + typealias SizeKey = PresentationState.SizeKey + + private var cache: Cache> = .init() + + func size( + sharing sharingKey: Key, + key: SizeKey, + measure: () -> CGSize + ) -> CGSize { + + if Key.self is DefaultSizingSharingKey.Type { + return measure() + } + + let anySharingKey = sharingKey.asAny + + + } +} + + +extension SizingSharingKey { + + var asAny : AnySizingSharingKey { + .init( + metatype: Metatype(Self.self), + key: self as AnyHashable + ) + } +} + + +struct AnySizingSharingKey : Hashable { + + var metatype : Metatype + var key : AnyHashable + + fileprivate init( + metatype : Metatype, + key: AnyHashable + ) { + self.metatype = metatype + self.key = key + } +} diff --git a/Podfile.lock b/Podfile.lock index 8996e2d37..fe93e886f 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,16 +2,16 @@ PODS: - BlueprintUI (5.0.0) - BlueprintUICommonControls (5.0.0): - BlueprintUI (= 5.0.0) - - BlueprintUILists (15.0.1): + - BlueprintUILists (16.0.0): - BlueprintUI (~> 5.0) - ListableUI - - BlueprintUILists/Tests (15.0.1): + - BlueprintUILists/Tests (16.0.0): - BlueprintUI (~> 5.0) - BlueprintUICommonControls (~> 5.0) - ListableUI - EnglishDictionary (1.0.0.LOCAL) - - ListableUI (15.0.1) - - ListableUI/Tests (15.0.1): + - ListableUI (16.0.0) + - ListableUI/Tests (16.0.0): - EnglishDictionary - Snapshot - Snapshot (1.0.0.LOCAL) @@ -46,9 +46,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: BlueprintUI: 0e2d2944bca6c0d6d96df711a43bce01154bb7ef BlueprintUICommonControls: 8f400ee3ecf2bc58bd21cce29caba9f7c83d22b8 - BlueprintUILists: 7e0c46a381e146c6c91951fc96146986cf25b860 + BlueprintUILists: dcc67c0991910216d7f45eec434422e478095e4f EnglishDictionary: 2cf40d33cc1b68c4152a1cc69561aaf6e4ba0209 - ListableUI: 126109e23ff38093884ddffd22dd68cd36dd2cc9 + ListableUI: 3dc22acdc7b6ff61e70e0e1a23a6dea14ab4fd5b Snapshot: 574e65b08c02491a541efbd2619c92cc26514d1c PODFILE CHECKSUM: 2b979d4f2436d28af7c87b125b646836119b89b7 From 882a517501f30a17067013dbe5911092179e0908 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 5 Apr 2025 20:24:42 -0400 Subject: [PATCH 2/6] Thar she builds --- ListableUI/Sources/CacheClearer.swift | 1 + .../HeaderFooter/HeaderFooterContent.swift | 16 ++++ .../PresentationState.HeaderFooterState.swift | 52 +++++++----- .../PresentationState.ItemState.swift | 54 ++++++------ ...PresentationState.SizingSharingCache.swift | 82 +++++++++++++++++++ .../PresentationState/PresentationState.swift | 62 ++++++++++++-- ListableUI/Sources/Item/ItemContent.swift | 10 ++- .../Layout/ListLayout/ListLayout+Layout.swift | 4 +- .../Sources/ListView/SizingSharingCache.swift | 75 +++++++++++++++++ ListableUI/Sources/SizingCacheKey.swift | 52 +----------- 10 files changed, 300 insertions(+), 108 deletions(-) create mode 100644 ListableUI/Sources/Internal/PresentationState/PresentationState.SizingSharingCache.swift create mode 100644 ListableUI/Sources/ListView/SizingSharingCache.swift diff --git a/ListableUI/Sources/CacheClearer.swift b/ListableUI/Sources/CacheClearer.swift index 6f43b2599..a4a98857d 100644 --- a/ListableUI/Sources/CacheClearer.swift +++ b/ListableUI/Sources/CacheClearer.swift @@ -15,5 +15,6 @@ public struct CacheClearer { public static func clearStaticCaches() { ListProperties.headerFooterMeasurementCache.removeAllObjects() ListProperties.itemMeasurementCache.removeAllObjects() + ListProperties.sizingSharingCache.clear() } } diff --git a/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift b/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift index a312d6032..4da0bd364 100644 --- a/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift +++ b/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift @@ -52,6 +52,14 @@ public protocol HeaderFooterContent : AnyHeaderFooterConvertible func isEquivalent(to other : Self) -> Bool + // + // MARK: Size Sharing Across Items + // + + associatedtype ContentSizingSharingKey : SizingSharingKey = NoSizingSharingKey + + var sizingSharingKey : ContentSizingSharingKey { get } + // // MARK: Default Properties // @@ -218,6 +226,14 @@ public extension HeaderFooterContent { } +public extension HeaderFooterContent where ContentSizingSharingKey == NoSizingSharingKey +{ + var sizingSharingKey : ContentSizingSharingKey { + NoSizingSharingKey() + } +} + + public extension HeaderFooterContent where Self:Equatable { /// If your `HeaderFooterContent` is `Equatable`, `isEquivalent` is based on the `Equatable` implementation. diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift index 7f5d15f4b..be9751d1c 100644 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift @@ -47,7 +47,8 @@ protocol AnyPresentationHeaderFooterState : AnyObject func size( for info : Sizing.MeasureInfo, - cache : ReusableViewCache, + viewCache : ReusableViewCache, + sizingSharingCache: PresentationState.SizingSharingCache, environment : ListEnvironment ) -> CGSize } @@ -241,7 +242,8 @@ extension PresentationState func size( for info : Sizing.MeasureInfo, - cache : ReusableViewCache, + viewCache : ReusableViewCache, + sizingSharingCache: PresentationState.SizingSharingCache, environment : ListEnvironment ) -> CGSize { @@ -256,28 +258,34 @@ extension PresentationState sizing: self.model.sizing ) - return self.cachedSizes.get(key) { - SignpostLogger.log(.begin, log: .updateContent, name: "Measure HeaderFooter", for: self.model) - - let size : CGSize = cache.use( - with: self.model.reuseIdentifier, - create: { - return HeaderFooterContentView(frame: .zero) - }, { view in - let views = HeaderFooterContentViews(view: view) + return sizingSharingCache.size( + contentType: Content.self, + sharingKey: self.model.content.sizingSharingKey, + sizingKey: key + ) { + self.cachedSizes.get(key) { + SignpostLogger.log(.begin, log: .updateContent, name: "Measure HeaderFooter", for: self.model) - self.model.content.apply( - to: views, - for: .measurement, - with: .init(environment: environment) - ) + let size : CGSize = viewCache.use( + with: self.model.reuseIdentifier, + create: { + return HeaderFooterContentView(frame: .zero) + }, { view in + let views = HeaderFooterContentViews(view: view) + + self.model.content.apply( + to: views, + for: .measurement, + with: .init(environment: environment) + ) + + return self.model.sizing.measure(with: view, info: info) + }) + + SignpostLogger.log(.end, log: .updateContent, name: "Measure HeaderFooter", for: self.model) - return self.model.sizing.measure(with: view, info: info) - }) - - SignpostLogger.log(.end, log: .updateContent, name: "Measure HeaderFooter", for: self.model) - - return size + return size + } } } } diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift index 979fd2452..6acd0ad51 100644 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift @@ -56,7 +56,8 @@ protocol AnyPresentationItemState : AnyObject func size( for info : Sizing.MeasureInfo, - cache : ReusableViewCache, + viewCache : ReusableViewCache, + sizingSharingCache: PresentationState.SizingSharingCache, environment : ListEnvironment ) -> CGSize @@ -510,7 +511,8 @@ extension PresentationState func size( for info : Sizing.MeasureInfo, - cache : ReusableViewCache, + viewCache : ReusableViewCache, + sizingSharingCache: PresentationState.SizingSharingCache, environment : ListEnvironment ) -> CGSize { @@ -525,29 +527,35 @@ extension PresentationState sizing: self.model.sizing ) - return self.cachedSizes.get(key) { - SignpostLogger.log(.begin, log: .updateContent, name: "Measure ItemContent", for: self.model) - - let size : CGSize = cache.use( - with: self.model.reuseIdentifier, - create: { - return ItemCell() - }, { cell in - let itemState = ListableUI.ItemState(isSelected: false, isHighlighted: false, isReordering: false) + return sizingSharingCache.size( + contentType: Content.self, + sharingKey: self.model.content.sizingSharingKey, + sizingKey: key + ) { + self.cachedSizes.get(key) { + SignpostLogger.log(.begin, log: .updateContent, name: "Measure ItemContent", for: self.model) - self.applyTo( - cell: cell, - itemState: itemState, - reason: .measurement, - environment: environment - ) + let size : CGSize = viewCache.use( + with: self.model.reuseIdentifier, + create: { + return ItemCell() + }, { cell in + let itemState = ListableUI.ItemState(isSelected: false, isHighlighted: false, isReordering: false) + + self.applyTo( + cell: cell, + itemState: itemState, + reason: .measurement, + environment: environment + ) + + return self.model.sizing.measure(with: cell, info: info) + }) - return self.model.sizing.measure(with: cell, info: info) - }) - - SignpostLogger.log(.end, log: .updateContent, name: "Measure ItemContent", for: self.model) - - return size + SignpostLogger.log(.end, log: .updateContent, name: "Measure ItemContent", for: self.model) + + return size + } } } diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.SizingSharingCache.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.SizingSharingCache.swift new file mode 100644 index 000000000..ac6c3803d --- /dev/null +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.SizingSharingCache.swift @@ -0,0 +1,82 @@ +// +// SizingSharingCache.swift +// ListableUI +// +// Created by Kyle Van Essen on 4/5/25. +// + +import Foundation + + +extension PresentationState { + + final class SizingSharingCache { + + typealias SizeKey = PresentationState.SizeKey + + private var cache: Cache>> = .init() + + func clear() { + cache.clear() + } + + func size( + contentType: ContentType.Type, + sharingKey: Key, + sizingKey: SizeKey, + measure: () -> CGSize + ) -> CGSize { + + /// By default, contents return this as their key, meaning we should not cache. + if Key.self is NoSizingSharingKey.Type { + return measure() + } + + let sizeSharingCache = cache.get(.init(contentType)) { + .init() + } + + let sizeCache = sizeSharingCache.get(sharingKey.asAny) { + .init() + } + + return sizeCache.get(sizingKey) { + measure() + } + } + + private struct ContentTypeKey : Hashable { + var metatype : Metatype + + init(_ type : ContentType.Type) { + metatype = .init(type) + } + } + } +} + + +extension SizingSharingKey { + + fileprivate var asAny : AnySizingSharingKey { + .init( + metatype: Metatype(Self.self), + key: self as AnyHashable + ) + } +} + + +fileprivate struct AnySizingSharingKey : Hashable { + + var metatype : Metatype + var key : AnyHashable + + fileprivate init( + metatype : Metatype, + key: AnyHashable + ) { + self.metatype = metatype + self.key = key + } +} diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.swift index 78584edc3..8c63038eb 100644 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.swift +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.swift @@ -42,6 +42,8 @@ final class PresentationState private let itemMeasurementCache : ReusableViewCache private let headerFooterMeasurementCache : ReusableViewCache + private let sizingSharingCache : SizingSharingCache + // // MARK: Initialization // @@ -63,16 +65,20 @@ final class PresentationState self.itemMeasurementCache = ReusableViewCache() self.headerFooterMeasurementCache = ReusableViewCache() + + self.sizingSharingCache = SizingSharingCache() } init( forMeasuringOrTestsWith content : Content, environment : ListEnvironment, itemMeasurementCache : ReusableViewCache, - headerFooterMeasurementCache : ReusableViewCache + headerFooterMeasurementCache : ReusableViewCache, + sizingSharingCache : SizingSharingCache ) { self.itemMeasurementCache = itemMeasurementCache self.headerFooterMeasurementCache = headerFooterMeasurementCache + self.sizingSharingCache = sizingSharingCache self.refreshControl = { if let refreshControl = content.refreshControl { @@ -543,10 +549,16 @@ extension PresentationState kind: .listContainerHeader, isPopulated: true, measurer: { info in - header.size(for: info, cache: self.headerFooterMeasurementCache, environment: environment) + header.size( + for: info, + viewCache: self.headerFooterMeasurementCache, + sizingSharingCache: self.sizingSharingCache, + environment: environment + ) } ) }(), + header: { guard let header = self.header.state else { return nil } @@ -555,10 +567,16 @@ extension PresentationState kind: .listHeader, isPopulated: true, measurer: { info in - header.size(for: info, cache: self.headerFooterMeasurementCache, environment: environment) + header.size( + for: info, + viewCache: self.headerFooterMeasurementCache, + sizingSharingCache: self.sizingSharingCache, + environment: environment + ) } ) }(), + footer: { guard let footer = self.footer.state else { return nil } @@ -567,10 +585,16 @@ extension PresentationState kind: .listFooter, isPopulated: true, measurer: { info in - footer.size(for: info, cache: self.headerFooterMeasurementCache, environment: environment) + footer.size( + for: info, + viewCache: self.headerFooterMeasurementCache, + sizingSharingCache: self.sizingSharingCache, + environment: environment + ) } ) }(), + overscrollFooter: { guard let footer = self.overscrollFooter.state else { return nil } @@ -579,7 +603,12 @@ extension PresentationState kind: .overscrollFooter, isPopulated: true, measurer: { info in - footer.size(for: info, cache: self.headerFooterMeasurementCache, environment: environment) + footer.size( + for: info, + viewCache: self.headerFooterMeasurementCache, + sizingSharingCache: self.sizingSharingCache, + environment: environment + ) } ) }(), @@ -594,10 +623,16 @@ extension PresentationState kind: .sectionHeader, isPopulated: true, measurer: { info in - header.size(for: info, cache: self.headerFooterMeasurementCache, environment: environment) + header.size( + for: info, + viewCache: self.headerFooterMeasurementCache, + sizingSharingCache: self.sizingSharingCache, + environment: environment + ) } ) }(), + footer: { guard let footer = section.footer.state else { return nil } @@ -606,17 +641,28 @@ extension PresentationState kind: .sectionFooter, isPopulated: true, measurer: { info in - footer.size(for: info, cache: self.headerFooterMeasurementCache, environment: environment) + footer.size( + for: info, + viewCache: self.headerFooterMeasurementCache, + sizingSharingCache: self.sizingSharingCache, + environment: environment + ) } ) }(), + items: section.items.mapWithIndex { itemIndex, _, item in .init( state: item, indexPath: IndexPath(item: itemIndex, section: sectionIndex), insertAndRemoveAnimations: item.anyModel.insertAndRemoveAnimations ?? defaults.itemInsertAndRemoveAnimations, measurer: { info in - item.size(for: info, cache: self.itemMeasurementCache, environment: environment) + item.size( + for: info, + viewCache: self.itemMeasurementCache, + sizingSharingCache: self.sizingSharingCache, + environment: environment + ) } ) } diff --git a/ListableUI/Sources/Item/ItemContent.swift b/ListableUI/Sources/Item/ItemContent.swift index 7f74a599c..938f8ffb3 100644 --- a/ListableUI/Sources/Item/ItemContent.swift +++ b/ListableUI/Sources/Item/ItemContent.swift @@ -314,7 +314,11 @@ public protocol ItemContent : AnyItemConvertible where Coordinator.ItemContentTy /// func wasMoved(comparedTo other : Self) -> Bool - associatedtype ContentSizingSharingKey : SizingSharingKey = DefaultSizingSharingKey + // + // MARK: Size Sharing Across Items + // + + associatedtype ContentSizingSharingKey : SizingSharingKey = NoSizingSharingKey var sizingSharingKey : ContentSizingSharingKey { get } @@ -662,10 +666,10 @@ public extension ItemContent } -public extension ItemContent where ContentSizingSharingKey == DefaultSizingSharingKey +public extension ItemContent where ContentSizingSharingKey == NoSizingSharingKey { var sizingSharingKey : ContentSizingSharingKey { - DefaultSizingSharingKey() + NoSizingSharingKey() } } diff --git a/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift b/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift index 6996d9a4e..f63bbc6d7 100644 --- a/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift +++ b/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift @@ -13,6 +13,7 @@ extension ListProperties { static let headerFooterMeasurementCache = ReusableViewCache() static let itemMeasurementCache = ReusableViewCache() + static let sizingSharingCache = PresentationState.SizingSharingCache() /// **Note**: For testing or measuring content sizes only. /// @@ -38,7 +39,8 @@ extension ListProperties { }(), environment: self.environment, itemMeasurementCache: Self.itemMeasurementCache, - headerFooterMeasurementCache: Self.headerFooterMeasurementCache + headerFooterMeasurementCache: Self.headerFooterMeasurementCache, + sizingSharingCache: Self.sizingSharingCache ) /// 2) Create the layout used to measure the content. diff --git a/ListableUI/Sources/ListView/SizingSharingCache.swift b/ListableUI/Sources/ListView/SizingSharingCache.swift new file mode 100644 index 000000000..68f0a27db --- /dev/null +++ b/ListableUI/Sources/ListView/SizingSharingCache.swift @@ -0,0 +1,75 @@ +// +// SizingSharingCache.swift +// ListableUI +// +// Created by Kyle Van Essen on 4/5/25. +// + +import Foundation + + +final class SizingSharingCache { + + typealias SizeKey = PresentationState.SizeKey + + private struct ContentTypeKey : Hashable { + var metatype : Metatype + + init(_ type : ContentType.Type) { + metatype = .init(type) + } + } + + private var cache: Cache>> = .init() + + func size( + contentType: ContentType.Type, + sharingKey: Key, + sizingKey: SizeKey, + measure: () -> CGSize + ) -> CGSize { + + /// By default, contents return this as their key, meaning we should not cache. + if Key.self is NoSizingSharingKey.Type { + return measure() + } + + let sizeSharingCache = cache.get(.init(contentType)) { + .init() + } + + let sizeCache = sizeSharingCache.get(sharingKey.asAny) { + .init() + } + + return sizeCache.get(sizingKey) { + measure() + } + } +} + + +extension SizingSharingKey { + + var asAny : AnySizingSharingKey { + .init( + metatype: Metatype(Self.self), + key: self as AnyHashable + ) + } +} + + +struct AnySizingSharingKey : Hashable { + + var metatype : Metatype + var key : AnyHashable + + fileprivate init( + metatype : Metatype, + key: AnyHashable + ) { + self.metatype = metatype + self.key = key + } +} diff --git a/ListableUI/Sources/SizingCacheKey.swift b/ListableUI/Sources/SizingCacheKey.swift index adebf5a83..e1c7adede 100644 --- a/ListableUI/Sources/SizingCacheKey.swift +++ b/ListableUI/Sources/SizingCacheKey.swift @@ -14,54 +14,4 @@ public protocol SizingSharingKey: Hashable { } -public struct DefaultSizingSharingKey : SizingSharingKey {} - - - -final class SizingSharingCache { - - typealias SizeKey = PresentationState.SizeKey - - private var cache: Cache> = .init() - - func size( - sharing sharingKey: Key, - key: SizeKey, - measure: () -> CGSize - ) -> CGSize { - - if Key.self is DefaultSizingSharingKey.Type { - return measure() - } - - let anySharingKey = sharingKey.asAny - - - } -} - - -extension SizingSharingKey { - - var asAny : AnySizingSharingKey { - .init( - metatype: Metatype(Self.self), - key: self as AnyHashable - ) - } -} - - -struct AnySizingSharingKey : Hashable { - - var metatype : Metatype - var key : AnyHashable - - fileprivate init( - metatype : Metatype, - key: AnyHashable - ) { - self.metatype = metatype - self.key = key - } -} +public struct NoSizingSharingKey : SizingSharingKey {} From a11f2202f73521ed198bf9120e2361a41e57d593 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 5 Apr 2025 20:38:51 -0400 Subject: [PATCH 3/6] more --- .../PresentationState.HeaderFooterState.swift | 4 +- .../PresentationState.ItemState.swift | 4 +- ...PresentationState.SizingSharingCache.swift | 82 ------------------- .../Sources/Internal/SizingSharingCache.swift | 79 ++++++++++++++++++ .../Layout/ListLayout/ListLayout+Layout.swift | 2 +- .../Sources/ListScrollPositionInfo.swift | 2 +- 6 files changed, 85 insertions(+), 88 deletions(-) delete mode 100644 ListableUI/Sources/Internal/PresentationState/PresentationState.SizingSharingCache.swift create mode 100644 ListableUI/Sources/Internal/SizingSharingCache.swift diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift index be9751d1c..f5b202506 100644 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift @@ -48,7 +48,7 @@ protocol AnyPresentationHeaderFooterState : AnyObject func size( for info : Sizing.MeasureInfo, viewCache : ReusableViewCache, - sizingSharingCache: PresentationState.SizingSharingCache, + sizingSharingCache: SizingSharingCache, environment : ListEnvironment ) -> CGSize } @@ -243,7 +243,7 @@ extension PresentationState func size( for info : Sizing.MeasureInfo, viewCache : ReusableViewCache, - sizingSharingCache: PresentationState.SizingSharingCache, + sizingSharingCache: SizingSharingCache, environment : ListEnvironment ) -> CGSize { diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift index 6acd0ad51..a53313e67 100644 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift @@ -57,7 +57,7 @@ protocol AnyPresentationItemState : AnyObject func size( for info : Sizing.MeasureInfo, viewCache : ReusableViewCache, - sizingSharingCache: PresentationState.SizingSharingCache, + sizingSharingCache: SizingSharingCache, environment : ListEnvironment ) -> CGSize @@ -512,7 +512,7 @@ extension PresentationState func size( for info : Sizing.MeasureInfo, viewCache : ReusableViewCache, - sizingSharingCache: PresentationState.SizingSharingCache, + sizingSharingCache: SizingSharingCache, environment : ListEnvironment ) -> CGSize { diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.SizingSharingCache.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.SizingSharingCache.swift deleted file mode 100644 index ac6c3803d..000000000 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.SizingSharingCache.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// SizingSharingCache.swift -// ListableUI -// -// Created by Kyle Van Essen on 4/5/25. -// - -import Foundation - - -extension PresentationState { - - final class SizingSharingCache { - - typealias SizeKey = PresentationState.SizeKey - - private var cache: Cache>> = .init() - - func clear() { - cache.clear() - } - - func size( - contentType: ContentType.Type, - sharingKey: Key, - sizingKey: SizeKey, - measure: () -> CGSize - ) -> CGSize { - - /// By default, contents return this as their key, meaning we should not cache. - if Key.self is NoSizingSharingKey.Type { - return measure() - } - - let sizeSharingCache = cache.get(.init(contentType)) { - .init() - } - - let sizeCache = sizeSharingCache.get(sharingKey.asAny) { - .init() - } - - return sizeCache.get(sizingKey) { - measure() - } - } - - private struct ContentTypeKey : Hashable { - var metatype : Metatype - - init(_ type : ContentType.Type) { - metatype = .init(type) - } - } - } -} - - -extension SizingSharingKey { - - fileprivate var asAny : AnySizingSharingKey { - .init( - metatype: Metatype(Self.self), - key: self as AnyHashable - ) - } -} - - -fileprivate struct AnySizingSharingKey : Hashable { - - var metatype : Metatype - var key : AnyHashable - - fileprivate init( - metatype : Metatype, - key: AnyHashable - ) { - self.metatype = metatype - self.key = key - } -} diff --git a/ListableUI/Sources/Internal/SizingSharingCache.swift b/ListableUI/Sources/Internal/SizingSharingCache.swift new file mode 100644 index 000000000..1c6889ce7 --- /dev/null +++ b/ListableUI/Sources/Internal/SizingSharingCache.swift @@ -0,0 +1,79 @@ +// +// SizingSharingCache.swift +// ListableUI +// +// Created by Kyle Van Essen on 4/5/25. +// + +import Foundation + + +final class SizingSharingCache { + + typealias SizeKey = PresentationState.SizeKey + + private var cache: Cache>> = .init() + + func clear() { + cache.clear() + } + + func size( + contentType: ContentType.Type, + sharingKey: Key, + sizingKey: SizeKey, + measure: () -> CGSize + ) -> CGSize { + + /// By default, contents return this as their key, meaning we should not cache. + if Key.self is NoSizingSharingKey.Type { + return measure() + } + + let sizeSharingCache = cache.get(.init(contentType)) { + .init() + } + + let sizeCache = sizeSharingCache.get(sharingKey.asAny) { + .init() + } + + return sizeCache.get(sizingKey) { + measure() + } + } + + private struct ContentTypeKey : Hashable { + var metatype : Metatype + + init(_ type : ContentType.Type) { + metatype = .init(type) + } + } +} + + +extension SizingSharingKey { + + fileprivate var asAny : AnySizingSharingKey { + .init( + metatype: Metatype(Self.self), + key: self as AnyHashable + ) + } +} + + +fileprivate struct AnySizingSharingKey : Hashable { + + var metatype : Metatype + var key : AnyHashable + + fileprivate init( + metatype : Metatype, + key: AnyHashable + ) { + self.metatype = metatype + self.key = key + } +} diff --git a/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift b/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift index f63bbc6d7..da3f5b6da 100644 --- a/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift +++ b/ListableUI/Sources/Layout/ListLayout/ListLayout+Layout.swift @@ -13,7 +13,7 @@ extension ListProperties { static let headerFooterMeasurementCache = ReusableViewCache() static let itemMeasurementCache = ReusableViewCache() - static let sizingSharingCache = PresentationState.SizingSharingCache() + static let sizingSharingCache = SizingSharingCache() /// **Note**: For testing or measuring content sizes only. /// diff --git a/ListableUI/Sources/ListScrollPositionInfo.swift b/ListableUI/Sources/ListScrollPositionInfo.swift index 0ec599203..031e72e35 100644 --- a/ListableUI/Sources/ListScrollPositionInfo.swift +++ b/ListableUI/Sources/ListScrollPositionInfo.swift @@ -173,7 +173,7 @@ extension UIEdgeInsets } } -extension UIRectEdge : CustomDebugStringConvertible +extension UIRectEdge : @retroactive CustomDebugStringConvertible { static func visibleScrollViewContentEdges( bounds : CGRect, From b3888f1b3e6ec4358290d1aea953cc8d9bc315c0 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sat, 5 Apr 2025 20:47:13 -0400 Subject: [PATCH 4/6] more --- Demo/Demo.xcodeproj/project.pbxproj | 4 +++ ...SizingSharingCacheDemoViewController.swift | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 Demo/Sources/Demos/Demo Screens/SizingSharingCacheDemoViewController.swift diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 0e954f48d..b53fa3ac8 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 0AA4D9C7248064A300CF95A5 /* SearchableDictionaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA4D9B6248064A300CF95A5 /* SearchableDictionaryViewController.swift */; }; 0AA4D9C8248064A300CF95A5 /* ReorderingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA4D9B7248064A300CF95A5 /* ReorderingViewController.swift */; }; 0AA4D9C9248064A300CF95A5 /* CollectionViewBasicDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA4D9B8248064A300CF95A5 /* CollectionViewBasicDemoViewController.swift */; }; + 0ABDBFD32DA205A000F5A5D2 /* SizingSharingCacheDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABDBFD22DA205A000F5A5D2 /* SizingSharingCacheDemoViewController.swift */; }; 0AC2A1962489F93E00779459 /* PagedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC2A1952489F93E00779459 /* PagedViewController.swift */; }; 0AC839A525EEAD110055CEF5 /* OnTapItemAnimationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC839A425EEAD110055CEF5 /* OnTapItemAnimationViewController.swift */; }; 0ACF96D624A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ACF96D524A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift */; }; @@ -90,6 +91,7 @@ 0AA4D9B6248064A300CF95A5 /* SearchableDictionaryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchableDictionaryViewController.swift; sourceTree = ""; }; 0AA4D9B7248064A300CF95A5 /* ReorderingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReorderingViewController.swift; sourceTree = ""; }; 0AA4D9B8248064A300CF95A5 /* CollectionViewBasicDemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewBasicDemoViewController.swift; sourceTree = ""; }; + 0ABDBFD22DA205A000F5A5D2 /* SizingSharingCacheDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizingSharingCacheDemoViewController.swift; sourceTree = ""; }; 0AC2A1952489F93E00779459 /* PagedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedViewController.swift; sourceTree = ""; }; 0AC839A425EEAD110055CEF5 /* OnTapItemAnimationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnTapItemAnimationViewController.swift; sourceTree = ""; }; 0ACF96D524A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInsertAndRemoveAnimationsViewController.swift; sourceTree = ""; }; @@ -167,6 +169,7 @@ 0AA4D9B0248064A300CF95A5 /* CoordinatorViewController.swift */, 0AD827DB2752C8A700D42B4A /* CarouselLayoutViewController.swift */, 0AA4D9A9248064A200CF95A5 /* SystemFlowLayoutViewController.swift */, + 0ABDBFD22DA205A000F5A5D2 /* SizingSharingCacheDemoViewController.swift */, 0AA4D9B1248064A300CF95A5 /* HorizontalLayoutViewController.swift */, 0AA4D9AF248064A300CF95A5 /* InvoicesPaymentScheduleDemoViewController.swift */, 0ACF96D524A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift */, @@ -512,6 +515,7 @@ 0AA4D9BA248064A300CF95A5 /* SystemFlowLayoutViewController.swift in Sources */, 0AC2A1962489F93E00779459 /* PagedViewController.swift in Sources */, 0AA4D9C5248064A300CF95A5 /* AutoScrollingViewController.swift in Sources */, + 0ABDBFD32DA205A000F5A5D2 /* SizingSharingCacheDemoViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Demo/Sources/Demos/Demo Screens/SizingSharingCacheDemoViewController.swift b/Demo/Sources/Demos/Demo Screens/SizingSharingCacheDemoViewController.swift new file mode 100644 index 000000000..735f76a48 --- /dev/null +++ b/Demo/Sources/Demos/Demo Screens/SizingSharingCacheDemoViewController.swift @@ -0,0 +1,31 @@ +// +// SizingSharingCacheDemoViewController.swift +// Demo +// +// Created by Kyle Van Essen on 4/5/25. +// Copyright © 2025 Kyle Van Essen. All rights reserved. +// + +import Foundation +import UIKit +import BlueprintUILists +import ListableUI + + +final class SizingSharingCacheDemoViewController: ListViewController { + + override func configure(list: inout ListProperties) { + + + + } + + fileprivate struct Demoitem: BlueprintItemContent, Equatable { + + var identifierValue: AnyHashable + + func element(with info: ApplyItemContentInfo) -> any Element { + Empty() + } + } +} From d7b7b351e5d22fe737ab686671bf6c41c553a448 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 24 Apr 2025 17:28:56 -0400 Subject: [PATCH 5/6] Update shape to allow passing in a special measured object --- Demo/Demo.xcodeproj/project.pbxproj | 4 --- .../ItemizationEditorViewController.swift | 8 +++++ ...SizingSharingCacheDemoViewController.swift | 31 ------------------- .../HeaderFooter/HeaderFooterContent.swift | 6 ++-- .../PresentationState.HeaderFooterState.swift | 9 ++++-- .../PresentationState.ItemState.swift | 14 +++++---- ListableUI/Sources/Item/ItemContent.swift | 6 ++-- ListableUI/Sources/SizingCacheKey.swift | 28 +++++++++++++++++ 8 files changed, 57 insertions(+), 49 deletions(-) delete mode 100644 Demo/Sources/Demos/Demo Screens/SizingSharingCacheDemoViewController.swift diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index b53fa3ac8..0e954f48d 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -36,7 +36,6 @@ 0AA4D9C7248064A300CF95A5 /* SearchableDictionaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA4D9B6248064A300CF95A5 /* SearchableDictionaryViewController.swift */; }; 0AA4D9C8248064A300CF95A5 /* ReorderingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA4D9B7248064A300CF95A5 /* ReorderingViewController.swift */; }; 0AA4D9C9248064A300CF95A5 /* CollectionViewBasicDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA4D9B8248064A300CF95A5 /* CollectionViewBasicDemoViewController.swift */; }; - 0ABDBFD32DA205A000F5A5D2 /* SizingSharingCacheDemoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABDBFD22DA205A000F5A5D2 /* SizingSharingCacheDemoViewController.swift */; }; 0AC2A1962489F93E00779459 /* PagedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC2A1952489F93E00779459 /* PagedViewController.swift */; }; 0AC839A525EEAD110055CEF5 /* OnTapItemAnimationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AC839A425EEAD110055CEF5 /* OnTapItemAnimationViewController.swift */; }; 0ACF96D624A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ACF96D524A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift */; }; @@ -91,7 +90,6 @@ 0AA4D9B6248064A300CF95A5 /* SearchableDictionaryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchableDictionaryViewController.swift; sourceTree = ""; }; 0AA4D9B7248064A300CF95A5 /* ReorderingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReorderingViewController.swift; sourceTree = ""; }; 0AA4D9B8248064A300CF95A5 /* CollectionViewBasicDemoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewBasicDemoViewController.swift; sourceTree = ""; }; - 0ABDBFD22DA205A000F5A5D2 /* SizingSharingCacheDemoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizingSharingCacheDemoViewController.swift; sourceTree = ""; }; 0AC2A1952489F93E00779459 /* PagedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedViewController.swift; sourceTree = ""; }; 0AC839A425EEAD110055CEF5 /* OnTapItemAnimationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnTapItemAnimationViewController.swift; sourceTree = ""; }; 0ACF96D524A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemInsertAndRemoveAnimationsViewController.swift; sourceTree = ""; }; @@ -169,7 +167,6 @@ 0AA4D9B0248064A300CF95A5 /* CoordinatorViewController.swift */, 0AD827DB2752C8A700D42B4A /* CarouselLayoutViewController.swift */, 0AA4D9A9248064A200CF95A5 /* SystemFlowLayoutViewController.swift */, - 0ABDBFD22DA205A000F5A5D2 /* SizingSharingCacheDemoViewController.swift */, 0AA4D9B1248064A300CF95A5 /* HorizontalLayoutViewController.swift */, 0AA4D9AF248064A300CF95A5 /* InvoicesPaymentScheduleDemoViewController.swift */, 0ACF96D524A0094D0090EAC4 /* ItemInsertAndRemoveAnimationsViewController.swift */, @@ -515,7 +512,6 @@ 0AA4D9BA248064A300CF95A5 /* SystemFlowLayoutViewController.swift in Sources */, 0AC2A1962489F93E00779459 /* PagedViewController.swift in Sources */, 0AA4D9C5248064A300CF95A5 /* AutoScrollingViewController.swift in Sources */, - 0ABDBFD32DA205A000F5A5D2 /* SizingSharingCacheDemoViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Demo/Sources/Demos/Demo Screens/ItemizationEditorViewController.swift b/Demo/Sources/Demos/Demo Screens/ItemizationEditorViewController.swift index feb0f0398..ea58af792 100644 --- a/Demo/Sources/Demos/Demo Screens/ItemizationEditorViewController.swift +++ b/Demo/Sources/Demos/Demo Screens/ItemizationEditorViewController.swift @@ -237,6 +237,14 @@ struct ChoiceItem : BlueprintItemContent, Equatable var identifierValue : String { self.title } + + var sizingSharingKey: DemoSizeSharingKey { + DemoSizeSharingKey(hasSubtitle: self.detail.isEmpty == false) + } + + struct DemoSizeSharingKey : SizingSharingKey { + var hasSubtitle : Bool + } } struct ToggleItem : BlueprintItemContent diff --git a/Demo/Sources/Demos/Demo Screens/SizingSharingCacheDemoViewController.swift b/Demo/Sources/Demos/Demo Screens/SizingSharingCacheDemoViewController.swift deleted file mode 100644 index 735f76a48..000000000 --- a/Demo/Sources/Demos/Demo Screens/SizingSharingCacheDemoViewController.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// SizingSharingCacheDemoViewController.swift -// Demo -// -// Created by Kyle Van Essen on 4/5/25. -// Copyright © 2025 Kyle Van Essen. All rights reserved. -// - -import Foundation -import UIKit -import BlueprintUILists -import ListableUI - - -final class SizingSharingCacheDemoViewController: ListViewController { - - override func configure(list: inout ListProperties) { - - - - } - - fileprivate struct Demoitem: BlueprintItemContent, Equatable { - - var identifierValue: AnyHashable - - func element(with info: ApplyItemContentInfo) -> any Element { - Empty() - } - } -} diff --git a/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift b/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift index 4da0bd364..581d1cd7e 100644 --- a/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift +++ b/ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift @@ -58,7 +58,7 @@ public protocol HeaderFooterContent : AnyHeaderFooterConvertible associatedtype ContentSizingSharingKey : SizingSharingKey = NoSizingSharingKey - var sizingSharingKey : ContentSizingSharingKey { get } + var sizingSharing : SizingSharing { get } // // MARK: Default Properties @@ -228,8 +228,8 @@ public extension HeaderFooterContent { public extension HeaderFooterContent where ContentSizingSharingKey == NoSizingSharingKey { - var sizingSharingKey : ContentSizingSharingKey { - NoSizingSharingKey() + var sizingSharing : SizingSharing { + SizingSharing(sizingSharingKey: NoSizingSharingKey()) } } diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift index f5b202506..d451726da 100644 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.HeaderFooterState.swift @@ -260,7 +260,7 @@ extension PresentationState return sizingSharingCache.size( contentType: Content.self, - sharingKey: self.model.content.sizingSharingKey, + sharingKey: self.model.content.sizingSharing.sizingSharingKey, sizingKey: key ) { self.cachedSizes.get(key) { @@ -273,7 +273,12 @@ extension PresentationState }, { view in let views = HeaderFooterContentViews(view: view) - self.model.content.apply( + let content = switch self.model.content.sizingSharing.source { + case .content: self.model.content + case .provided(let content): content + } + + content.apply( to: views, for: .measurement, with: .init(environment: environment) diff --git a/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift b/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift index a53313e67..6cc95423f 100644 --- a/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift +++ b/ListableUI/Sources/Internal/PresentationState/PresentationState.ItemState.swift @@ -318,14 +318,16 @@ extension PresentationState // Apply Model State - model - .content + let content = switch self.model.content.sizingSharing.source { + case .content: model.content + case .provided(let content): content + } + + content .contentAreaViewProperties(with: applyInfo) .apply(to: cell.contentContainer) - self - .model - .content + content .apply( to: ItemContentViews(cell: cell), for: reason, @@ -529,7 +531,7 @@ extension PresentationState return sizingSharingCache.size( contentType: Content.self, - sharingKey: self.model.content.sizingSharingKey, + sharingKey: self.model.content.sizingSharing.sizingSharingKey, sizingKey: key ) { self.cachedSizes.get(key) { diff --git a/ListableUI/Sources/Item/ItemContent.swift b/ListableUI/Sources/Item/ItemContent.swift index 938f8ffb3..93dbc1290 100644 --- a/ListableUI/Sources/Item/ItemContent.swift +++ b/ListableUI/Sources/Item/ItemContent.swift @@ -320,7 +320,7 @@ public protocol ItemContent : AnyItemConvertible where Coordinator.ItemContentTy associatedtype ContentSizingSharingKey : SizingSharingKey = NoSizingSharingKey - var sizingSharingKey : ContentSizingSharingKey { get } + var sizingSharing : SizingSharing { get } // // MARK: Default Item Properties @@ -668,8 +668,8 @@ public extension ItemContent public extension ItemContent where ContentSizingSharingKey == NoSizingSharingKey { - var sizingSharingKey : ContentSizingSharingKey { - NoSizingSharingKey() + var sizingSharing : SizingSharing { + SizingSharing(sizingSharingKey: NoSizingSharingKey()) } } diff --git a/ListableUI/Sources/SizingCacheKey.swift b/ListableUI/Sources/SizingCacheKey.swift index e1c7adede..1993d5599 100644 --- a/ListableUI/Sources/SizingCacheKey.swift +++ b/ListableUI/Sources/SizingCacheKey.swift @@ -14,4 +14,32 @@ public protocol SizingSharingKey: Hashable { } +public struct SizingSharing { + + public var sizingSharingKey: Key + + public var source : Source + + public init( + sizingSharingKey: Key, + content: () -> Content + ) { + self.sizingSharingKey = sizingSharingKey + self.source = .provided(content()) + } + + public init( + sizingSharingKey: Key + ) { + self.sizingSharingKey = sizingSharingKey + self.source = .content + } + + public enum Source { + case content + case provided(Content) + } +} + + public struct NoSizingSharingKey : SizingSharingKey {} From 00a0d03934ec6808be67ce3909dda6093afb26e8 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Thu, 24 Apr 2025 17:35:14 -0400 Subject: [PATCH 6/6] more --- .../ItemizationEditorViewController.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Demo/Sources/Demos/Demo Screens/ItemizationEditorViewController.swift b/Demo/Sources/Demos/Demo Screens/ItemizationEditorViewController.swift index ea58af792..6e9ee99f5 100644 --- a/Demo/Sources/Demos/Demo Screens/ItemizationEditorViewController.swift +++ b/Demo/Sources/Demos/Demo Screens/ItemizationEditorViewController.swift @@ -238,13 +238,16 @@ struct ChoiceItem : BlueprintItemContent, Equatable self.title } - var sizingSharingKey: DemoSizeSharingKey { - DemoSizeSharingKey(hasSubtitle: self.detail.isEmpty == false) + var sizingSharing: SizingSharing { + SizingSharing(sizingSharingKey: .init()) { + ChoiceItem( + title: "A longer title to ensure we're measuring the maximum size", + detail: "A longer detail to ensure we're measuring the maximum size" + ) + } } - struct DemoSizeSharingKey : SizingSharingKey { - var hasSubtitle : Bool - } + struct DemoSizeSharingKey : SizingSharingKey {} } struct ToggleItem : BlueprintItemContent