mirror of
https://github.com/phranck/TUIkit.git
synced 2026-05-21 09:50:35 +00:00
Refactor: Replace RenderNotifier.current global with dependency injection
- Make RenderNotifier.current optional (nil by default) to support DI - Add RenderNotifierKey and renderNotifier property to EnvironmentValues - Initialize renderNotifier in RenderLoop.buildEnvironment() - Migrate Spinner animation loop to read from environment - Migrate NotificationHostModifier to read from environment via parameter - Update StateBox.didSet to use optional chaining fallback - Update AppStorage.wrappedValue setter to use optional chaining fallback - Update NotificationService.post() to use optional chaining fallback - Update test comments to reflect optional chaining pattern - All 1069 tests pass, no new compiler warnings Phase 3 implementation of P4.16 dependency injection refactor complete.
This commit is contained in:
@@ -351,6 +351,7 @@ extension RenderLoop {
|
||||
environment.keyEventDispatcher = tuiContext.keyEventDispatcher
|
||||
environment.renderCache = tuiContext.renderCache
|
||||
environment.preferenceStorage = tuiContext.preferences
|
||||
environment.renderNotifier = RenderNotifier.current
|
||||
|
||||
return environment
|
||||
}
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
// Created by LAYERED.work
|
||||
// License: MIT
|
||||
|
||||
// MARK: - Render Notifier (AppState)
|
||||
|
||||
/// EnvironmentKey for the AppState reference used to trigger re-renders.
|
||||
/// Render-time consumers (Spinner, NotificationHostModifier) read from here.
|
||||
private struct RenderNotifierKey: EnvironmentKey {
|
||||
static let defaultValue: AppState? = nil
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle Manager
|
||||
|
||||
/// EnvironmentKey for view lifecycle tracking (appear/disappear/task).
|
||||
@@ -57,6 +65,13 @@ private struct ActiveFocusSectionKey: EnvironmentKey {
|
||||
|
||||
extension EnvironmentValues {
|
||||
|
||||
/// The AppState reference for triggering re-renders.
|
||||
/// Used by render-time consumers (Spinner, NotificationHostModifier) to signal render needs.
|
||||
var renderNotifier: AppState? {
|
||||
get { self[RenderNotifierKey.self] }
|
||||
set { self[RenderNotifierKey.self] = newValue }
|
||||
}
|
||||
|
||||
/// View lifecycle tracking (appear, disappear, task management).
|
||||
var lifecycle: LifecycleManager? {
|
||||
get { self[LifecycleKey.self] }
|
||||
|
||||
@@ -54,7 +54,8 @@ extension NotificationHostModifier: Renderable {
|
||||
// Start the animation timer if not already running.
|
||||
startAnimationTask(
|
||||
entries: activeEntries,
|
||||
lifecycle: context.environment.lifecycle!
|
||||
lifecycle: context.environment.lifecycle!,
|
||||
renderNotifier: context.environment.renderNotifier
|
||||
)
|
||||
|
||||
let now = Date().timeIntervalSinceReferenceDate
|
||||
@@ -149,7 +150,8 @@ private extension NotificationHostModifier {
|
||||
/// The task stops automatically when no notifications are active.
|
||||
func startAnimationTask(
|
||||
entries: [NotificationEntry],
|
||||
lifecycle: LifecycleManager
|
||||
lifecycle: LifecycleManager,
|
||||
renderNotifier: AppState?
|
||||
) {
|
||||
let token = "notification-host-animation"
|
||||
|
||||
@@ -171,12 +173,12 @@ private extension NotificationHostModifier {
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: triggerNanos)
|
||||
guard !Task.isCancelled else { break }
|
||||
RenderNotifier.current.setNeedsRender()
|
||||
renderNotifier?.setNeedsRender()
|
||||
}
|
||||
|
||||
// Final render to clear expired notifications.
|
||||
lifecycle.resetAppearance(token: token)
|
||||
RenderNotifier.current.setNeedsRender()
|
||||
renderNotifier?.setNeedsRender()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,8 @@ extension NotificationService {
|
||||
lock.lock()
|
||||
entries.append(entry)
|
||||
lock.unlock()
|
||||
RenderNotifier.current.setNeedsRender()
|
||||
// Property wrapper setters lack render context, so fall back to global
|
||||
RenderNotifier.current?.setNeedsRender()
|
||||
}
|
||||
|
||||
/// Returns a snapshot of all currently active notifications.
|
||||
|
||||
@@ -281,7 +281,8 @@ public struct AppStorage<Value: Codable>: @unchecked Sendable {
|
||||
}
|
||||
nonmutating set {
|
||||
storage.setValue(newValue, forKey: key)
|
||||
RenderNotifier.current.setNeedsRender()
|
||||
// Property wrapper setters lack render context, so fall back to global
|
||||
RenderNotifier.current?.setNeedsRender()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -283,11 +283,12 @@ private struct _SpinnerCore: View, Renderable {
|
||||
_ = lifecycle.recordAppear(token: token) {}
|
||||
|
||||
let triggerNanos: UInt64 = 28_000_000 // 28ms — matches run loop poll rate (~35 FPS)
|
||||
let renderNotifier = context.environment.renderNotifier
|
||||
lifecycle.startTask(token: token, priority: .medium) {
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(nanoseconds: triggerNanos)
|
||||
guard !Task.isCancelled else { break }
|
||||
RenderNotifier.current.setNeedsRender()
|
||||
renderNotifier?.setNeedsRender()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -141,7 +141,9 @@ public enum RenderNotifier {
|
||||
/// No concurrent access is possible: they are initialized once and then
|
||||
/// read/written only during rendering.
|
||||
///
|
||||
public nonisolated(unsafe) static var current = AppState()
|
||||
/// Optional fallback for property wrapper setters that lack render context.
|
||||
/// Render-time consumers should read from EnvironmentValues.renderNotifier instead.
|
||||
public nonisolated(unsafe) static var current: AppState?
|
||||
|
||||
/// The active render cache for subtree memoization.
|
||||
///
|
||||
|
||||
@@ -158,7 +158,8 @@ public final class StateBox<Value>: @unchecked Sendable {
|
||||
} else {
|
||||
RenderNotifier.renderCache?.clearAll()
|
||||
}
|
||||
RenderNotifier.current.setNeedsRender()
|
||||
// Property wrapper setters lack render context, so fall back to global
|
||||
RenderNotifier.current?.setNeedsRender()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ struct StatePropertyWrapperTests {
|
||||
|
||||
@Test("State mutation triggers render via RenderNotifier")
|
||||
func stateTriggerRender() {
|
||||
// StateBox.didSet calls RenderNotifier.current.setNeedsRender().
|
||||
// StateBox.didSet calls RenderNotifier.current?.setNeedsRender() (optional chaining).
|
||||
// We swap in a fresh AppState, mutate, and check immediately.
|
||||
// This is a single-expression sequence with no yield points,
|
||||
// so no parallel test can interfere between set and check.
|
||||
|
||||
Reference in New Issue
Block a user