Reputation: 37590
I want to change the color of a link within a UILabel. I've found loads of past questions on how to do this for a UITextView, and past questions with answers in Obj-C (but can't translate these to Swift as properties that did exist in Obj-c no longer do such as NSMutableAttributedString.linkTextAttribtues for example). But I cannot find how to do this for a UILabel and in Swift 4.
Upvotes: 9
Views: 12903
Reputation: 51
Using attachments as said Iurie Manea is a good idea. You can't change the link appearance in the attributed string. So you can use attachments instead.
Here is a protocol and class handling tap
import UIKit
@objc
protocol LinkTapHandling: AnyObject
{
@objc
func handleTap(_ gesture: UITapGestureRecognizer)
}
protocol LinkLabelSetup
{
var linkTapHandler: LinkTapHandling { get }
func setupLabelWithLinks(
label: UILabel,
text: String,
font: UIFont,
textColor: UIColor,
linkColor: UIColor
)
}
extension LinkLabelSetup
{
func setupLabelWithLinks(
label: UILabel,
text: String,
font: UIFont,
textColor: UIColor,
linkColor: UIColor
)
{
label.attributedText = attributedStringWithLinks(from: text, font: font, textColor: textColor, linkColor: linkColor)
label.isUserInteractionEnabled = true
label.addGestureRecognizer(UITapGestureRecognizer(target: linkTapHandler, action: #selector(LinkTapHandling.handleTap(_:))))
}
func attributedStringWithLinks(from text: String, font: UIFont, textColor: UIColor, linkColor: UIColor) -> NSAttributedString
{
let attributedString = NSMutableAttributedString(string: text)
// Применяем базовые атрибуты ко всему тексту
let baseAttributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: textColor
]
attributedString.addAttributes(baseAttributes, range: NSRange(location: 0, length: text.utf16.count))
// Регулярное выражение для поиска URL
let urlPattern = #"(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)"#
guard let regex = try? NSRegularExpression(pattern: urlPattern, options: .caseInsensitive) else {
return attributedString
}
let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count))
for match in matches {
let range = match.range
if let url = URL(string: (text as NSString).substring(with: range)) {
var linkAttributes: [NSAttributedString.Key: Any] = [
.attachment: url,
.foregroundColor: linkColor,
.underlineStyle: NSUnderlineStyle.single.rawValue,
.underlineColor:linkColor
]
// Отключаем системные стили для ссылок
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byWordWrapping
linkAttributes[.paragraphStyle] = paragraphStyle
attributedString.addAttributes(linkAttributes, range: range)
}
}
return attributedString
}
}
class LinkTapHandler: NSObject, LinkTapHandling
{
@objc
func handleTap(_ gesture: UITapGestureRecognizer)
{
guard let label = gesture.view as? UILabel,
let attributedText = label.attributedText else { return }
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: .zero)
let textStorage = NSTextStorage(attributedString: attributedText)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
let labelSize = label.bounds.size
textContainer.size = labelSize
let locationOfTouchInLabel = gesture.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(
x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y
)
let locationOfTouchInTextContainer = CGPoint(
x: locationOfTouchInLabel.x - textContainerOffset.x,
y: locationOfTouchInLabel.y - textContainerOffset.y
)
let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
attributedText.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributedText.length), options: []) { value, range, _ in
if NSLocationInRange(indexOfCharacter, range), let url_ = value as? URL {
var urlToOpen = url_.absoluteString
if !urlToOpen.lowercased().hasPrefix("http://") && !urlToOpen.lowercased().hasPrefix("https://") {
urlToOpen = "https://" + urlToOpen
}
if let url = URL(string: urlToOpen) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}
}
}
Usage:
import UIKit
class ExampleView: UIVIew, LinkLabelSetup
{
lazy var linkTapHandler: LinkTapHandling = LinkTapHandler()
@IBOutlet weak var messageLabel: BaseLabel!
// your code
func updateView()
{
let text = "An SKScene object apple.com represents a scene of content in SpriteKit. A scene is the root node in a tree of SpriteKit nodes (SKNode). These nodes provide content that the scene animates and renders for display. To display a scene, you present it from an SKView, SKRenderer, or WKInterfaceSKScene."
setupLabelWithLinks(
label: messageLabel,
text: text,
font: .systemFont(ofSize: 12),
textColor: .black,
linkColor: .red
)
messageLabel.sizeToFit()
}
}
Upvotes: 0
Reputation: 1084
The answers above are correct but setting .attachment
as the url doesn't open the url, at least not for me (using iOS 13).
The color of .link
is not affected by the .foregroundColor
in NSAttributedString
, but from the tintColor
of your UITextView
let urlAttributes: [NSAttributedString.Key: Any] = [
.link: URL(string:"https://google.com"),
.foregroundColor: textColor,
.underlineColor: textColor,
.underlineStyle: NSUnderlineStyle.single.rawValue
.underlinColor: UIColor.green
]
textView.tintColor = UIColor.green
textView.attributed = urlAttributes
should set the text and the underline of the link to green
Upvotes: 1
Reputation: 1236
For default NSAttributedString.Key.link color will be blue.
If you need custom colors for links you can set the attribute as NSAttributedString.Key.attachment instead of .link and set the foreground and underline colors like this:
let linkCustomAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 14),
NSAttributedString.Key.foregroundColor: UIColor.red,
NSAttributedString.Key.underlineColor: UIColor.magenta,
NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
NSAttributedString.Key.attachment: URL(string: "https://www.google.com")] as [NSAttributedString.Key : Any]
If you need to handle touches on links you can use this custom label class:
import UIKit
public protocol UILabelTapableLinksDelegate: NSObjectProtocol {
func tapableLabel(_ label: UILabelTapableLinks, didTapUrl url: String, atRange range: NSRange)
}
public class UILabelTapableLinks: UILabel {
private var links: [String: NSRange] = [:]
private(set) var layoutManager = NSLayoutManager()
private(set) var textContainer = NSTextContainer(size: CGSize.zero)
private(set) var textStorage = NSTextStorage() {
didSet {
textStorage.addLayoutManager(layoutManager)
}
}
public weak var delegate: UILabelTapableLinksDelegate?
public override var attributedText: NSAttributedString? {
didSet {
if let attributedText = attributedText {
textStorage = NSTextStorage(attributedString: attributedText)
findLinksAndRange(attributeString: attributedText)
} else {
textStorage = NSTextStorage()
links = [:]
}
}
}
public override var lineBreakMode: NSLineBreakMode {
didSet {
textContainer.lineBreakMode = lineBreakMode
}
}
public override var numberOfLines: Int {
didSet {
textContainer.maximumNumberOfLines = numberOfLines
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
private func setup() {
isUserInteractionEnabled = true
layoutManager.addTextContainer(textContainer)
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = lineBreakMode
textContainer.maximumNumberOfLines = numberOfLines
}
public override func layoutSubviews() {
super.layoutSubviews()
textContainer.size = bounds.size
}
private func findLinksAndRange(attributeString: NSAttributedString) {
links = [:]
let enumerationBlock: (Any?, NSRange, UnsafeMutablePointer<ObjCBool>) -> Void = { [weak self] value, range, isStop in
guard let strongSelf = self else { return }
if let value = value {
let stringValue = "\(value)"
strongSelf.links[stringValue] = range
}
}
attributeString.enumerateAttribute(.link, in: NSRange(0..<attributeString.length), options: [.longestEffectiveRangeNotRequired], using: enumerationBlock)
attributeString.enumerateAttribute(.attachment, in: NSRange(0..<attributeString.length), options: [.longestEffectiveRangeNotRequired], using: enumerationBlock)
}
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let locationOfTouch = touches.first?.location(in: self) else {
return
}
textContainer.size = bounds.size
let indexOfCharacter = layoutManager.glyphIndex(for: locationOfTouch, in: textContainer)
for (urlString, range) in links where NSLocationInRange(indexOfCharacter, range) {
delegate?.tapableLabel(self, didTapUrl: urlString, atRange: range)
return
}
}
}
Setup label in your code:
customLabel.attributedText = <<Your attributed text with custom links>>
customLabel.delegate = self
Implement delegate:
extension YourClass: UILabelTapableLinksDelegate {
func tapableLabel(_ label: UILabelTapableLinks, didTapUrl url: String, atRange range: NSRange) {
print("didTapUrl: ", url)
}
}
Upvotes: 26
Reputation: 173
It is easier to use UITextView instead of UILabel and write something like:
textView.linkTextAttributes = [
.foregroundColor: UIColor.red,
.underlineColor: UIColor.red,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
Upvotes: 3