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.
A few workarounds that I've tried:
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.Neither of these solutions feel very "Swifty". What is another way to go about this? Am I misunderstanding something in SwiftUI?
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: ...)