Build Recursive Text View in SwiftUI

772 views Asked by At

My goal is to create a SwiftUI view that takes a String and automatically formats that text into Text views. The portion of the string that needs formatting is found using regex and then returned as a Range<String.Index>. This can be used to reconstruct the String once the formatting has been applied to the appropriate Text views. Since there could be multiple instances of text that needs to be formatted, running the formatting function should be done recursively.

struct AttributedText: View {
    @State var text: String
    
    var body: some View {
        AttributedTextView(text: text)
    }
    
    @ViewBuilder
    private func AttributedTextView(text: String) -> some View {
        if let range = text.range(of: "[0-9]+d[0-9]+", options: .regularExpression) {
       
        //The unattributed text
        Text(text[text.startIndex..<range.lowerBound]) +
            
            //Append the attributed text
            Text(text[range]).bold() +
            
            //Search for additional instances of text that needs attribution
            AttributedTextView(text: String(text[range.upperBound..<text.endIndex]))
        
        } else {
            //If the searched text is not found, add the rest of the string to the end
            Text(text)
        }
    }

I get an error Cannot convert value of type 'some View' to expected argument type 'Text', with the recommended fix being to update the recursive line to AttributedTextView(text: String(text[range.upperBound..<text.endIndex])) as! Text. I apply this fix, but still see the same compiler error with the same suggested fix.

Compiler error showing "Insert as! Text" as the recommended solution

A few workarounds that I've tried:

  • Changing the return type from some View to Text. This creates a different error Cannot convert value of type '_ConditionalContent<Text, Text>' to specified type 'Text'. I didn't really explore this further, as it does make sense that the return value is reliant on that conditional.
  • Returning a Group rather than a Text, which causes additional errors throughout the SwiftUI file

Neither of these solutions feel very "Swifty". What is another way to go about this? Am I misunderstanding something in SwiftUI?

1

There are 1 answers

0
Tristan Warner-Smith On BEST ANSWER

There are a few things to clarify here:

The + overload of Text only works between Texts which is why it's saying it cannot convert some View (your return type) to Text. Text + Text == Text, Text + some View == ☠️

Changing the return type to Text doesn't work for you because you're using @ViewBuilder, remove @ViewBuilder and it'll work fine.

Why? @ViewBuilder allows SwiftUI to defer evaluation of the closure until later but ensures it'll result in a specific view type (not AnyView). In the case where your closure returns either a Text or an Image this is handy but in your case where it always results in Text there's no need, @ViewBuilder forces the return type to be ConditionalContent<Text, Text> so that it could have different types.

Here's what should work:

private static func attributedTextView(text: String) -> Text {
    if let range = text.range(of: "[0-9]+d[0-9]+", options: .regularExpression) {

    //The unattributed text
    return Text(text[text.startIndex..<range.lowerBound]) +

        //Append the attributed text
        Text(text[range]).bold() +

        //Search for additional instances of text that needs attribution
        AttributedTextView(text: String(text[range.upperBound..<text.endIndex]))

    } else {
        //If the searched text is not found, add the rest of the string to the end
        return Text(text)
    }
}

I made it static too because there's no state here it's a pure function and lowercased it so it was clear it was a function not a type (the function name looks like a View type).

You'd just call it Self.attributedTextView(text: ...)