Swift package

swift-keypaths-extensions

Compose optional key paths and map SwiftUI bindings without losing animation transactions

Keep SwiftUI updates on the binding path

A Binding carries more than read and write closures. SwiftUI propagates update context – including animation – through its Transaction. Rebuilding a binding with Binding(get:set:) can sever that path: the value still changes, but an update may arrive without the animation information attached to the original binding.

KeyPathsExtensions re-exports KeyPathMapping, so the same focused import provides reusable, key-path-based binding transformations.

import SwiftUI

struct ProgressEditor: View {
  @SwiftUI.State
  private var progress: Float = 0.25

  var body: some View {
    let sliderValue = Binding<Double>(
      get: { Double(progress) },
      set: { newValue in
        progress = Float(newValue)
      }
    )

    Slider(value: sliderValue, in: 0...1)
  }
}
Binding(get:set:)
import KeyPathsExtensions
import SwiftUI

struct ProgressEditor: View {
  @SwiftUI.State
  private var progress: Float = 0.25

  var body: some View {
    Slider(
      value: $progress[
        convert: .to(Double.self)
      ],
      in: 0...1
    )
  }
}
KeyPathMapping

Why properties and subscripts work

Binding supports dynamic-member lookup through writable key paths. A computed property can therefore derive a binding without reconstructing it from unrelated closures:

extension BinaryFloatingPoint {
  var double: Double {
    get { Double(self) }
    set { self = Self(newValue) }
  }
}

Slider(value: $progress.double, in: 0...1)

This preserves the structural path SwiftUI understands. The trade-off is where the transformation lives: a private extension cannot be reused, while a public double property becomes part of every conforming type’s namespace.

Put reusable transformations in their own namespace

KeyPathMapper gives transformations a dedicated, generic namespace. The predefined numeric conversion used above follows this shape:

extension KeyPathMapper.MutatingConversionTo
where Root: BinaryFloatingPoint {
  static func to<T: BinaryFloatingPoint>(
    _ type: T.Type
  ) -> Self where Member == T {
    .inline(
      extract: { T($0) },
      embed: { Root($0) }
    )
  }
}

Apply the conversion through [convert:], and Binding’s dynamic-member subscript continues the original key-path chain. The mapping stays reusable without adding application-specific properties to Float, Double, or other numeric types.

Use [map:] for read-only derivation, [convert:] for extraction and write-back, or [getter:setter:] when a transformation needs a custom pair. swift-keypaths-extensions enables the predefined conversions – including numeric .to(...) – through its package dependency.

Compose through optional roots

Swift can express \Root.child?.count directly, but separately supplied paths stop composing once one side is optional. The package restores that operation while preserving the strongest key-path type available.

let child: KeyPath<Root, Root.Child?> = \.child
let count: KeyPath<Root.Child, Int> = \.count

let optionalCount: KeyPath<Root, Int?> =
  child.appending(path: count)

let countWithDefault: KeyPath<Root, Int> =
  optionalCount.unwrapped(with: 0)

Use withOptionalRoot() to lift a standalone path into an optional context. Keep unwrapping nonaggressive by default: writes through a nil root are ignored unless replacing nil is explicitly supported by the value semantics.

APIResult
[convert:]Apply a reusable read/write transformation through a key path
appending(path:)Compose independently supplied paths through an optional root
withOptionalRoot()Lift a path so its root may be absent
unwrapped(with:)Expose an optional value through an explicit default
unsafeSendable()Spell an audited sendable key-path existential

Writable overloads preserve writability only when both component paths support it. unsafeSendable() is an unchecked cast; it does not make captured state thread-safe.