swiftparameter-packwwdc

How to use Swift parameter packs in a View


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!


Solution

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