SwiftUI Text with Clickable Links
In this article, I will show you how to create a custom SwiftUI component TextWithClickableLinks
, that finds and makes any links clickable inside the given text input.
Note: This implementation uses AttributedString, which requires iOS15+
Detecting URLs
In a helper file DetectURLs.swift
, create a function detectURLs
with the following code:
import SwiftUI
/// Detects URLs in the given text and splits it into parts.
func detectURLs(in text: String) -> [(String, URL?)] {
let types: NSTextCheckingResult.CheckingType = .link
let detector = try? NSDataDetector(types: types.rawValue)
let matches = detector?.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count)) ?? []
var results: [(String, URL?)] = []
var lastIndex = text.startIndex
for match in matches {
if let range = Range(match.range, in: text) {
if range.lowerBound > lastIndex {
let nonLinkText = String(text[lastIndex..<range.lowerBound])
results.append((nonLinkText, nil))
}
let linkText = String(text[range])
results.append((linkText, match.url))
lastIndex = range.upperBound
}
}
if lastIndex < text.endIndex {
let remainingText = String(text[lastIndex..<text.endIndex])
results.append((remainingText, nil))
}
return results
}
This function will detect any URLs in the text input and returns a list containing the original string, split up in parts:
let input = "Visit isak.me for more posts on SwiftUI!"
let output = [("Visit ", nil), ("isak.me", Optional("http://isak.me")), (" for more posts on SwiftUI!", nil)]
Rendering the text and styling the links
To create a view rendering the text with stylized and clickable links, we utilize AttributedString. AttributedString
is a value type for a string with associated attributes for portions of its text. We create a computed variable attributedString
, which uses our previously created detectURLs
function, and loops through the result, giving appropriate attribute to any links found along the way.
private var attributedString: AttributedString {
var attributedString = AttributedString(text)
let parts = detectURLs(in: text)
for part in parts {
if let url = part.1, let range = attributedString.range(of: part.0) {
attributedString[range].link = url
attributedString[range].foregroundColor = .blue
attributedString[range].underlineStyle = .single
}
}
return attributedString
}
This attributedString
variable can be rendered in a normal Text
view.
Text(attributedString)
Usage
I’ve added maxFrameWidth
and padding
to the component to allow for more more flexible styling of the view.
The entire component ends up looking like the following:
import SwiftUI
/// A view that displays text with clickable links.
/// Clickable links are only supported on iOS 15 and later.
struct TextWithClickableLinks: View {
let text: String
var customFont: Font
var multilineTextAlignment: TextAlignment
var maxFrameWidth: CGFloat?
var padding: CGFloat
var body: some View {
Text(attributedString)
.multilineTextAlignment(multilineTextAlignment)
.frame(maxWidth: maxFrameWidth)
.padding(padding)
}
private var attributedString: AttributedString {
var attributedString = AttributedString(text)
let parts = detectURLs(in: text)
for part in parts {
if let url = part.1, let range = attributedString.range(of: part.0) {
attributedString[range].link = url
attributedString[range].foregroundColor = .blue
attributedString[range].underlineStyle = .single
}
}
return attributedString
}
}
#Preview {
TextWithClickableLinks(
text: "Visit isak.me/blog for more posts on SwiftUI!",
multilineTextAlignment: .center,
maxFrameWidth: 300,
padding: 16
)
}