TextRenderer is a new protocol introduced at WWDC 2024 which lets us enhance how text is rendered in SwiftUI. In this small post, I want to show how to create a view that enables you to highlight certain parts of a given String. Previously this was primarily done using NSAttributedString, but with TextRenderer it is now possible to do the same in a pure SwiftUI way.
TextRenderer setup
Before starting with the actual TextRenderer implementation, we need a way to tell the rendering which parts of a given text should be highlighted. To do so, we utilize the TextAttribute type. It can be seen as a simple marker that can be attached to Text view instances. It will be read during rendering to attach certain changes to the text. It does not need any real implementation, just a type that conforms to the protocol.
Next, we can create our HighlightTextRenderer type which will take care of the text rendering. To change the style of a highlighted text the renderer will hold a reference to a ShapeStyle which can be provided to change the look of the highlighting.
To more efficiently access the text layouts and the individual lines, runs and run slices we add two extensions on Text.Layout which you can also find in Apples sample code here:
Having this in place, we start to implement the draw(layout:in:) method of our renderer.
Let's take a closer look at what is happening here. First, we check if the current run has our custom TextAttribute attached. If it doesn't, we simply draw the text without making any changes. If the TextAttribute is attached, we proceed to modify the rendering. We obtain the rect where the text will be drawn. Then we define the shape of the highlight. Finally, we fill the shape with the specified style and draw it onto the screen.
Now we have prepared our text renderer - let's look at how to use it.
Usage
The only two modifiers we need are customAttribute(_:) to attach the highlight attribute and textRenderer(_:) to make use of the HighlightTextRenderer
Highlighted View
After laying the foundations, we now can combine the logic of highlighting a text into a dedicated view to make it the logic better reusable. A HighlightedText view will consist of the base text and the part of the text that should be highlighted. If no text is highlighted this property will be nil.
If we think about what highlighting a text means, we can summarise by the following two statements:
Get all ranges of characters of highlightedText within text
Get the remaining ranges that are not covered by 1.
To accomplish these requirements, let's implement two extensions on String.
After defining the two helper methods, we can implement the final lines in 'HighlightedText'. We define a small helper struct that holds a 'Text' instance and a 'range' property describing the position of the text in the initial text.
Now we add a method that will, in order, extract an array of HighlightedTextComponent from the text property of HighlightedText.
To highlight specific ranges of text, we can create a Text instance by incorporating a custom highlight attribute for the intended ranges. For the remaining ranges, the attribute won't be added. Then, the components are sorted to maintain the original text order when eventually rendered.
The final step is implementing the body. This can be done by straightforwardly iterating over all text components and associating the HighlightTextRenderer with the Text instance if a highlight needs to be displayed.
The final HighlightedText.swift file looks like this:
With this component in place, you can start creating any view that needs highlighting. For example, a search view that highlights why a certain entry in the list is found.
Conclusion
In this small post, I showed how to use the new TextRenderer protocol to influence the rendering of Text. Later we saw how this can be used to create a SwiftUI native text highlighting view.
Please feel free to reach out to me if you find any mistakes, or have questions or suggestions. 🙂