Luminate/Sources/LuminateCore/Observation.swift

120 lines
4 KiB
Swift

//
// Observation.swift
//
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
import Foundation
// MARK: - ObservationRegistrar
/// Manages observation notifications for `@Observable` classes.
///
/// When a tracked property changes, ``didChange()`` triggers the global
/// ``onChange`` handler, which should be wired to the view update system
/// during app initialization.
///
/// ## Wiring
/// Connect the registrar to the view update system during app startup:
/// ```swift
/// ObservationRegistrar.onChange = { StateManager.updateViews() }
/// ```
public class ObservationRegistrar {
/// Global callback invoked when any tracked property changes.
/// Set this during app startup to connect to the view update system.
public static var onChange: (() -> Void)?
public init() {}
/// Notifies the system that a tracked property changed.
/// Triggers the global ``onChange`` handler.
public func didChange() {
Self.onChange?()
}
}
// MARK: - ObservableProtocol
/// Conforming types get automatic property-mutation notifications via ``Observable``.
///
/// All `@Observable` classes automatically conform to this protocol.
/// The ``_$observationRegistrar`` property is added by the `@Observable` macro.
public protocol ObservableProtocol: AnyObject {
/// The observation registrar that tracks property mutations.
var _$observationRegistrar: ObservationRegistrar { get }
}
// MARK: - @Observable Macro Declaration
/// Registers a class for observable property mutation tracking.
///
/// When a property of an observable class is mutated, the framework
/// automatically triggers a view update. Use with ``@State`` in views:
///
/// ```swift
/// @Observable
/// class PlayerViewModel {
/// var title: String = ""
/// var position: TimeInterval = 0
/// let id: UUID = .init()
/// }
///
/// struct PlayerView: View {
/// @State private var model = PlayerViewModel()
/// var view: Body {
/// VStack {
/// Text(model.title)
/// Button("Seek") { model.position += 10 }
/// }
/// }
/// }
/// ```
///
/// The macro:
/// - Adds an ``_$observationRegistrar`` stored property to the class
/// - Adds a stored backing property ``_$<name>`` for each tracked `var`, preserving
/// its type and initial value
/// - Converts each stored `var` property to a computed `get`/`set` pair that
/// reads/writes the ``_$<name>`` backing property and calls
/// ``ObservationRegistrar/didChange()`` on every mutation
///
/// Stored `let` properties and computed properties are left untouched.
///
/// - Important: The registrar must be wired up before use:
/// ```swift
/// ObservationRegistrar.onChange = { StateManager.updateViews() }
/// ```
@attached(member, names: named(_$observationRegistrar), arbitrary)
@attached(memberAttribute)
public macro Observable() =
#externalMacro(
module: "LuminateObservationMacros",
type: "ObservableMacro"
)
/// Internal macro used by ``Observable`` to add accessor observers to stored properties.
///
/// This macro is applied automatically by `@Observable` to each stored `var` property.
/// Do not use directly.
@attached(accessor)
public macro ObservationTracked() =
#externalMacro(
module: "LuminateObservationMacros",
type: "ObservableMacro"
)