Where clause on init, and opaque types?

This is another language question that came up for me when using SwiftUI. I did not try to boil down the example to remove the SwiftUI, because I think it's useful that the real example shows why you want to do this.

I have this generic struct, which I can instantiate with a calls like:

RowWithNavArrow(first: MyCustomView(), ...)
RowWithNavArrow(text: "Foo", ...)

In the second line, I need a where clause on the init, like FirstView == Text, so that it can infer the generic parameter. My problem is that if I add the call to lineLimit as seen below, I don't know what to put in place of Text. Because the return type of lineLimit(...) is some View.

Is there a way to deal with this without resorting to type erasure to AnyView?

I want to say FirstView == typeof( Text.lineLimit(Int) ) But I don't think there is a way to do that. ??

public struct RowWithNavArrow<FirstView: View>: View {
    let firstView: FirstView
    let tap: () -> Void
    
    // where clause here is not right
    public init(text: String, tap: @escaping () -> Void) where FirstView == Text {
        self.firstView = Text(verbatim: text).lineLimit(1)
        self.tap = tap
    }
    public init(first: FirstView, tap: @escaping () -> Void) {
        self.firstView = first
        self.tap = tap
    }
   ...
}

Are you against introducing a new view to solve this?

If not, you could do:

struct LineLimitedText {
    
    let text: Text
    
    let lineLimit: Int
    
    var body: some View {
        text.lineLimit(lineLimit)
    }
    
}

public struct RowWithNavArrow<FirstView: View>: View {

    public init(text: String, tap: @escaping () -> Void) where FirstView == LineLimitedText {
        self.firstView = LineLimitedText(
            text: Text(verbatim: text),
            lineLimit: 1
        )
    }
    
}
1 Like

This is the solution:

extension RowWithNavArrow where FirstView == Text {
    
    public init(text: String, tap: @escaping () -> Void) {
        self.firstView = Text(verbatim: text)
        self.tap = tap
    }

}

Thank you! That works for me.

But this doesn't add the lineLimit(...) call on the Text. If you add that it won't work.

Yes, because the type of Text(verbatim: text).lineLimit(1) is not Text. Either figure out the type of this expression (by calling type(of:) at runtime), or wrap it in AnyView, and use that type, respectively, in the where clause of the extension that contains the initializer.

The pattern here should be clear to you by now: Figure out the concrete type you want to use, and specify that in the where clause of the extension.

Yes, because the type of Text(verbatim: text).lineLimit(1) is not Text . Either figure out the type of this expression (by calling type(of:) at runtime), or wrap it in AnyView , and use that type, respectively, in the where clause of the extension that contains the initializer.

Just to clarify: type(of:) is a not a great idea: it'll break as soon as SwiftUI happens to change the underlying type the modifiers use, and will pollute your code with noisy type signatures. AnyView is also less than ideal and Apple generally recommends against it, with the exception of cases where it can't be avoided.

There are other, more stable ways — like the one I shared before — to work around this problem.