Scrungepipes
Scrungepipes

Reputation: 37590

How can you change the color of links in a UILabel?

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

Answers (4)

Alex
Alex

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

Marat Ibragimov
Marat Ibragimov

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

Iurie Manea
Iurie Manea

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

Maxvale
Maxvale

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

Related Questions