// // Preference.swift // // Copyright 2026 Brendan Szymanski // // 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 . // // SPDX-License-Identifier: GPL-3.0-or-later // import LuminateCore import Portico /// Shared reference storage for a wrapper's mount-time slot resolution. @MainActor private final class ResolvedPreferences { var coordinator: Preferences? var slot: PreferenceSlot? } /// Reads and writes one persisted preference like Portico's `@State`. /// /// A missing key reads as `nil` without a fallback. Reads inside reactive closure modifiers track the /// key, writes update every reader, and persistence is queued asynchronously off the main actor. @propertyWrapper @MainActor package struct Preference: DynamicProperty { private let key: PreferenceKey private let resolved = ResolvedPreferences() /// Creates a preference declaration for `key`. /// /// - Parameter key: The stable typed preference identifier. package init(_ key: PreferenceKey) { self.key = key } /// Resolves the declaration against the mounting environment. /// /// - Parameter environment: The environment supplied by the mounted view tree. package func _resolve(in environment: EnvironmentValues) { let preferences = environment.preferences if let previous = resolved.coordinator, previous !== preferences { fatalError( "@Preference: view instance mounted into two different environment scopes. " + "A view instance resolves its preferences once; build a fresh instance per mount." ) } resolved.coordinator = preferences resolved.slot = preferences.slot(for: key) } /// The current persisted value, or `nil` when the key is unset. package var wrappedValue: Value? { get { slot.value } nonmutating set { slot.value = newValue } } /// A live binding to the optional preference value. package var projectedValue: Binding { Binding(slot, \PreferenceSlot.value) } /// The slot bound at mount. /// /// Traps rather than inventing storage when read before `_resolve`. private var slot: PreferenceSlot { guard let slot = resolved.slot else { fatalError("@Preference read before its view was mounted.") } return slot } }