Mentions in UITextView using Proton Editor

Rajdeep Kwatra
5 min readAug 26, 2022

--

Mentioning someone is one of the most common feature in social/work apps these days, and it looks a trivial task to implement the same too — or at least, one would hope so. In a typical app, there would be a UITextView in which users may type the content, and also mention someone. However, as you might know, to get a simple mention in a UITextView is not as simple after all. This is one of the motivations of extending the Open-Source library, Proton to add support for a better background than what is provided by iOS natively.

Let’s see what options do iOS provides and what can we build on top of it to not only make user experience better, but developer experience too.

Plain vanilla Mentions

The simplest approach to adding a specific UI treatment for mentions in UITextView would be to use NSAttributedString with background attribute. The code looks pretty simple to add background to given range of text:

let mention = NSAttributedString(string: "@rajdeep", attributes: [
.backgroundColor: UIColor.systemBlue,
.foregroundColor: UIColor.white
])
editor.attributedText = mention

However, as you can see, it looks pretty bland and there’s not much that we can do with just attributes. For instance, at the very least, it would be desirable to have rounded corners for the mention text. Unfortunately, it is not doable without really changing how backgrounds are drawn in NSLayoutManager.

Extending UITextView and NSAttributedString

Mentions is one of initial requirements that I looked at when I started working on Proton. I always felt that background style in NSAttributedString is severely limited in terms of what developers can do with it and hence I developed my own version of background style attribute, appropriately called backgroundStyle in Proton. Proton just extends a UITextView and NSAttributedString to add powerful new features, and a more intuitive API for existing features.

Let’s see how we can utilize backgroundStyle along with a few other attributes to build a perfect mention:

Starting off

Just like background example above, we can use backgroundStyle to get exact same output:

let mention = NSAttributedString(string: "@rajdeep", attributes: [
.backgroundStyle: BackgroundStyle(color: .systemBlue),
.foregroundColor: UIColor.white
])
editor.attributedText = mention

Adding style

Unlike background attribute though, it is just as simple to add rounding to the backgroundStyle:

let mention = NSAttributedString(string: "@rajdeep", attributes: [
.backgroundStyle:
BackgroundStyle(color: .systemBlue,
roundedCornerStyle: .relative(percent: 50)),
.foregroundColor: UIColor.white
])
editor.attributedText = mention

roundedCornersStyle may be a relative percentage of height of text to get perfectly rounded corners irrespective of text size, or it can have a absolute value for corner radius.

Not only that, if you wish, you can add also shadow and borders:

let mention = NSAttributedString(string: "@rajdeep", attributes: [
.backgroundStyle:
BackgroundStyle(color: .systemBlue,
roundedCornerStyle: .relative(percent: 50),
border: BorderStyle(lineWidth: 1, color: .systemRed),
shadow: ShadowStyle(color: .systemGray,
offset: CGSize(width: 1, height: 1),blur: 2)),
.foregroundColor: UIColor.white
])
editor.attributedText = mention

What about editing

Editing text with attributes like that for mentions bring along a whole new set of issues because of how attributes are treated in UITextView.

Bleeding attributes

This is one of the issues you might not realise until you see one. When a set of attributes are applied to string, these are automatically added to typingAttributes. This means, if you have applied background or backgroundStyle or for that matter any other attribute, it will automatically be carried forward in the following range:

Of course, this is not desirable. This can be fixed very easily in Proton by using another attribute called lockedAttributes. Any attribute that is not desired to be carry forward (in this case, backgroundStyle and foregroundColor), can be added to lockedAttribute to prevent bleeding these into the following range.

let mention = NSAttributedString(string: "@rajdeep", attributes: [
.backgroundStyle:
BackgroundStyle(color: .systemBlue,
roundedCornerStyle: .relative(percent: 50)),
.foregroundColor: UIColor.white,
.lockedAttributes:
[
NSAttributedString.Key.foregroundColor,
.backgroundStyle
]
])
editor.attributedText = mention

Partial deletion

Another issue that this implementation would show is, that user can delete part of the mention. This is because it’s still just text in the UITextView which is editable.

A typical requirement would be that if a part of mention is deleted, the entire mention should be removed. This can be solved by adding yet another attribute called textBlock to the range. This makes the entire range of text with this attribute act as a single block i.e. you can only delete it as a single unit, and you can not have the cursor anywhere in the middle of it:

let mention = NSAttributedString(string: "@rajdeep", attributes: [
.backgroundStyle:
BackgroundStyle(color: .systemBlue,
roundedCornerStyle: .relative(percent: 50)),
.foregroundColor: UIColor.white,
.lockedAttributes:
[
NSAttributedString.Key.foregroundColor,
.backgroundStyle
],
.textBlock: true
])
editor.attributedText = mention

Wrapping

As with any other part of text, a mention may also be long enough that it wraps to next line. By default, it will show up as 2 capsules.

However, we may want to show the wrapping as a continuity. This can be achieved by providing another argument, hasSquaredOffJoins to backgroundStyle. This ensures that at for all the continuous ranges, having wrapped text, the inner edges would be squared off giving an impression that it is continuing from previous line:

let mention = NSAttributedString(string: "@rajdeep", attributes: [
.backgroundStyle:
BackgroundStyle(color: .systemBlue,
roundedCornerStyle: .relative(percent: 50),
hasSquaredOffJoins: true),
.foregroundColor: UIColor.white,
.lockedAttributes:
[
NSAttributedString.Key.foregroundColor,
.backgroundStyle
],
.textBlock: true
])
editor.attributedText = mention

And as simple as that, we have a mention that works just as we expect in an editable text view.

--

--

Rajdeep Kwatra

Rajdeep is an iOS developer at Atlassian. He believes that a piece of code can always be improved, but the cost may not always be justified against the benefits