How to Create UILabel With HTML Tags in UIKit and SwiftUI

Written by maxkalik | Published 2022/08/14
Tech Story Tags: ios | uikit | swiftui | html | swiftprogramming | ios-development | uilabel | ios-app-development

TLDRUIKit and SwiftUI are creating a UILabel that handles basic HTML tags. It must work in both frameworks: UIKit or SwiftUI in the same way. Link attributes should have normal and active states: normal, active, and disabled. All links should be configurable with colors for several states: normal***, active***, and***visited***. In the mobile world, there is a bit of a different vision, so we are going to have 3 states:***normal*** ,***active*** and***inactive***.via the TL;DR App

Some time ago, I got a very specific task: to create UILabel that handles basic HTML tags <b>, <i>, <u>, <a>, etc. The links should have normal and active states. It must work in both frameworks: UIKit and SwiftUI in the same way. The result should look like this:

TL;DR

GitHub: https://github.com/maxkalik/ExtendedLabel

The plan

In this article, I’d like to describe step-by-step how to make this feature, so bear with me and enjoy the process :). But before diving into coding let’s describe the task in detail:

  1. UILabel might have links and it should also understand the HTML tags: <a>, <b>, <i> with links.
  2. Tap gesture action on a link should be implemented with delegate or closure.
  3. All links should be configurable with colors for several states: normal, active, and disabled.
  4. Attributed UILabel should behave as a normal UILabel. This means the label has to have all basic properties like: textColornumberOfLinesfontSizetextAlignmentlineBreakMode, etc. They should work as expected even with HTML.

Before, implementing this feature we need a plan. Let’s break it into four steps:

  1. Implementing attributed UILabel with tappable links.
    – Creating models with attributed string
    – Creating UILabel with basic methods
    – Tap Location
    – Touch states
  2. Extending UILabel to understand HTML tags.
    enumerateAttributes to determine links in the attributed string
  3. Combining two implementations: the tappable links with HTML tags.
    – Concat andaddLinkmethods
    – Styling of links and text
    – UIFontDescriptor
  4. Integrating UILabel as a UIViewRepresentable for SwiftUI.
    – UIViewRepresentable with MutatingWrapper
    – Dynamic Height
    – Expandable view

UILabel with tappable links

We are not going to reinvent the wheel, so let’s borrow a ready-to-use solution from the ancient TTTAttributedLabel. Basically, we need to start with link attributes. What are the attributes? As we know from the web perspective the links have 4 states: normalhoveractive, and visited. In the mobile world, there is a bit of a different vision, so we are going to have 3 states: normalactive, and inactive.

As you can see we cannot have such states as hover or visited but additionally, we added an inactive state. So, to make the links tappable with states we are going to create a model called LinkAttributes. All the states basically are described as NSAttributedString  dictionary.

struct LinkAttributes {
  var attributes: [NSAttributedString.Key: Any]
  var activeAttributes: [NSAttributedString.Key: Any]
  var inactiveAttributes: [NSAttributedString.Key: Any]
}

UILabel should show text with links. The text also is going to be NSAttributedString. So let’s make another struct to cover all cases:

struct AttributedTextWithLink {
  var text: String
  var attributes: [NSAttributedString.Key: Any]
  var link: String?
  var linkAttributes: LinkAttributes?
}

As you can see, the attributes will be our styles for text.

Also, we’re going to have a special struct for HTML links. Let’s call it UniversalLabelLink with two properties: linkAttributes (see below), and NSTextCheckingResult.

struct UniversalLabelLink {
  var linkAttributes: LinkAttributes
  var textCheckingResult: NSTextCheckingResult
}

NSTextCheckingResult will perform a special service for our links. Looking ahead we going to use it for storing the links' string ranges and their URLs.

All these three models will help us to implement UILabel with tappable links and visual states. Let’s create the UILabel. If you remember the task, the label should have a tap delegate or closure for the links.

As you can see we prepared a basic configuration for the UILabel. So let’s take a look at the setup part:

In the setup part of UniversalLabel, we have a general function setupAttributes which just sets attributedText for a specific range. Yes, it will be our links. This function will use our two other functions which will prepare our states: normal and active.

That was easy. Now it is going to be more interesting. We need to make links tappable. Somehow on tap on a particular link, it should be found in the text. Let’s take a look at the code snippet:

On tap gesture, the sender UITapGestureRecognizer gives us a location of the touch on the view. It means we can get the point CGPoint and based on this identify the link: the range of text in the string. But how? So, the scene is taken by NSTextStorage and NSLayoutManager.

Let’s take a look at the last guy and its apple documentation:

“An object that coordinates the layout and display of text characters”.

Eventually, we need an index of that tapped character in the string.

Using the NSTextContainer configuration on the frame of view we can get this index. This means these 3 functions will direct us to the link, so we can identify them. This means we can easily add our link state overriding the touches methods:

UILabel with HTML tags

Since UILabel can have links in the text and identifies these links on tap we can start with HTML tags. Converting HTML strings to the text is pretty easy using, again NSAttributedString. Let’s take a look at this code snippet:


All we need is to make an attributed string from the string data with a specific option document type as HTML. It will allow us to use a cool method in an attributed string:

func enumerateAttributes(
  in enumerationRange: NSRange,
  options opts: NSAttributedString.EnumerationOptions = [],
  using block: ([NSAttributedString.Key : Any], NSRange, UnsafeMutablePointer<ObjCBool>
) -> Void)

In the completion of this function, we need a couple of things: attributes and range. So we can identify each link in the text using the attributes option, like this:

if let link = attributes[.link], let url = link as? URL { ... }

At this point, we know almost everything about our HTML, so let’s add this property to our UniversalLabel.

Combining tappable links with HTML tags

The last step is to finalize our UIKit component. To combine and use all implementations which we have done above, we need to extend our label with several methods.

Concat is a general method that takes the TextWithLinks struct and builds an attributed string with links. All the pieces of strings have their own attributes. Additionally, this method stores each link in the array. It’s needed to get a particular link config after identifying the link from the text and then to use link attributes and URL on tap.

The other two methods are all about styling. I would say it is the most tricky part. Basically, all the links and other pieces of the string should be substituted with the attributed strings with new styles.

For example, the normal link color is configured from the outside, using textColor property of UILabel. The link of active state (while pressing on the link) has the same textColor but with alpha, or it also could be configured separately from the outside.

The function prepareAttributedTextWithLinks identifies the link and normal text and adjusts the styles for them. From the textAttributes, we can catch underline style from HTML and set them to the text attributes as [.underlineStyle].

So it means UILabel with HTML will be with links and underlined text. But what about <b>, <i> HTML tags? You cannot catch them so easily from attributes.

It was one of the most surprising parts to handle italic and bold text from HTML. I thought it would be the same story as with [.underlineStyle] but it is not. How to solve it?

To catch italic and bold styles we need to know about the font from NSAttributedString. UIFont itself has an interesting part called UIFontDescriptor — a collection of attributes that describes a font. From this collection, we can get some information including about italic font style.

To know about font-weight we have even to dive deeper using the same UIFontDescriptor. The weight could be fetched from UIFont.Weight using rawValue as CGFloat.

As you can see in the implementation of prepareTextAttributes we substitute the UIFont and eventually return the new paragraph style that we are using in our enumerateAttributes block:

Implementing SwiftUI expandable view with our new UILabel

The UniversalLabel is done and we can use it in UIKit, but I got a task to use this label in SwiftUI as well which complicates some things.

Let’s start with an obvious part and create UIViewRepresentable. In our case, we need to prepare two methods + mutatingWrapper which stores for us UILabel’s attributes like fontSize, textAlignment, numberOfLines, etc. We’re going to use them from the SwiftUI view.

Also, this view has to have a set of methods to mutate mutatingWrapper:

Let’s prepare and test the HTML string with all tags which I got in the task:

let html = """
<h1>Message Title</h1>
<p>The message has <a href="http://maxkalik.com">HTML link</a>. The text supports <i>italic format</i> and <u>underlined style</u> and <b>bold text</b>. All this message could be wrapped in paragraph tag and can include <a href="http://apple.com">multiple links<a/>.</p>
"""

As you can see the HTML string has even <h1> tag. I supposed it should be handled as well. But regarding SwiftUI implementation I thought the code could be like this, only a few lines:

I was so happy to see that it worked! All basic HTML tags work correctly and even the header. But wait, the links are not tappable! After some time I figured out why. Let’s take a look at the height of the Label view. The height is not the same as the text block height. So, we need somehow to know the height of the Label dynamically right from the UIViewRepresentable.

For this reason, we need this binding property called dynamicHeight.

@Binding var dynamicHeight: CGFloat

It’s going to bind the height value from self with the height state in the SwfitUI View.

The result will be like this:

At the beginning of the article, I presented the result with an expandable message component which shows us how easy to use the label and how flexible it is. So the final SwiftUI implementation looks like this:

I’m not going to go through all this code line by line. I just would like to highlight one important thing. Now it is clear how the height state bound from the Label works in this situation. We even don’t need to have something like onPreferenceChange with ViewSizePreferenceKey to read the height of the view. Everything happens inside of the UIViewRepresentable.

We got a universal solution — UILabel with tappable and configurable links. The label perfectly understands HTML tags and it behaves as on the web, even more, the links are still tappable and configurable with the states normal and active. It can be used in UIKit and SwiftUI.

Conclusion

During the time of slow transitioning from UIKit to SwiftUI sometimes we expect some native ready-to-use solution. SwiftUI already understands markdown and HTML but it is only from iOS 15 and it is even with limited usage. The task is getting more complicated if we need such a feature for UIKit and SwiftUI with the same behavior.

To build UILabel like this at the first sight seems a trivial task but when we develop it, all the time we encounter problems: taping on a link, HTML configuration, font styling, etc.

This article is not only for engineers, it is also useful for managers because it shows how deep we — developers can go just to make some small feature and how much time it could take.

I will appreciate it if you come up with any suggestions in the comments, and also feel free to fork the repo as well.

GitHub: https://github.com/maxkalik/ExtendedLabel

Thanks for reading! 🚀


Also published here.


Written by maxkalik | iOS Developer. Triumph contributor. Launched WordDeposit, Simple Ruler Apps.
Published by HackerNoon on 2022/08/14