Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BindingOf

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:).

Why BindingOf?

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))

Requirements

  • Xcode 15 or later
  • Swift 5.9 or later
  • iOS 15+, iPadOS 15+, macOS 12+, tvOS 15+, watchOS 8+

Installation

Xcode (Swift Package Manager)

  1. In Xcode, choose: FileAdd Package Dependencies….
  2. In “Search or Enter Package URL”, paste: https://github.com/carlosypunto/BindingOf
  3. For “Dependency Rule”, choose “Branch”.
  4. In “Branch”, enter main.
  5. Select the target(s) where you want to add the package and finish.

Package.swift

// In your Package.swift
.dependencies: [
    .package(url: "https://github.com/carlosypunto/BindingOf", branch: "main")
],
.targets: [
    .target(
        name: "<NameOfYourTarget>",
        dependencies: ["BindingOf"]
    )
]

Quick Start

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.

Usage

  • 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

How It Works

#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:).

Macro Details

#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])

Constraints and Notes

  • 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.

Safety and Performance

  • 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.

Limitations

  • 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.

Testing

  • Previews: Use #Preview or 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.

Error Messages and Troubleshooting

  • “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.

Performance

• 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.

FAQ

  • Why not just use Binding(get:set:)?
    • You can! #bindingOf just removes boilerplate and reduces mistakes.
  • 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 $title when you already have @State var title.

Alternatives

  • Manual Binding(get:set:) for full control.
  • Key paths with custom wrappers (more verbose).
  • $ projected values when available (e.g., @State, @FocusState, etc.).

Roadmap

  • Better diagnostics for optional chains
  • Additional checks for subscript safety (where feasible at compile time)
  • Expanded examples and cookbook

Contributing

Contributions are welcome! Please open an issue or pull request with a clear description, test coverage where applicable, and rationale for the change.

License

This library is released under the MIT license. See LICENSE for details.

About

A tiny Swift macro library that generates a SwiftUI `Binding`

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages