What I’m trying to do:
To pass an array (ideally with simple ViewBuilder
syntax) of View
s conforming to a protocol
to some Layout view’s init
, so that the Layout view can iterate through the passed views and display them, and also be able to cast them to their protocol
type and use the methods declared in this protocol.
Why do I need this:
I want to recreate Apple’s Form
, so that the fields in my form can be auto-cycled by pressing keyboard’s next
button. Since the default Form
forces substantial visual changes and in general is very limited
What I tried so far:
-
Layout
protocol// the protocol which asks the field to be able to detect its `next` button press protocol FormField: View { var nextButtonPushPublisher: PassthroughSubject<Void, Never> { get } } // one of the complying field types struct AppTextField: FormField { // ... } struct AppForm: Layout { func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let idealViewSizes = subviews.map { $0.sizeThatFits(.unspecified) } let height = idealViewSizes.reduce(0) { $0 + $1.height } return CGSize(width: proposal.width ?? 0, height: height) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let origin = CGPoint(x: bounds.minX, y: bounds.minY) var currentY = 0.0 for v in subviews { let idealViewSize = v.sizeThatFits(.unspecified) let x = origin.x let y = origin.y + currentY - idealViewSize.height/2 let point = CGPoint(x: x, y: y) v.place(at: point, anchor: .topLeading, proposal: .unspecified) currentY += idealViewSize.height // <--- here I would like to do something like: // if let field = v as? FormField { <--- nil // field.nextButtonPushPublisher... // } } } }
-
Passing as a TupleView (doesn’t compile)
struct AppForm: View { private let views: [any FormField] @FocusState private var focus: Int? init<View: FormField>(@ViewBuilder content: @escaping () -> TupleView<View>) { self.views = content() // <--- error: Cannot assign value of type 'TupleView<View>' to type '[any FormField]' self.focus = 0 } var body: some View { VStack { ForEach(0..<views.count, id: .self) { index in let field = views[index] if let f = field as? (any FormField) { field .focused($focus, equals: index) .onReceive(f.nextButtonPushPublisher) { p in if var focus { focus += 1 } } } } } } }
-
Using ‘any FormField’ (doesn’t compile)
struct AppForm: View { private let views: [any FormField] public init(_ content: any FormField...) { self.views = content } var body: some View { VStack { ForEach(0..<views.count, id: .self) { index in views[index] // <--- error: No exact matches in reference to static method 'buildExpression' } } } }
None of them work.
Fields inside the form can be of different types, but all conforming to FormField
, which means I cannot pass them as a generic for Form
which only wants one concrete type (or I don’t know how). I could pass them as AnyView, but then they are impossible to cast back to FormField
. I feel like there must be another approach, please help.