Implementing Custom Underline Styles in iOS
Creating cards in Brill is one fluid gesture of holding down the Record button, speaking, and letting go when you’re done. When you let go, a card will be created with your speech transcribed into text. ✨
… but dictation is not perfect. Sometimes we generate a transcript for which there is low confidence in its accuracy. In these cases we may have different possible interpretations of the speech.
There are approaches we can use to refine low confidence alternatives (such as evaluation of grammar or prior learning) but there will be occasions where we need to defer to the user and ask them to clarify.
iOS Keyboard Dictation handles this by using private APIs to communicate low confidence interpretations to UITextView
which are indicated to the user by a blue dashed underline, and presented via UIMenuController
.
As we implemented our own dictation system in Brill, we’re unable to utilise the alternative interpretations system UITextView
has built-in.
So we decided to reimplement it. 🙃
Defining a custom style
iOS users are already accustomed to the native dashed blue underline so we want to match that to avoid cognitive dissonance and ease on-boarding.
NSAttributedString
supports underlining text with a number of different predefined styles in NSUnderlineStyle
. However, none of these styles match the underline UITextView
uses.
We need to define our own underline style and rendering. 👨🎨
You can define a new underline style by extending NSUnderlineStyle
with a getter, although make sure your value doesn’t clash with any of the existing underline styles.
Now we need to provide a way to render our new style. During the process of rendering a string, the text view will pass layout and drawing tasks to its associated NSLayoutManager
. NSLayoutManager
has numerous APIs for the different types of drawing tasks it is required to perform, one of which is rendering underlined text.
By overriding this API we can intercept underline tasks and render our custom underline style.
Intercept when needed
We only want to override rendering when the underline argument is our newly defined style, this way we don’t change default behaviour. This is simple to achieve by guarding the argument and deferring to super when needed.
guard underlineVal == .patternLargeDot else {
super.drawUnderline(...)
return
}
Note:
NSUnderlineStyle
is an option set, so if you want to utilise more advanced underline configuration be sure to perform bitwise ops to determine if your custom style is set, rather than a direct raw value comparison.
To render our own style we need to determine where the underline should be drawn. One strategy for this is to find the bounding box of the characters, and use the bottom edge as a reference to draw the dashes.
UITextView
contains some methods than can help us out here. textContainer(forGlyphAt: glyphIndex)
can convert a glyph location into a text container, an object that defines a rectangle for text to be laid out.
let container = textContainer(forGlyphAt: glyphRange.location,
effectiveRange: nil)
We can then use this text container, and the provided glyph range, to generate a bounding box for the glyphs to be underlined in this layout pass. UITextView
helps us out again here by providing us with a helper method, boundingRect(forGlyphRange: NSRange)
.
let rect = boundingRect(forGlyphRange: glyphRange, in: container)
One final consideration is that our text container may be offset within the text view which can affect the location of rendering, e.g. due to UIEdgeInsets
. We can account for this is by offsetting the bounding rect by the container origin.
let offsetRect = rect.offsetBy(dx: containerOrigin.x,
dy: containerOrigin.y)
There we have it. We have a rect that bounds glyphs to be underlined.
Drawing the line
Now that we have a defined rect, we can draw our custom underline using Core Graphics. You can go reasonably crazy here, but for our style we stick to a line of dots.
Note: Make sure to set your stroke colour for drawing. We read a colour value for the
.underlineColor
attribute from the text store, although you could access aUIColor
value directly if preferred.
Using your layout manager
We have our custom layout manager that can render our custom underline style, but now we need to associate it with our UITextView
. Associating a custom layout manager with a text view is a bit… convoluted. There’s not much to say here other than its a bit of a monstrosity and if anyone knows of a simpler way feel free to give a shout. 😬
Now that the text view is setup we can underline using our custom style in the same way you would use the native underline styles.
Note: If your text view has editing enabled, you will need to track editing events and adjust your underline ranges to match the new text where needed, otherwise your underlines will fall out of sync with the text they are meant to underline.
Conclusion
This is one part to reimplementing text views alternative interpretation suggestions. Next time we’ll discuss how to extend UITextView
to present dictation suggestions and letting the user select alternative suggestions.