SwiftUI View: two different initializers: cannot convert value of type 'Text' to closure result type 'Content'
Asked Answered
W

3

7

The code:

import SwiftUI

public struct Snackbar<Content>: View where Content: View {
    private var content: Content

// Works OK
    public init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    init(_ text: String) {
        self.init {
            Text(text) // cannot convert value of type 'Text' to closure result type 'Content'
                .font(.subheadline)
                .foregroundColor(.white)
                .multilineTextAlignment(.leading)
        }
    }

    public var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                content
            }
            Spacer()
        }
        .frame(maxWidth: .infinity,
               minHeight: 26)
        .padding(.fullPadding)
        .background(Color.black)
        .clipShape(RoundedRectangle(cornerRadius: .defaultCornerRadius))
        .shadow(color: Color.black.opacity(0.125), radius: 4, y: 4)
        .padding()
    }
}

I'm getting this error:

cannot convert value of type 'Text' to closure result type 'Content'

The goal I'm trying to achieve is to have 2 separate initializers, one for the content of type View and the other is a shortcut for a string, which will place a predefined Text component with some styling in place of Content.

Why am I getting this error if Text is some View and I think it should compile.

Workmanship answered 7/3, 2022 at 14:57 Comment(0)
C
0

You can specify the type of Content.

Code:

public struct Snackbar<Content>: View where Content: View {
    private var content: Content

// Works OK
    public init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    init(_ text: String) where Content == ModifiedContent<Text, _EnvironmentKeyWritingModifier<TextAlignment>> {
        self.init {
            Text(text)
                .font(.subheadline)
                .foregroundColor(.white)
                .multilineTextAlignment(.leading) as! ModifiedContent<Text, _EnvironmentKeyWritingModifier<TextAlignment>>
        }
    }

    /* ... */
}

The only difference here is the where after the init and the force-cast to the type inside the init.

To avoid the specific type, you can abstract this into a separate view:

init(_ text: String) where Content == ModifiedText {
    self.init {
        ModifiedText(text: text)
    }
}

/* ... */

struct ModifiedText: View {
    let text: String

    var body: some View {
        Text(text)
            .font(.subheadline)
            .foregroundColor(.white)
            .multilineTextAlignment(.leading)
    }
}
Counterclockwise answered 7/3, 2022 at 15:12 Comment(5)
and the key is depending on the last property of Text.Dispart
I like the idea of it and actually this is what I was looking for in the first place, but looks like Swift Type System ain't yet flexible enough to encapsulate "any modification of Text ", so this solution is quite fragileWorkmanship
@RichardTopchii Yes, but if you know what type this is at compile time so you can force cast it. You can find the type by simply force-casting to the wrong type, and the copy & paste the correct type. Haven't tested if the underscored type annotations in the new Xcode beta fix this and infer it.Counterclockwise
@RichardTopchii Additionally, you could create this whole Text part in a separate view and then you won't have to deal with types like this. That may be a more preferred way, and you will be able to change the modifiers without changing the type. I have modified the answer with a better way to avoid the long type cast.Counterclockwise
Interesting idea! I ended up with completely changing the problem, but this would have helped me quite well.Workmanship
T
6

The general-purpose solution to this is to provide a wrapper that is semantically equivalent to some View. AnyView is built in and serves that purpose.

init(_ text: String) where Content == AnyView {
  self.init {
    AnyView(
      Text(text)
        .font(.subheadline)
        .foregroundColor(.white)
        .multilineTextAlignment(.leading)
    )
  }
}

Also, change your code to

private let content: () -> Content

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

so that you don't have to wrap the result of content in another closure.

VStack(alignment: .leading, spacing: 4, content: content)
Trow answered 8/3, 2022 at 0:54 Comment(4)
I ended up with a very similar solution to yours! In my case, I had to show the "text" component always, so I've just added two initializers with an EmptyView if only text is to be displayed.Workmanship
This was really helpful. However, if you know you're going to return Text instead of wrapping in AnyView, you can just use Content == Text. I just don't like seeing AnyViewAiley
I don't like it either, but ModifiedContent<Text, _EnvironmentKeyWritingModifier<TextAlignment>> is not Text, and you can't constrain the init to ModifiedContent<Text, _EnvironmentKeyWritingModifier<TextAlignment>> anyway, because it's opaqued.Trow
@Ailey Thank you so much!!!!, really liked your idea, definitely avoids AnyView and also since the SwiftUI heavily relies on the type system, so using AnyView might lose some of the type information (not an expert but it affects unnecessary re-rendering or while using animation may just fade in fade out as would be considered as 2 different views)Tolmann
D
0

One way is to make content optional and use another text var and show view based on a nil value.

public struct Snackbar<Content>: View where Content: View {
    private var content: Content? // <= Here
    private var text: String = "" // <= Here
    
    // Works OK
    public init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    init(_ text: String) {
        self.text = text // <= Here
    }
    
    public var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                if let content = content { // <= Here
                    content
                } else {
                    Text(text)
                        .font(.subheadline)
                        .foregroundColor(.white)
                        .multilineTextAlignment(.leading)
                }
            }
            Spacer()
        }
        // Other code



You can also use AnyView

public struct Snackbar: View {
    private var content: AnyView // Here
    
    // Works OK
    public init<Content: View>(@ViewBuilder content: () -> Content) {
        self.content = AnyView(content()) // Here
    }
    
    init(_ text: String) {
        self.content = AnyView(Text(text)
                            .font(.subheadline)
                            .foregroundColor(.white)
                            .multilineTextAlignment(.leading)
        ) // Here
    }
    
    public var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                self.content
            }
            Spacer()
        }
Dispart answered 7/3, 2022 at 15:9 Comment(7)
Any option to "create" Text content in the initializer?Workmanship
Generally, init is not for doing this kind of stuff. but still, I'm checking.Dispart
Yeah, please check, very interested in this kind of initializer specifically.Workmanship
You can solve it by using AnyView but not recommended.Dispart
Could you provide an example (feel free to either edit your post or add a new one)Workmanship
Very interesting solution, thanks! I came up with a similar one after your suggestion,Workmanship
That's not the way to do it. Constrain the init to where Content == AnyView.Trow
C
0

You can specify the type of Content.

Code:

public struct Snackbar<Content>: View where Content: View {
    private var content: Content

// Works OK
    public init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    init(_ text: String) where Content == ModifiedContent<Text, _EnvironmentKeyWritingModifier<TextAlignment>> {
        self.init {
            Text(text)
                .font(.subheadline)
                .foregroundColor(.white)
                .multilineTextAlignment(.leading) as! ModifiedContent<Text, _EnvironmentKeyWritingModifier<TextAlignment>>
        }
    }

    /* ... */
}

The only difference here is the where after the init and the force-cast to the type inside the init.

To avoid the specific type, you can abstract this into a separate view:

init(_ text: String) where Content == ModifiedText {
    self.init {
        ModifiedText(text: text)
    }
}

/* ... */

struct ModifiedText: View {
    let text: String

    var body: some View {
        Text(text)
            .font(.subheadline)
            .foregroundColor(.white)
            .multilineTextAlignment(.leading)
    }
}
Counterclockwise answered 7/3, 2022 at 15:12 Comment(5)
and the key is depending on the last property of Text.Dispart
I like the idea of it and actually this is what I was looking for in the first place, but looks like Swift Type System ain't yet flexible enough to encapsulate "any modification of Text ", so this solution is quite fragileWorkmanship
@RichardTopchii Yes, but if you know what type this is at compile time so you can force cast it. You can find the type by simply force-casting to the wrong type, and the copy & paste the correct type. Haven't tested if the underscored type annotations in the new Xcode beta fix this and infer it.Counterclockwise
@RichardTopchii Additionally, you could create this whole Text part in a separate view and then you won't have to deal with types like this. That may be a more preferred way, and you will be able to change the modifiers without changing the type. I have modified the answer with a better way to avoid the long type cast.Counterclockwise
Interesting idea! I ended up with completely changing the problem, but this would have helped me quite well.Workmanship

© 2022 - 2025 — McMap. All rights reserved.