5

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.

Richard Topchii
  • 7,075
  • 8
  • 48
  • 115

3 Answers3

4

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)
  • 1
    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. – Richard Topchii Mar 08 '22 at 12:22
  • 1
    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 `AnyView` – lbarbosa Mar 11 '23 at 15:23
  • I don't like it either, but `ModifiedContent>` is not `Text`, and you can't constrain the init to `ModifiedContent>` anyway, because it's opaqued. –  Mar 12 '23 at 00:45
  • @lbarbosa 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) – user1046037 May 15 '23 at 03:07
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()
        }
Raja Kishan
  • 16,767
  • 2
  • 26
  • 52
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)
    }
}
George
  • 25,988
  • 10
  • 79
  • 133
  • and the key is depending on the last property of Text. – Raja Kishan Mar 07 '22 at 15:17
  • 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 fragile – Richard Topchii Mar 07 '22 at 15:56
  • @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. – George Mar 07 '22 at 16:03
  • @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. – George Mar 07 '22 at 16:07
  • Interesting idea! I ended up with completely changing the problem, but this would have helped me quite well. – Richard Topchii Mar 07 '22 at 16:25