Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: How do I make my closure return 'Content' instead of 'some View' when calling it from another initialiser?

I am currently learning SwiftUI, and I ran into a problem.

In the below code I have two initialisers. The first one works (assuming I comment out the second one long enough to test the code). The second one does not. It throws a compiler error saying "Cannot assign value of type 'some View' to type 'Content'."

What am I doing wrong here? I am able to successfully call the first initialiser from outside the struct using Infobox() { Text("Some Text") }, which seems to me to be the exact same syntax I'm using when trying to call it from the second initialiser. No matter how much I google, I can't seem to figure this one out.

I'd very much appreciate any help you can give.

struct Infobox<Content>: View where Content: View {

    let content: Content

    var body: some View {
        content
    }

    init(@ViewBuilder _ content: @escaping () -> Content) {
        self.content = content()
    }

    init(withHeader headerText: String, @ViewBuilder _ content: @escaping () -> Content) {
        self.init() {
            VStack(alignment: .leading) {
                Text(headerText)
                    .font(.headline)
                }
                content()
            }
        }
    }
}
like image 848
Robin Avatar asked Oct 26 '25 06:10

Robin


1 Answers

What you mean is probably this:

init<T: View>(withHeader headerText: String, @ViewBuilder _ content: @escaping () -> T) 
    where Content == VStack<TupleView<(Text, T)>> {
    self.init {
        VStack(alignment: .leading) {
            Text(headerText)
                .font(.headline)
            content()
        }
    }
}

The view created by this initialiser will always be of the form (And yes)

InfoBox<VStack<TupleView<(Text, SomeOtherView)>>>

In other words, this init is deciding what Content should be. So it is only applicable to InfoBoxes with Content == ThatLongTypeName, hence the constraint.

Note that the caller can specify the type of SomeOtherView, but that is not Content! Content is VStack<TupleView<(Text, SomeOtherView)>>. Therefore, an extra generic parameter is added, and the closure return type is changed.


IMO, it'd be much simpler if you just did this:

struct Infobox<Content>: View where Content: View {

    let content: () -> Content
    let headerText: String

    var body: some View {
        VStack(alignment: .leading) {
            if !headerText.isEmpty {
                Text(headerText)
                    .font(.headline)
            }
            content()
        }
    }

    // note the optional parameter here
    init(withHeader headerText: String = "", @ViewBuilder _ content: @escaping () -> Content) {
        self.content = content
        self.headerText = headerText
    }
}
like image 77
Sweeper Avatar answered Oct 29 '25 09:10

Sweeper



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!