Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3abebaf
CacheStorage
maxg-square Jul 16, 2025
6429e56
Cache string normalization
maxg-square Jul 16, 2025
76aee10
Fix key name
maxg-square Jul 16, 2025
44aa682
Fix testkey
maxg-square Jul 16, 2025
10d11e9
Merge branch 'maxg/cache_1_equiv' into maxg/cache_2_envcache
maxg-square Jul 16, 2025
dad612b
Merge branch 'maxg/cache_2_envcache' into cache_3c_stringnorm
maxg-square Jul 16, 2025
7c6b2dc
Merge branch 'maxg/cache_1_equivalency' into maxg/cache_2_envcache
maxg-square Jul 16, 2025
5ef770e
Merge branch 'maxg/cache_2_envcache' into maxg/cache_3c_stringnorm
maxg-square Jul 16, 2025
6dafc50
Fix HSC tests
maxg-square Jul 17, 2025
42104f9
Merge branch 'maxg/cache_1_equivalency' into maxg/cache_2_envcache
maxg-square Jul 25, 2025
398dc40
Merge.
maxg-square Jul 25, 2025
4907f48
Merge branch 'maxg/cache_2_envcache' into maxg/cache_3c_stringnorm
maxg-square Jul 25, 2025
6ce656e
Merge
maxg-square Jul 25, 2025
942c1fa
Tests and fixes
maxg-square Jul 25, 2025
6519418
More tests
maxg-square Jul 25, 2025
3d2ceae
Merge branch 'maxg/cache_2_envcache' into maxg/cache_3c_stringnorm
maxg-square Jul 25, 2025
afbbc3c
More tests
maxg-square Jul 25, 2025
f4c5c2e
Tweak env tests
maxg-square Jul 28, 2025
5e43911
Merge branch 'maxg/cache_2_envcache' into maxg/cache_3c_stringnorm
maxg-square Jul 28, 2025
9c0da9f
Existentials for Xcode 15
maxg-square Jul 28, 2025
1842de6
Merge branch 'main' into maxg/cache_2_envcache
maxg-square Jul 28, 2025
149a666
Merge branch 'maxg/cache_2_envcache' into maxg/cache_3c_stringnorm
maxg-square Jul 28, 2025
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
7 changes: 7 additions & 0 deletions BlueprintUI/Sources/BlueprintView/BlueprintView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public final class BlueprintView: UIView {

private var sizesThatFit: [SizeConstraint: CGSize] = [:]

private var cacheStorage = Environment.CacheStorageEnvironmentKey.defaultValue

/// A base environment used when laying out and rendering the element tree.
///
/// Some keys will be overridden with the traits from the view itself. Eg, `windowSize`, `safeAreaInsets`, etc.
Expand Down Expand Up @@ -148,6 +150,7 @@ public final class BlueprintView: UIView {

self.element = element
self.environment = environment
self.environment.cacheStorage = cacheStorage

rootController = NativeViewController(
node: NativeViewNode(
Expand Down Expand Up @@ -542,9 +545,13 @@ public final class BlueprintView: UIView {
environment.layoutMode = layoutMode
}

environment.cacheStorage = cacheStorage

return environment
}



private func handleAppeared() {
rootController.traverse { node in
node.onAppear?()
Expand Down
27 changes: 27 additions & 0 deletions BlueprintUI/Sources/Environment/Cache/CacheKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation

/// Types conforming to this protocol can be used as keys in `CacheStorage`.
///
/// Using a type as the key allows us to strongly type each value, with the
/// key's `CacheKey.Value` associated value.
///
/// ## Example
///
/// Usually a key is implemented with an uninhabited type, such an empty enum.
///
/// enum WidgetCountsKey: CacheKey {
/// static let emptyValue: [WidgetID: Int] = [:]
/// }
///
/// You can write a small extension on `CacheStorage` to make it easier to use your key.
///
/// extension CacheStorage {
/// var widgetCounts: [WidgetID: Int] {
/// get { self[WidgetCountsKey.self] }
/// set { self[WidgetCountsKey.self] = newValue }
/// }
/// }
public protocol CacheKey {
associatedtype Value
static var emptyValue: Self.Value { get }
}
89 changes: 89 additions & 0 deletions BlueprintUI/Sources/Environment/Cache/CacheStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Foundation
#if canImport(UIKit)
import UIKit
#endif

/// Environment-associated storage used to cache types used across layout passes (eg, size calculations).
/// The storage itself is type-agnostic, requiring only that its keys and values conform to the `CacheKey` protocol
/// Caches are responsible for managing their own lifetimes and eviction strategies.
@_spi(CacheStorage) public final class CacheStorage: Sendable, CustomDebugStringConvertible {

// Optional name to distinguish between instances for debugging purposes.
public var name: String? = nil
fileprivate var storage: [ObjectIdentifier: Any] = [:]

init() {
#if canImport(UIKit)
NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.storage.removeAll()
}
#endif
}

public subscript<KeyType>(key: KeyType.Type) -> KeyType.Value where KeyType: CacheKey {
get {
storage[ObjectIdentifier(key), default: KeyType.emptyValue] as! KeyType.Value
}
set {
storage[ObjectIdentifier(key)] = newValue
}
}

public var debugDescription: String {
let debugName = if let name {
"CacheStorage (\(name))"
} else {
"CacheStorage"
}
return "\(debugName): \(storage.count) entries"
}

}

extension Environment {

struct CacheStorageEnvironmentKey: InternalEnvironmentKey {
static var defaultValue = CacheStorage()
}


@_spi(CacheStorage) public var cacheStorage: CacheStorage {
get { self[internal: CacheStorageEnvironmentKey.self] }
set { self[internal: CacheStorageEnvironmentKey.self] = newValue }
}

}

/// A UUID that changes based on value changes of the containing type.
/// Two fingerprinted objects may be quickly compared for equality by comparing their fingerprints.
/// This is roughly analagous to a hash, although with inverted properties: Two objects with the same fingerprint can be trivially considered equal, but two otherwise equal objects may have different fingerprint.
/// - Note: This type is deliberately NOT equatable – this is to prevent accidental inclusion of it when its containing type is equatable.
struct ComparableFingerprint: ContextuallyEquivalent, CustomStringConvertible {

typealias Value = UUID

var value: Value

init() {
value = Value()
}

mutating func modified() {
value = Value()
}

/// - Note: This is a duplicate message but: this type is deliberately NOT equatable – this is to prevent accidental inclusion of it when its containing type is equatable. Use this instead.
func isEquivalent(to other: ComparableFingerprint?, in context: EquivalencyContext) -> Bool {
value == other?.value
}

var description: String {
value.uuidString
}

}

194 changes: 194 additions & 0 deletions BlueprintUI/Sources/Environment/Cache/ValidatingCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import Foundation

/// Validating cache is a cache which, if it has a value for a key, runs a closure to verify that the cache value is still relevant and not state.
/// This is useful for cases when you might otherwise wish to store the validation data as a key, but it does not conform to Hashable, or its hashability properties do not neccessarily affect the validity of the cached data.
@_spi(CacheStorage) public struct ValidatingCache<Key, Value, ValidationData>: Sendable where Key: Hashable {

private var storage: [Key: ValueStorage] = [:]

private struct ValueStorage {
let value: Value
let validationData: ValidationData
}

public init() {}

/// Retrieves the value for a given key, without evaluating any validation conditions.
public subscript(uncheckedKey key: Key) -> Value? {
storage[key]?.value
}

/// Retrieves or creates a value based on a key and validation function.
/// - Parameters:
/// - key: The key to look up.
/// - validate: A function that evaluates whether or not a given result is still valid.
/// - create: Creates a fresh cache entry no valid cached data is available, and stores it.
/// - Returns: Either a cached or newly created value.
public mutating func retrieveOrCreate(
key: Key,
validate: (ValidationData) -> Bool,
create: () -> (Value, ValidationData)
) -> Value {
if let valueStorage = storage[key] {
Logger.logValidatingCacheKeyHit(key: key)
let validationToken = Logger.logValidatingCacheValidationStart(key: key)
if validate(valueStorage.validationData) {
Logger.logValidatingCacheHitAndValidationSuccess(key: key)
Logger.logValidatingCacheValidationEnd(validationToken, key: key)
return valueStorage.value
#if DEBUG
// FIXME: WAY TO MAKE SURE THIS DOESN'T SHIP ON.
// Enable this to always evaluate the create block to assert that the caching is producing the expected value.
// if let stored = valueStorage.value as? (any Equatable) {
// let fresh = create().0 as! Equatable
// assert(stored.isEqual(fresh))
// }
// return valueStorage.value
#endif
} else {
Logger.logValidatingCacheHitAndValidationFailure(key: key)
Logger.logValidatingCacheValidationEnd(validationToken, key: key)
}
} else {
Logger.logValidatingCacheKeyMiss(key: key)
}
let createToken = Logger.logValidatingCacheFreshValueCreationStart(key: key)
let (fresh, validationData) = create()
Logger.logValidatingCacheFreshValueCreationEnd(createToken, key: key)
storage[key] = ValueStorage(value: fresh, validationData: validationData)
return fresh
}

public mutating func removeValue(forKey key: Key) -> Value? {
storage.removeValue(forKey: key)?.value
}

}

/// A convenience wrapper around ValidatingCache which ensures that only values which were cached in equivalent environments are returned.
@_spi(CacheStorage) public struct EnvironmentValidatingCache<Key, Value>: Sendable where Key: Hashable {

private var backing = ValidatingCache<Key, Value, FrozenEnvironment>()

public init() {}

/// Retrieves or creates a value based on a key and environment validation.
/// - Parameters:
/// - key: The key to look up.
/// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison.
/// - context: The equivalency context in which the environment should be evaluated.
/// - create: Creates a fresh cache entry no valid cached data is available, and stores it.
/// - Returns: Either a cached or newly created value.
mutating func retrieveOrCreate(
key: Key,
environment: Environment,
context: EquivalencyContext,
create: () -> Value
) -> Value {
backing.retrieveOrCreate(key: key) {
environment.isEquivalent(to: $0, in: context)
} create: {
(create(), environment.frozen)
}
}

}

/// A convenience wrapper around ValidatingCache which ensures that only values which were cached in equivalent environments are returned, and allows for additional data to be stored to be validated.
@_spi(CacheStorage) public struct EnvironmentAndValueValidatingCache<Key, Value, AdditionalValidationData>: Sendable where Key: Hashable {

private var backing = ValidatingCache<Key, Value, (FrozenEnvironment, AdditionalValidationData)>()

public init() {}

/// Retrieves or creates a value based on a key and a validation function, alongside environment validation.
/// - Parameters:
/// - key: The key to look up.
/// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison.
/// - context: The equivalency context in which the environment should be evaluated.
/// - validate: A function that evaluates whether or not a given result is still valid.
/// - create: Creates a fresh cache entry no valid cached data is available, and stores it.
/// - Returns: Either a cached or newly created value.
/// - Note: Generally, prefer the `validationValue` versions of this method if the validation value conforms to ContextuallyEquivalent or Equatable.
mutating func retrieveOrCreate(
key: Key,
environment: Environment,
context: EquivalencyContext,
validate: (AdditionalValidationData) -> Bool,
create: () -> (Value, AdditionalValidationData)
) -> Value {
backing.retrieveOrCreate(key: key) {
environment.isEquivalent(to: $0.0, in: context) && validate($0.1)
} create: {
let (fresh, additional) = create()
return (fresh, (environment.frozen, additional))
}
}

}


@_spi(CacheStorage) extension EnvironmentAndValueValidatingCache where AdditionalValidationData: ContextuallyEquivalent {

/// Retrieves or creates a value based on a key and a validation value, alongside environment validation.
/// - Parameters:
/// - key: The key to look up.
/// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison.
/// - context: The equivalency context in which the environment and validation values should be evaluated.
/// - validationValue: A value that will be compared using contextual equivalence that evaluates whether or not a given result is still valid.
/// - create: Creates a fresh cache entry no valid cached data is available, and stores it.
/// - Returns: Either a cached or newly created value.
public mutating func retrieveOrCreate(
key: Key,
environment: Environment,
validationValue: AdditionalValidationData,
context: EquivalencyContext,
create: () -> (Value)
) -> Value {
retrieveOrCreate(key: key, environment: environment, context: context) {
$0.isEquivalent(to: validationValue, in: context)
} create: {
(create(), validationValue)
}

}

}

@_spi(CacheStorage) extension EnvironmentAndValueValidatingCache where AdditionalValidationData: Equatable {

/// Retrieves or creates a value based on a key and a validation value, alongside environment validation.
/// - Parameters:
/// - key: The key to look up.
/// - environment: The current environment. A frozen version of this environment will be preserved along with freshly cached values for comparison.
/// - context: The equivalency context in which the environment should be evaluated.
/// - validationValue: A value that will be compared using strict equality that evaluates whether or not a given result is still valid.
/// - create: Creates a fresh cache entry no valid cached data is available, and stores it.
/// - Returns: Either a cached or newly created value.
@_disfavoredOverload public mutating func retrieveOrCreate(
key: Key,
environment: Environment,
validationValue: AdditionalValidationData,
context: EquivalencyContext,
create: () -> (Value)
) -> Value {
retrieveOrCreate(key: key, environment: environment, context: context) {
$0 == validationValue
} create: {
(create(), validationValue)
}
}

}


extension Equatable {

fileprivate func isEqual(_ other: any Equatable) -> Bool {
guard let other = other as? Self else {
return false
}
return self == other
}

}
Loading
Loading