How to remove extra padding when converting HTML to NSAttributedString
Table of Contents
We learned from How to display HTML in UILabel and UITextView a way to convert HTML string to an NSAttributedString for using in UILabel or UITextView.
You might not be aware that sometimes, you will get a bottom padding at the end of your result NSAttributedString. Learn what is causing this and how to mitigate the problem.
Problem when converting HTML string to NSAttributedString
When you convert HTML string to NSAttributedString, sometimes, you get an extra unwanted bottom padding. Here is how the problem looks like.
I paint UILabel with a pink background to show its bounds.
And here is the code that produce above result.
let label1 = UILabel()
label1.backgroundColor = .systemPink
label1.numberOfLines = 0
let label2 = UILabel()
label2.backgroundColor = .systemPink
label2.numberOfLines = 0
let label3 = UILabel()
label3.backgroundColor = .systemPink
label3.numberOfLines = 0
// 1
let text1 = "This is a <b>bold</b> text."
let text2 = "<p>This is a <b>bold</b> text.</p>"
let text3 = "<div>This is a <b>bold</b> text.</div>"
// 2
label1.attributedText = text1.htmlAttributedString()
label2.attributedText = text2.htmlAttributedString()
label3.attributedText = text3.htmlAttributedString()
// 3
extension String {
func htmlAttributedString() -> NSAttributedString? {
let htmlTemplate = """
<!doctype html>
<html>
<head>
<style>
body {
font-family: -apple-system;
font-size: 24px;
}
</style>
</head>
<body>
\(self)
</body>
</html>
"""
guard let data = htmlTemplate.data(using: .utf8) else {
return nil
}
guard let attributedString = try? NSAttributedString(
data: data,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil
) else {
return nil
}
return attributedString
}
}
1 Our example HTML strings.
2 We convert HTML string to NSAttributedString and assign them to each label. I leave out layout/constraint code for brevity.
3 .htmlAttributedString()
is a helper method that convert HTML string to NSAttributedString. You can read more about it here.
Can you guess the cause of the extra padding?
You can easily support sarunw.com by checking out this sponsor.
Translate your app In 1 click: Simplifies app localization and helps you reach more users.
Cause of the problem
This problem is caused when two conditions are met.
- Your HTML string contains an element with a
block
display behavior, such asdiv
andp
. - You have multiple lines UILabel (
numberOfLines = 0
).
Your HTML string contains an element with a block display behavior
Block is a display behavior in HTML which displays an element that starts on a new line and would take up the whole width.
From mozilla.org
block
The element generates a block element box, generating line breaks both before and after the element when in the normal flow.
UIKit does a good job preserving this semantic and adding a new line (\n
) and space for each display block element.
This example has multiple paragraphs (<p>
), and having a new line after each paragraph makes our text more readable.
let loremString = """
<p>Lorem ipsum dolor sit amet, qui eu scripta liberavisse. Ei est case fastidii apeirian, an possim regione expetenda sed.</p>
<p>Est at alii solum. Per unum elit ad. At mel everti habemus. Munere philosophia id mea, omnes postea reprimique ne eum.</p>
<p>Euismod inimicus sea ne, pri tota senserit ut.</p>
"""
label.attributedText = loremString.htmlAttributedString()
It is good to know this is expected behavior, but you might still want to remove the last paragraph's padding anyway.
You have multiple lines UILabel
Since the problem is about a new line character, this problem only shows up when you have UILabel that supports multiple lines (numberOfLines = 0
).
How to remove unwanted bottom padding
There are many ways to tackle this problem, but I will tell you two solutions that I know so far.
- Remove a new line from NSAttributedString.
- Solve it with CSS.
Remove new line from NSAttributedString
The first solution is straightforward. Since we know that the extra bottom padding at the end is a new line character, we can write a code to remove it from our NSAttributedString.
Here is one way of doing it.
extension NSAttributedString {
func trimmedAttributedString() -> NSAttributedString {
let nonNewlines = CharacterSet.whitespacesAndNewlines.inverted
// 1
let startRange = string.rangeOfCharacter(from: nonNewlines)
// 2
let endRange = string.rangeOfCharacter(from: nonNewlines, options: .backwards)
guard let startLocation = startRange?.lowerBound, let endLocation = endRange?.lowerBound else {
return self
}
// 3
let range = NSRange(startLocation...endLocation, in: string)
return attributedSubstring(from: range)
}
}
1 Find first non-whitespace character and new line character.
2 Find last non-whitespace character and new line character.
3 Getting range out of locations. This trim out leading and trailing whitespaces and new line characters.
Then use it like this.
label.attributedText = loremString.htmlAttributedString()?.trimmedAttributedString()
CSS
As you can see in our helper method, we can apply CSS style by modifying <style>
in the htmlTemplate
.
extension String {
func htmlAttributedString() -> NSAttributedString? {
let htmlTemplate = """
<!doctype html>
<html>
<head>
<style>
body {
font-family: -apple-system;
font-size: 24px;
}
</style>
</head>
<body>
\(self)
</body>
</html>
"""
...
}
}
An extra newline character (\n
) is added only for a block display element. To fix this with CSS, we have to change the display
property of the last p
tag to a non-block display type.
extension String {
func htmlAttributedString() -> NSAttributedString? {
// 1
let htmlTemplate = """
<!doctype html>
<html>
<head>
<style>
body {
font-family: -apple-system;
font-size: 24px;
}
p:last-child {
display: inline;
}
</style>
</head>
<body>
\(self)
</body>
</html>
"""
guard let data = htmlTemplate.data(using: .utf8) else {
return nil
}
guard let attributedString = try? NSAttributedString(
data: data,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil
) else {
return nil
}
return attributedString
}
}
1 We add a new CSS p:last-child
which will matches last p
tag and set display
to inline
.
The CSS selector I use here only removes the new line for the last p
tag. If you have a different HTML, you might need to modify it to suit your need.
p:last-child {
display: inline;
}
You can easily support sarunw.com by checking out this sponsor.
Translate your app In 1 click: Simplifies app localization and helps you reach more users.
Conclusion
When converting HTML string to NSAttributedString, ** all display block elements such as div
and p
will produce an extra newline at the end**. I have shown you two ways to mitigate this problem, CSS and trimming method.
Using CSS requires less code but might require some CSS knowledge to create the right selector for your content. On the other hand, trimming method requires more code, but you don't have to know about CSS or the content of your HTML string.
Read more article about UIKit, HTML, or see all available topic
Enjoy the read?
If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.
Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.
If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.
Become a patron Buy me a coffee Tweet ShareHow to make a SwiftUI view to fill its container width and height
We can use a frame modifier to make a view appear to be full width and height, but the result might not be what you expected.
How to add custom fonts to iOS app
In iOS, you can add and use a custom font to your app and use it like other assets. The process is not hard, but it might not be straightforward as adding an image. Let's learn how to do it.