New SwiftUI.ForEach.init<C>(_ data: Binding<C>, content: @escaping (Binding<C.Element>) -> Content) What's going on with the content closure parameter?

It used like this:

ForEach($model.users) { $user in
    VStack {
        HStack {
            // user is: var user { get set }
            // where is it (no $ in front) from?
            Text(user.name)
            Spacer()
            Toggle("User has been contacted", isOn: $user.isContacted)
                .labelsHidden()
        }
        Text("Contacted: \(user.isContacted ? "Yes" : "No")")
    }
}

The content closure parameter is declared as $user with this $ prefix. And user (without $) can be used to access the element value. Where is this user come to from?

The content closure parameter type is a Binding<[User]>.Element. Just by the look of the code, I thought $user is just conventional naming with this $ prefix to signify this is a binding. But the plain user variable is used to access the element as a value, seems some kind of code synthesis happen behind the scenes some how?

What's going on? I can use shorthand parameter $0 or omit the $ in the closure parameter name, but then there is no way to refer to the element as value.

doc

My Test
import SwiftUI

struct User: Identifiable {
    let id = UUID()
    var name: String
    var isContacted = false
}

final class Model: ObservableObject {
    @Published var users: [User]

    init(_ users: [User]) {
        self.users = users
    }
}

struct ContentView: View {
    @EnvironmentObject private var model: Model

    var body: some View {
        List {
            ForEach($model.users) { $user in
                VStack {
                    HStack {
                        // user is: var user { get set }
                        // where is it (no $ in front) from?
                        Text(user.name)
                        Spacer()
                        Toggle("User has been contacted", isOn: $user.isContacted)
                            .labelsHidden()
                    }
                    Text("Contacted: \(user.isContacted ? "Yes" : "No")")
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let data = [
            User(name: "Larry"),
            User(name: "Curly"),
            User(name: "Moe")
        ]
        ContentView()
            .environmentObject(Model(data))
    }
}

It is a recently-extended wrapper: SE-0293. For the most part, if you do { $user in }, you be able to access $user, and user, which is just $user.wrappedValue.

1 Like

Thank you for solving the mystry for me! So this property wrapper magic is not visible by looking at "Jump to Definition"? Can't tell the content closure parameter is a property wrapper.

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ForEach where ID == Data.Element.ID, Content : View, Data.Element : Identifiable {

    /// Creates an instance that uniquely identifies and creates views across
    /// updates based on the identity of the underlying data.
    ///
    /// It's important that the `id` of a data element doesn't change unless you
    /// replace the data element with a new data element that has a new
    /// identity. If the `id` of a data element changes, the content view
    /// generated from that data element loses any current state and animations.
    ///
    /// - Parameters:
    ///   - data: The identified data that the ``ForEach`` instance uses to
    ///     create views dynamically.
    ///   - content: The view builder that creates views dynamically.
    public init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content)
}

It is indeed not necessary that content's first argument (aka, Data.Element) is a property wrapper. It is together from the fact that $model.users (Data) is a collection of property wrappers.

From SE-0293 - Closure:

For closures that take in a projected value, the property-wrapper attribute is not necessary if the backing property wrapper and the projected value have the same type, such as the @ Binding

I think I understand now: Binding<Value>'s projectedValue is also Binding<Value>. So since ForEach.init content closure parameter is a Binding<Value>, its projectedValue is also Binding<Value>, which makes $user means take the parameter's projectedValue to initialize the closure parameter with .init(projectedValue:).

Terms of Service

Privacy Policy

Cookie Policy