Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,17 @@ struct ChoiceItem : BlueprintItemContent, Equatable
var identifierValue : String {
self.title
}

var sizingSharing: SizingSharing<DemoSizeSharingKey, ChoiceItem> {
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 {}
}

struct ToggleItem : BlueprintItemContent
Expand Down
1 change: 1 addition & 0 deletions ListableUI/Sources/CacheClearer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ public struct CacheClearer {
public static func clearStaticCaches() {
ListProperties.headerFooterMeasurementCache.removeAllObjects()
ListProperties.itemMeasurementCache.removeAllObjects()
ListProperties.sizingSharingCache.clear()
}
}
16 changes: 16 additions & 0 deletions ListableUI/Sources/HeaderFooter/HeaderFooterContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ public protocol HeaderFooterContent : AnyHeaderFooterConvertible

func isEquivalent(to other : Self) -> Bool

//
// MARK: Size Sharing Across Items
//

associatedtype ContentSizingSharingKey : SizingSharingKey = NoSizingSharingKey

var sizingSharing : SizingSharing<ContentSizingSharingKey, Self> { get }

//
// MARK: Default Properties
//
Expand Down Expand Up @@ -218,6 +226,14 @@ public extension HeaderFooterContent {
}


public extension HeaderFooterContent where ContentSizingSharingKey == NoSizingSharingKey
{
var sizingSharing : SizingSharing<ContentSizingSharingKey, Self> {
SizingSharing(sizingSharingKey: NoSizingSharingKey())
}
}


public extension HeaderFooterContent where Self:Equatable
{
/// If your `HeaderFooterContent` is `Equatable`, `isEquivalent` is based on the `Equatable` implementation.
Expand Down
29 changes: 29 additions & 0 deletions ListableUI/Sources/Internal/Cache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Cache.swift
// ListableUI
//
// Created by Kyle Van Essen on 4/5/25.
//

import Foundation


final class Cache<Key:Hashable, Value> {

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
}
}
}
35 changes: 35 additions & 0 deletions ListableUI/Sources/Internal/Metatype.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ protocol AnyPresentationHeaderFooterState : AnyObject

func size(
for info : Sizing.MeasureInfo,
cache : ReusableViewCache,
viewCache : ReusableViewCache,
sizingSharingCache: SizingSharingCache,
environment : ListEnvironment
) -> CGSize
}
Expand Down Expand Up @@ -232,16 +233,17 @@ extension PresentationState
}
}

private var cachedSizes : [SizeKey:CGSize] = [:]
private var cachedSizes : Cache<SizeKey,CGSize> = .init()

func resetCachedSizes()
{
self.cachedSizes.removeAll()
self.cachedSizes.clear()
}

func size(
for info : Sizing.MeasureInfo,
cache : ReusableViewCache,
viewCache : ReusableViewCache,
sizingSharingCache: SizingSharingCache,
environment : ListEnvironment
) -> CGSize
{
Expand All @@ -256,32 +258,39 @@ extension PresentationState
sizing: self.model.sizing
)

if let size = self.cachedSizes[key] {
return size
} else {
SignpostLogger.log(.begin, log: .updateContent, name: "Measure HeaderFooter", for: self.model)

let size : CGSize = cache.use(
with: self.model.reuseIdentifier,
create: {
return HeaderFooterContentView<Content>(frame: .zero)
}, { view in
let views = HeaderFooterContentViews<Content>(view: view)
return sizingSharingCache.size(
contentType: Content.self,
sharingKey: self.model.content.sizingSharing.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<Content>(frame: .zero)
}, { view in
let views = HeaderFooterContentViews<Content>(view: view)

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

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

self.cachedSizes[key] = size

SignpostLogger.log(.end, log: .updateContent, name: "Measure HeaderFooter", for: self.model)

return size
return size
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ protocol AnyPresentationItemState : AnyObject

func size(
for info : Sizing.MeasureInfo,
cache : ReusableViewCache,
viewCache : ReusableViewCache,
sizingSharingCache: SizingSharingCache,
environment : ListEnvironment
) -> CGSize

Expand Down Expand Up @@ -317,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,
Expand Down Expand Up @@ -501,16 +504,17 @@ extension PresentationState
}
}

private var cachedSizes : [SizeKey:CGSize] = [:]
private var cachedSizes : Cache<SizeKey,CGSize> = .init()

func resetCachedSizes()
{
self.cachedSizes.removeAll()
self.cachedSizes.clear()
}

func size(
for info : Sizing.MeasureInfo,
cache : ReusableViewCache,
viewCache : ReusableViewCache,
sizingSharingCache: SizingSharingCache,
environment : ListEnvironment
) -> CGSize
{
Expand All @@ -525,33 +529,35 @@ extension PresentationState
sizing: self.model.sizing
)

if let size = self.cachedSizes[key] {
return size
} else {
SignpostLogger.log(.begin, log: .updateContent, name: "Measure ItemContent", for: self.model)

let size : CGSize = cache.use(
with: self.model.reuseIdentifier,
create: {
return ItemCell<Content>()
}, { cell in
let itemState = ListableUI.ItemState(isSelected: false, isHighlighted: false, isReordering: false)
return sizingSharingCache.size(
contentType: Content.self,
sharingKey: self.model.content.sizingSharing.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<Content>()
}, { 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)
})

self.cachedSizes[key] = size

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

Expand Down
Loading
Loading