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
20 changes: 20 additions & 0 deletions UDF/Common/PropertyWrappers/SourceOfTruth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -21,6 +23,10 @@ public final class SourceOfTruth<AppState: AppReducer> {
/// 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<any Store<AppState>>

Expand Down Expand Up @@ -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 }
}
}
6 changes: 5 additions & 1 deletion UDF/Common/RCDictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public struct RCDictionary<Key: Hashable & Sendable, Value: Initable & Equatable
box.value = value
keyValues[key] = box
}

func isUniquelyReferenced(key: Key) -> Bool {
keyValues[key]?.referenceCount == 1
}

public subscript(_ key: Key) -> Value? {
keyValues[key]?.value
Expand All @@ -43,7 +47,7 @@ public struct RCDictionary<Key: Hashable & Sendable, Value: Initable & Equatable
public extension RCDictionary {
struct ReducerBox: Equatable {
var value: Value
private var referenceCount: Int = 1
private(set) var referenceCount: Int = 1

init(value: Value) {
self.value = value
Expand Down
21 changes: 21 additions & 0 deletions UDF/Store/Reducer/AnyBindableReducer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//===--- Reducing.swift ------------------------------------------===//
//
// This source file is part of the UDF open source project
//
// Copyright (c) 2026 You are launched
// Licensed under Apache License v2.0
//
// See https://opensource.org/licenses/Apache-2.0 for license information
//
//===----------------------------------------------------------------------===//

protocol AnyBindableReducer: Sendable {
/// The type of the container this reducer is bound to (e.g., UserDetailsContainer.self)
var boundContainerType: any Any.Type { get }

/// Returns whether a reducer instance currently exists in the dictionary for the given ID
func hasReducer(for id: any Hashable) -> Bool

/// Returns whether the reducer instance for the given ID is the last active one (refCount == 1)
func isLastInstance(for id: any Hashable) -> Bool
}
34 changes: 34 additions & 0 deletions UDF/Store/Reducer/BindableReducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

30 changes: 29 additions & 1 deletion UDF/View/Container/BindableContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<ContainerState>)

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

public extension BindableContainer {
@MainActor
func onBindableContainerStateDidLoad(store: EnvironmentStore<ContainerState>) {}

@MainActor
func onBindableContainerStateDidUnload(store: EnvironmentStore<ContainerState>) {}
}

public extension BindableContainer {
/// The main view body that connects the container to the state using `ConnectedContainer`.
Expand All @@ -66,6 +92,8 @@ public extension BindableContainer {
onContainerDisappear: onContainerDisappear,
onContainerDidLoad: onContainerDidLoad,
onContainerDidUnload: onContainerDidUnload,
onBindableContainerStateDidLoad: onBindableContainerStateDidLoad,
onBindableContainerStateDidUnload: onBindableContainerStateDidUnload,
useHooks: useHooks
)
}
Expand Down
52 changes: 52 additions & 0 deletions UDF/View/Container/ConnectedContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -113,6 +114,8 @@ struct ConnectedContainer<C: Component, State: AppReducer>: 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<BindedContainer: BindableContainer>(
store: EnvironmentStore<State>,
Expand All @@ -124,6 +127,8 @@ struct ConnectedContainer<C: Component, State: AppReducer>: View {
onContainerDisappear: @escaping @MainActor (EnvironmentStore<State>) -> Void,
onContainerDidLoad: @escaping (EnvironmentStore<State>) -> Void,
onContainerDidUnload: @escaping (EnvironmentStore<State>) -> Void,
onBindableContainerStateDidLoad: @escaping (EnvironmentStore<State>) -> Void,
onBindableContainerStateDidUnload: @escaping (EnvironmentStore<State>) -> Void,
useHooks: @escaping () -> [Hook<State>]
) where BindedContainer.ID: Sendable {
self.store = store
Expand All @@ -138,10 +143,22 @@ struct ConnectedContainer<C: Component, State: AppReducer>: 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)
Expand All @@ -163,3 +180,38 @@ struct ConnectedContainer<C: Component, State: AppReducer>: 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<T: BindableContainer>(
with store: EnvironmentStore<State>,
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
}
}
Loading