Issues creating init similar to Label(_:systemImage:)

I'm trying to create a custom view that uses a @ViewBuilder for it's content, but then has specialized initializers like the one for Label(_:systemImage:) but I seem to be running into an issue.

Here's my sample code:

struct IconView<Content: View>: View {
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        Rectangle()
            .frame(width: 55, height: 55)
            .foregroundStyle(.blue)
            .overlay {
                content
            }
    }
}

extension IconView where Content == Image {
    init(systemImage named: String) {
        self.init {
            Image(systemName: named)
//                .imageScale(.large)
        }
    }
}

If I leave the line that I have commented out, out, then it builds fine but If I comment back in the line .imageScale(.large) I get this error:

Cannot convert return expression of type 'some View' to return type 'Image'

I feel like I'm just not understanding why this error is happening. Is it not possible to make an initializer where I can customize the returned image?

Why does using my IconView work in this way:

struct ContentView: View {
    var body: some View {
        IconView() {
            Image(systemName: "swift")
                .imageScale(.large) // ← why does this work here?
        }
    }
}

but not in my IconView(systemImage:) initializer?

imageScale gives "some View", not an "Image", so your "Content == Image" constraint can't be satisfied. Maybe this as a workaround?

func largeIconView(systemImage name: String) -> some View {
    Image(systemName: name)
        .imageScale(.large)
}

Or just drop the where Content == Image. SwiftUI modifiers are designed so that you can apply them to wrapper views and they’ll apply to all the relevant inner views. That’s why you can do Image(systemName: "arrow.right").border(Color.red).imageScale(.large), even though there’s a border view wrapped around the Image.

Converting the code to this:

extension IconView {
    init(systemImage named: String) {
        self.init {
            Self.systemImage(named)
        }
    }
    
    private static func systemImage(_ named: String) -> some View {
        Image(systemName: named)
            .imageScale(.large)
    }
}

Then changes the error to:

Cannot convert return expression of type 'some View' to return type 'Content'

Or just drop the where Content == Image . SwiftUI modifiers are designed so that you can apply them to wrapper views and they’ll apply to all the relevant inner views.

Originally I didn't have the where clause but I couldn't get any combination of what I was trying to do to work.I then I looked into Label since it had a similar initializer to what I was trying to do. The header has it defined this way:

extension Label where Title == Text, Icon == Image {

    /// Creates a label with a system icon image and a title generated from a
    /// localized string.
    ///
    /// - Parameters:
    ///    - titleKey: A title generated from a localized string.
    ///    - systemImage: The name of the image resource to lookup.
    public init(_ titleKey: LocalizedStringKey, systemImage name: String)

Which made think that that was the direction I need to go down, because if I leave out the where then even returning just an Image with no modifiers doesn't work:

extension IconView {
    init(systemImage named: String) {
        self.init {
            Image(systemName: named)
        }
    }
}

The error from this is:

Cannot convert return expression of type 'Image' to return type 'Content'

Because the imageScale modifier returns some View, you cannot write a constraint matching its return type. So you must avoid the need for its return type. Try introducing a wrapper view type that applies the modifier. Then you can use the wrapper view type in your constraint.

struct _IconView_SystemImageContent: View {
    let name: String

    var body: some View {
        Image(systemName: name)
            .imageScale(.large)
    }
}

extension IconView where Content == _IconView_SystemImageContent {
    init(systemName: String) {
        self.init {
            _IconView_SystemImageContent(name: systemName)
        }
    }
}
2 Likes

Huh. I would also have expected this to work, but I can see why it might not. This is probably for the best, because otherwise the compiler would be invisibly making an ABI promise that this initializer will always produce an IconView<Image>. You clearly want the flexibility to produce something else, namely an Image wrapped in whatever modifier view .imageScale() returns.

Can you do extension IconView where Content == some View?

Edit: nope:

error: repl.swift:10:37: error: 'some' types are only permitted in properties, subscripts, and functions
extension IconView where Content == some View {

Maybe one of the experts (@hborla, @Douglas_Gregor, @Slava_Pestov) know the right spelling to convince the type system that your initializer produces an ImageView<some View>.

I meant merely this:

sample
import SwiftUI

struct IconView<Content: View>: View {
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        Rectangle()
            .frame(width: 55, height: 55)
            .foregroundStyle(.blue)
            .overlay {
                content
            }
    }
}

func iconView(systemImage named: String, scale: Image.Scale) -> some View {
    Image(systemName: named)
        .imageScale(.large)
}

struct ContentView: View {
    var body: some View {
        IconView {
            iconView(systemImage: "swift", scale: .large)
        }
    }
}

Yeah, that was the same line of thinking that I went down but was also surprised by the error :disappointed:

I see. Yeah that could work but I was more interested how Label was pulling off it's initializer because it was less manual work on the user.

Interesting, this makes sense. I wonder if this is why Apple is doing internally for Label's init.

It doesn’t seem like it:

import SwiftUI

let l = Label("Hello", systemImage: "arrow.right")
print(type(of: l)) // Label<Text, Image>

The trick might be that this initializer can never modify its Image, and all modifiers have to be inherited from the Environment (including the inherited LabelStyle).