diff --git a/UDF/Common/PropertyWrappers/SourceOfTruth.swift b/UDF/Common/PropertyWrappers/SourceOfTruth.swift index d81115ad..8176964b 100644 --- a/UDF/Common/PropertyWrappers/SourceOfTruth.swift +++ b/UDF/Common/PropertyWrappers/SourceOfTruth.swift @@ -10,6 +10,8 @@ //===----------------------------------------------------------------------===// import Foundation +import os +@preconcurrency import Runtime /// A property wrapper used to represent the central source of truth for the application state. /// It allows for dynamic member lookup to access reducers and bindable containers within the `AppState`. @@ -21,6 +23,10 @@ public final class SourceOfTruth { /// The current value of the application state. public var wrappedValue: AppState + /// A thread-safe lock protecting the cache of resolved property metadata info. + /// Used by `ConnectedContainer` to optimize bindable reducer state lookups. + private let propertyCacheLock = OSAllocatedUnfairLock(initialState: [ObjectIdentifier: PropertyInfo]()) + /// A reference to the store that holds and manages the application state. private weak var store: Optional> @@ -75,3 +81,17 @@ extension SourceOfTruth: Equatable { lhs.wrappedValue == rhs.wrappedValue } } + +extension SourceOfTruth { + /// Returns the cached property metadata for a given container type. + /// + /// This is used by `ConnectedContainer` to optimize state reflection lookups from O(N^2) to O(N) at scale. + func getPropertyMetadata(for containerType: Any.Type) -> PropertyInfo? { + propertyCacheLock.withLock { $0[ObjectIdentifier(containerType)] } + } + + /// Caches the property metadata for a given container type. + func setPropertyMetadata(_ property: PropertyInfo, for containerType: Any.Type) { + propertyCacheLock.withLock { $0[ObjectIdentifier(containerType)] = property } + } +} diff --git a/UDF/Common/RCDictionary.swift b/UDF/Common/RCDictionary.swift index d70fe4ae..7a3821c1 100644 --- a/UDF/Common/RCDictionary.swift +++ b/UDF/Common/RCDictionary.swift @@ -33,6 +33,10 @@ public struct RCDictionary Bool { + keyValues[key]?.referenceCount == 1 + } public subscript(_ key: Key) -> Value? { keyValues[key]?.value @@ -43,7 +47,7 @@ public struct RCDictionary Bool + + /// Returns whether the reducer instance for the given ID is the last active one (refCount == 1) + func isLastInstance(for id: any Hashable) -> Bool +} diff --git a/UDF/Store/Reducer/BindableReducer.swift b/UDF/Store/Reducer/BindableReducer.swift index 93546892..46553eea 100644 --- a/UDF/Store/Reducer/BindableReducer.swift +++ b/UDF/Store/Reducer/BindableReducer.swift @@ -129,3 +129,37 @@ public extension BindableReducer { } } } + + +// MARK: - AnyBindableReducer +extension BindableReducer: AnyBindableReducer { + /// The type of container this reducer is bound to. + var boundContainerType: any Any.Type { + containerType + } + + /// Checks if a reducer is registered for the specified container identifier. + /// + /// - Parameter id: The identifier of the container, expected to be of type `BindedContainer.ID`. + /// - Returns: `true` if a reducer exists for the given identifier; otherwise, `false`. + func hasReducer(for id: any Hashable) -> Bool { + guard let id = id as? BindedContainer.ID else { + return false + } + + return reducers.contains { $0.key == id } + } + + /// Determines whether the reducer for the given identifier is the last active instance. + /// + /// - Parameter id: The identifier of the container, expected to be of type `BindedContainer.ID`. + /// - Returns: `true` if the reducer is uniquely referenced; otherwise, `false`. + func isLastInstance(for id: any Hashable) -> Bool { + guard let containerID = id as? BindedContainer.ID else { + return false + } + + return reducers.isUniquelyReferenced(key: containerID) + } +} + diff --git a/UDF/View/Container/BindableContainer.swift b/UDF/View/Container/BindableContainer.swift index ff95a79a..8b0539fb 100644 --- a/UDF/View/Container/BindableContainer.swift +++ b/UDF/View/Container/BindableContainer.swift @@ -48,7 +48,33 @@ import SwiftUI /// } /// } /// ``` -public protocol BindableContainer: Container, Identifiable where ID: Sendable {} +public protocol BindableContainer: Container, Identifiable where ID: Sendable { + /// A lifecycle callback executed when the dynamic reducer state associated with this container's `id` is loaded and online. + /// + /// This callback is triggered when the dynamic reducer (e.g. form or flow) is successfully allocated and visible in the `EnvironmentStore`. + /// Use this callback to perform state-dependent actions (like loading details or checking validation states) that require the reducer to be active. + /// + /// - Parameter store: The `EnvironmentStore` instance managing the state. + @MainActor + func onBindableContainerStateDidLoad(store: EnvironmentStore) + + /// A lifecycle callback executed when the dynamic reducer state associated with this container's `id` is unloaded and offline. + /// + /// This callback is triggered when the dynamic reducer is deallocated from the store (e.g. when the last container with this ID is unloaded). + /// Use this callback to perform cleanup operations or clear state variables that are no longer needed. + /// + /// - Parameter store: The `EnvironmentStore` instance managing the state. + @MainActor + func onBindableContainerStateDidUnload(store: EnvironmentStore) +} + +public extension BindableContainer { + @MainActor + func onBindableContainerStateDidLoad(store: EnvironmentStore) {} + + @MainActor + func onBindableContainerStateDidUnload(store: EnvironmentStore) {} +} public extension BindableContainer { /// The main view body that connects the container to the state using `ConnectedContainer`. @@ -66,6 +92,8 @@ public extension BindableContainer { onContainerDisappear: onContainerDisappear, onContainerDidLoad: onContainerDidLoad, onContainerDidUnload: onContainerDidUnload, + onBindableContainerStateDidLoad: onBindableContainerStateDidLoad, + onBindableContainerStateDidUnload: onBindableContainerStateDidUnload, useHooks: useHooks ) } diff --git a/UDF/View/Container/ConnectedContainer.swift b/UDF/View/Container/ConnectedContainer.swift index ab8149ad..6e99ce1d 100644 --- a/UDF/View/Container/ConnectedContainer.swift +++ b/UDF/View/Container/ConnectedContainer.swift @@ -11,6 +11,7 @@ import Foundation import SwiftUI +import Runtime /// A SwiftUI view that connects a `Component` with its associated state in the UDF architecture. /// @@ -113,6 +114,8 @@ struct ConnectedContainer: View { /// - onContainerDisappear: A closure executed when the container disappears. /// - onContainerDidLoad: A closure executed when the container is loaded. /// - onContainerDidUnload: A closure executed when the container is unloaded. + /// - onBindableContainerStateDidLoad: A closure executed when the bindable container's state is loaded and verified online. + /// - onBindableContainerStateDidUnload: A closure executed when the bindable container's state is unloaded and verified offline. /// - useHooks: A closure that provides an array of hooks to use within the container. init( store: EnvironmentStore, @@ -124,6 +127,8 @@ struct ConnectedContainer: View { onContainerDisappear: @escaping @MainActor (EnvironmentStore) -> Void, onContainerDidLoad: @escaping (EnvironmentStore) -> Void, onContainerDidUnload: @escaping (EnvironmentStore) -> Void, + onBindableContainerStateDidLoad: @escaping (EnvironmentStore) -> Void, + onBindableContainerStateDidUnload: @escaping (EnvironmentStore) -> Void, useHooks: @escaping () -> [Hook] ) where BindedContainer.ID: Sendable { self.store = store @@ -138,10 +143,22 @@ struct ConnectedContainer: View { Actions._OnContainerDidLoad(containerType: containerType, id: containerId()).silent(), priority: .userInteractive ) + onContainerDidLoad(store) + + let boundReducer = Self.getBoundReducer(with: store, for: containerType) + if boundReducer?.hasReducer(for: containerId()) == false { + onBindableContainerStateDidLoad(store) + } }, didUnloadCommand: { store in onContainerDidUnload(store) + + let boundReducer = Self.getBoundReducer(with: store, for: containerType) + if boundReducer?.isLastInstance(for: containerId()) == true { + onBindableContainerStateDidUnload(store) + } + store.dispatch( Actions._OnContainerDidUnLoad(containerType: containerType, id: containerId()) .with(delay: 0.15) @@ -163,3 +180,38 @@ struct ConnectedContainer: View { .onDisappear { onContainerDisappear(store) } } } + +extension ConnectedContainer { + /// Resolves and returns the bound reducer associated with a container type. + /// + /// Uses cached property metadata inside the `SourceOfTruth` wrapper to avoid repeatedly reflecting over + /// the entire `AppState` properties list. + /// + /// - Parameters: + /// - store: The environment store containing the state and cache. + /// - type: The type of the bindable container. + /// - Returns: The matched bindable reducer existential, or `nil` if not found. + static func getBoundReducer( + with store: EnvironmentStore, + for type: T.Type + ) -> (any AnyBindableReducer)? { + if let property = store.$state.getPropertyMetadata(for: T.self) { + return try? property.get(from: store.state) as? AnyBindableReducer + } + + guard let info = try? typeInfo(of: State.self) else { + return nil + } + + for property in info.properties { + if let bindableReducer = try? property.get(from: store.state) as? AnyBindableReducer { + store.$state.setPropertyMetadata(property, for: bindableReducer.boundContainerType) + if bindableReducer.boundContainerType == T.self { + return bindableReducer + } + } + } + + return nil + } +}