Skip to content

Instantly share code, notes, and snippets.

@moyerr
Created April 2, 2026 19:25
Show Gist options
  • Select an option

  • Save moyerr/375a6d1ada83088a7583be9440bbb281 to your computer and use it in GitHub Desktop.

Select an option

Save moyerr/375a6d1ada83088a7583be9440bbb281 to your computer and use it in GitHub Desktop.
An environment action for mutating state owned by an ancestor view
/// An action that updates a value stored in the environment.
///
/// ## Overview
///
/// Use an `Updater` to allow descendant views to modify a value owned by
/// an ancestor view, without passing Bindings or specific update actions for each property.
/// The updater accepts a ``WritableKeyPath`` and a new value, then applies
/// the change through a closure you provide at initialization.
///
/// ### Setting up the updater
///
/// Create an `Updater` in the view that owns the state and place it in the environment:
///
/// ```swift
/// @State private var settings = SettingsValues()
/// @Namespace private var id
///
/// var body: some View {
/// ContentView()
/// .environment(\.settings, settings)
/// .environment(\.updateSettings, Updater(id: id) { transform in
/// transform(&settings)
/// })
/// }
/// ```
///
/// The `id` parameter should be a stable and unique identifier tied to the lifetime
/// of the view that owns the state being modified. The `update` closure receives a
/// transform function that mutates the root value in place.
///
/// ### Using the updater
///
/// Read the updater from the environment and call it with a key path and
/// value:
///
/// ```swift
/// @Environment(\.settings.notificationsEnabled) private var notificationsEnabled
/// @Environment(\.updateSettings) private var updateSettings
///
/// var body: some View {
/// Toggle(
/// "Enable Notifications",
/// isOn: Binding(
/// get: { notificationsEnabled },
/// set: { updateSettings(\.notificationsEnabled, $0) }
/// )
/// )
/// }
/// ```
struct Updater<Root>: Hashable, Identifiable {
let id: AnyHashable
private let update: ((inout Root) -> Void) -> Void
/// Creates an updater with the given identity and mutation handler.
///
/// - Parameters:
/// - id: A stable, unique identifier for this updater, preferably tied
/// to the lifetime of the view that owns the state being updated.
/// - update: A closure that receives a transform function. Call the
/// transform with a mutable reference to the root value to apply
/// the change.
init(
id: some Hashable,
update: @escaping ((inout Root) -> Void) -> Void
) {
self.id = AnyHashable(id)
self.update = update
}
/// Updates the property at the given key path to the specified value.
///
/// - Parameters:
/// - keyPath: A writable key path from `Root` to the property to update.
/// - value: The new value to assign.
func callAsFunction<Value>(_ keyPath: WritableKeyPath<Root, Value>, _ value: Value) {
let transform: (inout Root) -> Void = { root in
root[keyPath: keyPath] = value
}
update(transform)
}
/// Updates the root value with the specified value.
///
/// - Parameter root: The new root value to assign.
func callAsFunction(_ root: Root) {
let transform: (inout Root) -> Void = { $0 = root }
update(transform)
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(ObjectIdentifier(Root.self))
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment