I want to easily provide mutable bindings to my views for SwiftUI previews (so that the preview is interactive unlike when you pass a .constant(...)
binding). I followed the BindingProvider approach which allows for one value at a time, like this:
#Preview {
BindingProvider(true) { binding in
SomeToggleView(toggleBinding: binding)
}
}
It would be nice to pass in multiple values to be bound, like this
#Preview {
BindingProvider(true, "text") { toggleBinding, textFieldBinding in
SomeToggleAndTextFieldView(toggleBinding: toggleBinding, textFieldBinding: textFieldBinding)
}
}
So far I've arrived at this code, but it's not compiling and just says "Command SwiftCompile failed with a nonzero exit code"
struct BindingProvider<each T, Content: View>: View {
@State private var state: (repeat State<each T>)
private var content: ((repeat Binding<each T>)) -> Content
init(_ initialState: repeat each T,
@ViewBuilder content: @escaping ((repeat Binding< each T>)) -> Content)
{
self.content = content
self._state = State(initialValue: (repeat State(initialValue: each initialState)))
}
var body: Content {
content((repeat (each state).projectedValue))
}
}
Maybe I'm doing something wrong or maybe this is just a compiler issue? Any help would be greatly appreciated!
Seems like compiler can't manage some Protocol
constructs together with parameter packs yet. Specifying the concrete view type helps so just replace
var body: some View {
with
var body: Content {
Solution without type erasing does not provide smooth Binding unwrapping... And without enumerating somehow tuple's elements you can't unwrap it. I think it will not be possible until @dynamicMemberLookup will learn how to handle parameter packs.
struct BindingProvider<each T, Content: View>: View {
@State private var state: (repeat each T)
private var content: (_ binding: Binding<(repeat each T)>) -> Content
init(_ initialState: repeat each T, @ViewBuilder content: @escaping (Binding<(repeat each T)>) -> Content) {
self.content = content
self._state = State(initialValue: (repeat each initialState))
}
var body: Content {
self.content($state)
}
}
#Preview {
BindingProvider(0, true) { bind in
let (count, isOn) = (bind.0, bind.1)
Stepper("", value: count)
Toggle("", isOn: isOn)
}
}
Here is solution with type erasing and enumerating tuple's values:
struct BindingProvider<each T, Content: View>: View {
@State private var states: [Any]
private var content: ((repeat Binding<each T>)) -> Content
init(_ initialState: repeat each T, @ViewBuilder content: @escaping (repeat Binding<each T>) -> Content) {
self.content = { (args: (repeat Binding<each T>)) in
content(repeat each args)
}
// convert arguments to an array
var states = [Any]()
repeat states.append(each initialState as Any)
_states = State(initialValue: states)
}
// Specify body type as Content. Compile time error otherwise
var body: Content {
content((repeat each makeBindings()))
}
private func makeBindings() -> (repeat Binding<each T>) {
var index = 0
func makeBinding<Result>(_ index: inout Int) -> Binding<Result> {
let currentIndex = index
index += 1
return Binding<Result> {
states[currentIndex] as! Result
} set: { newValue in
states[currentIndex] = newValue
}
}
return (repeat makeBinding(&index) as Binding<each T>)
}
}
#Preview {
BindingProvider(18, "Hello") { $temperature, $greeting in
VStack(alignment: .leading) {
Text("\(greeting), the temperature is \(temperature)º")
TextField("Greeting", text: $greeting)
Stepper("\(temperature)", value: $temperature)
}
.padding()
}
}
The solution is strongly typed even though types erasure happens under the hood. Also indexing is safe as it is bounded to number of "A"s inside (repeat each A) that is the same throughout any instance's lifetime.