Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

ios

swift

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.

like image 711
Gruntcakes Avatar asked Dec 26 '17 21:12

Gruntcakes


1 Answers

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)
    }
}
like image 155
Iurie Manea Avatar answered Sep 22 '22 05:09

Iurie Manea