3

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!

8
  • State won’t work in a view unless it has @ annotation. So having @State of States won’t give you updates you expect. Commented Oct 3, 2023 at 6:28
  • 1
    State can work even when not used as a property wrapper. For example, var countState: State<Int> = .init(initialValue: 0) works fine in a View, but you have to access its value as count.wrappedValue and its Binding as count.projectedValue. Commented Oct 3, 2023 at 15:55
  • Updates to such a state won’t cause rerender of the view Commented Oct 3, 2023 at 21:29
  • 1
    Yeah I tried var state: (repeat State<each T>) and found that SwiftUI doesn't see those States, probably because they're wrapped inside a tuple. Commented Oct 4, 2023 at 15:23
  • 1
    I wasn’t familiar with the philosophy of stackoverflow and once found that my answer didn’t help I did remove it. But then moderators explained me everything and reverted my changes Commented Oct 5, 2023 at 10:01

1 Answer 1

6

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.

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for the reply! I'm not a huge fan of the index logic and Any type casting though, it seems like the compiler should be able to accomplish the same thing with the right repeat/each syntax. Your response did get me closer to what I was hoping to achieve though, I updated the post question with my new code if you wouldn't mind taking a look.
Please notice the code without type erasing.
Nobody is a fan of type erasing and indexing but this is completely safe in this situation as it is only an implementation detail and public api stay strongly typed. You got plenty of type erasure throughout frameworks (AnySequence or AnyPublisher etc) so there is nothing to be afraid of.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.