A tiny Swift macro library that generates a SwiftUI Binding at compile time from a writable key-path–like expression. Write #bindingOf(viewModel.title) and get a Binding<String> you can pass to SwiftUI controls — without manually constructing Binding(get:set:).
Writing Binding(get:set:) by hand is repetitive and error-prone, especially for nested properties or collections. #bindingOf(...) generates the Binding for you at compile time, so your code stays concise and type-safe without sacrificing performance.
- Zero runtime overhead — expansion happens at compile time
- Type-safe, concise, and expressive
- Designed for SwiftUI ergonomics
// Before
TextField("Title", text: Binding(
get: { viewModel.title },
set: { viewModel.title = $0 }
))
// After
TextField("Title", text: #bindingOf(viewModel.title))- Xcode 15 or later
- Swift 5.9 or later
- iOS 15+, iPadOS 15+, macOS 12+, tvOS 15+, watchOS 8+
- In Xcode, choose:
File→Add Package Dependencies…. - In “Search or Enter Package URL”, paste: https://github.com/carlosypunto/BindingOf
- For “Dependency Rule”, choose “Branch”.
- In “Branch”, enter
main. - Select the target(s) where you want to add the package and finish.
// In your Package.swift
.dependencies: [
.package(url: "https://github.com/carlosypunto/BindingOf", branch: "main")
],
.targets: [
.target(
name: "<NameOfYourTarget>",
dependencies: ["BindingOf"]
)
]import SwiftUI
import BindingOf
final class ViewModel: ObservableObject {
@Published var title: String = "Hello"
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
TextField("Title", text: #bindingOf(viewModel.title))
.padding()
}
}That’s it. #bindingOf(viewModel.title) expands at compile time into a standard Binding<String> that reads and writes viewModel.title.
-
Basic:
TextField("Title", text: #bindingOf(viewModel.title)) Toggle("Enabled", isOn: #bindingOf(viewModel.isEnabled))
-
Nested properties:
TextField("City", text: #bindingOf(viewModel.profile.address.city))
-
Collections (binding to an element by index/key, when safe and in range):
TextField("First Tag", text: #bindingOf(viewModel.tags[0]))
-
With custom controls:
MyControl(value: #bindingOf(viewModel.sliderValue))
If Xcode doesn’t auto-insert the import, add:
import BindingOf#bindingOf(...) is a macro that analyzes a writable expression and generates:
- A get closure that reads the value
- A set closure that writes the value back
Because it’s a compile-time expansion, there’s no runtime reflection and no additional overhead beyond a normal Binding(get:set:).
#bindingOf(...) is a freestanding expression macro. It accepts a writable expression similar to a writable key path:
- Stored properties (e.g.,
viewModel.title) - Writable computed properties (get + set)
- Nested properties (e.g.,
viewModel.profile.address.city) - Indexed subscripts, when in range (e.g.,
viewModel.tags[0])
- The expression must be writable (similar to a writable key path). Read-only computed properties are not supported.
- Optional chains that can become nil at write-time can produce unexpected results. Consider unwrapping or providing defaults.
- If you’re binding into @Published properties on an ObservableObject, ensure your object lifetimes are correct (@StateObject vs @ObservedObject).
- Concurrency: If your model is @MainActor or you mutate on the main thread (as typical with SwiftUI), you’re fine. If you mutate from background threads, ensure thread safety.
- Indexing into arrays must be safe; out-of-bounds will crash just like normal code.
- Arrays and collections: Index must be valid at both read and write time—out-of-bounds will crash just like normal code.
- Compile-time generation: No reflection, no dynamic lookup.
- Runs as fast as manually written Binding(get:set:).
- Memory: No extra allocations beyond a standard Binding.
- Not a drop-in replacement for $ projections (e.g., @State, @FocusState). Prefer the native projections when available.
- Avoid expressions that allocate temporary values or depend on non-persistent state during write-back.
- Optional write-back through ?? or map-like transformations won’t persist changes into the original storage.
- Previews: Use
#Previewor SwiftUI previews with simple mock view models. - Unit tests: Since the macro expands at compile time, you can test the resulting behavior by exercising the bound view model property in a view or in isolated binding scenarios.
- “Expression is not writable”: Ensure you pass something that can be assigned to (e.g., a stored property, not a computed read-only).
- “Cannot infer type for macro argument”: Add explicit context (e.g., TextField(_, text: ...) provides type context).
- “Use of unresolved identifier ‘BindingOf’”: Make sure import BindingOf is present and the package is added to your target.
• Compile-time generation, no runtime cost beyond a normal Binding. • No allocations beyond what Binding(get:set:) already does. • Safe to use throughout your SwiftUI view hierarchy.
- Why not just use Binding(get:set:)?
- You can!
#bindingOfjust removes boilerplate and reduces mistakes.
- You can!
- Does it support computed properties?
- Only if they are writable (have both get and set) and don’t rely on temporary values that can’t be assigned back.
- Does it work with
@State?- Yes.
#bindingOf(self.title)or simply use$titlewhen you already have @State var title.
- Yes.
- Manual
Binding(get:set:)for full control. - Key paths with custom wrappers (more verbose).
- $ projected values when available (e.g., @State, @FocusState, etc.).
- Better diagnostics for optional chains
- Additional checks for subscript safety (where feasible at compile time)
- Expanded examples and cookbook
Contributions are welcome! Please open an issue or pull request with a clear description, test coverage where applicable, and rationale for the change.
This library is released under the MIT license. See LICENSE for details.