Implementing Custom Underline Styles in iOS

Shaun Merchant
Brill.app
Published in
5 min readFeb 28, 2019

--

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.

Native iOS Keyboard Dictation alternative suggestions.

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.

The native blue dashed underline style.
The underline styles provided by NSAttributedString.

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 a UIColor 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. 😬

Cry.

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.

The output from using our custom layout manager.

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.

◾️

--

--