how do I detect and make clickable links in a UILabel NOT using UITextView
Asked Answered
O

1

14

I am building a chat app and for performance reasons I need to use UILabel's instead of UITextView's to display the chat messages. I have previously used TextView's but with data detection on the scrolling is very slow and choppy.

The problem is there is currently no link/phone/address etc... detection for UILabels.

How can I know where a link or phone number exists in a string, and then highlight and make it clickable within a UILabel?

I have read many articles on how to add attributes for links to do just that but they have all been links which you know the range or substring of.

I would like to take any string and find out whether is contains links and where those links are and then add the tapGestureRecognizer to the label and perform actions based on where the tap occurred.

I have tried to incorporate an external library (TTTAttributedLabel) but I'm using swift and found the documentation for swift to be limited. I did manage to import the library but the links are not being automatically detected.

Ovule answered 15/2, 2017 at 4:19 Comment(0)
F
8

Reference source -

Create tap-able "links" in the NSAttributedString of a UILabel?

It is Converted into swift 4.0

Try this -

Create a sub class for UILabel like below -

Swift 4.0

class CustomLabel: UILabel {

let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
var textStorage = NSTextStorage() {
    didSet {
        textStorage.addLayoutManager(layoutManager)
    }
}
var onCharacterTapped: ((_ label: UILabel, _ characterIndex: Int) -> Void)?

let tapGesture = UITapGestureRecognizer()

override var attributedText: NSAttributedString? {
    didSet {
        if let attributedText = attributedText {
            textStorage = NSTextStorage(attributedString: attributedText)
        } else {
            textStorage = NSTextStorage()
        }
    }
}
override var lineBreakMode: NSLineBreakMode {
    didSet {
        textContainer.lineBreakMode = lineBreakMode
    }
}

override var numberOfLines: Int {
    didSet {
        textContainer.maximumNumberOfLines = numberOfLines
    }
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setUp()
}

override init(frame: CGRect) {
    super.init(frame: frame)
    setUp()
}
func setUp() {
    isUserInteractionEnabled = true
    layoutManager.addTextContainer(textContainer)
    textContainer.lineFragmentPadding = 0
    textContainer.lineBreakMode = lineBreakMode
    textContainer.maximumNumberOfLines = numberOfLines
    tapGesture.addTarget(self, action: #selector(CustomLabel.labelTapped(_:)))
    addGestureRecognizer(tapGesture)
}

override func layoutSubviews() {
    super.layoutSubviews()
    textContainer.size = bounds.size
}

@objc func labelTapped(_ gesture: UITapGestureRecognizer) {
    guard gesture.state == .ended else {
        return
    }
    let locationOfTouch = gesture.location(in: gesture.view)
    let textBoundingBox = layoutManager.usedRect(for: textContainer)
    let textContainerOffset = CGPoint(x: (bounds.width - textBoundingBox.width) / 2 - textBoundingBox.minX,
                                      y: (bounds.height - textBoundingBox.height) / 2 - textBoundingBox.minY)
    let locationOfTouchInTextContainer = CGPoint(x: locationOfTouch.x - textContainerOffset.x, y: locationOfTouch.y - textContainerOffset.y)
    let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer,
                                                        in: textContainer,  fractionOfDistanceBetweenInsertionPoints: nil)

    onCharacterTapped?(self, indexOfCharacter)
 }

}

Within your viewDidLoad method of View controller create an instance of that class like below -

    override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.

    let label = CustomLabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(label)
    view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-[view]-|",
                                                       options: [], metrics: nil, views: ["view" : label]))
    view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-[view]-|",
                                                       options: [], metrics: nil, views: ["view" : label]))

    let attributedString = NSMutableAttributedString(string: "String with a link", attributes: nil)
    let linkRange = NSMakeRange(14, 4); // for the word "link" in the string above

    let linkAttributes: [NSAttributedStringKey : AnyObject] = [
        NSAttributedStringKey.foregroundColor : UIColor.blue, NSAttributedStringKey.underlineStyle : NSUnderlineStyle.styleSingle.rawValue as AnyObject,
        NSAttributedStringKey.link: "http://www.apple.com" as AnyObject ]
    attributedString.setAttributes(linkAttributes, range:linkRange)

    label.attributedText = attributedString

    label.onCharacterTapped = { label, characterIndex in

        // DO YOUR STUFF HERE
    }
}
Fireproof answered 15/2, 2017 at 4:51 Comment(6)
you are adding a link in your viewDidLoad() which you know the range of to be (14, 4). In my case, I don't know the range of the link. That is what I want to determine.Ovule
As per my knowledge, If you want to achieve this using UILable for that you should know the range otherwise you should go with UITextView.Fireproof
In other words, this is a two-part questions: (1) how to add clickable links in UILabel? The answer told you. (2) how to find the location of links in run-time? The answer is to use a parser. For example, if your source is HTML based, you can use Soup library. Let me provide you more details laterLavena
This doesn't seem to work. characterIndex always returns 0 no matter where label is tapped. label.onCharacterTapped = { label, characterIndex in print(characterIndex) }Diwan
The following line returns 0 each time: let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)Diwan
@OnurŞahindur Thanks.Fireproof

© 2022 - 2024 — McMap. All rights reserved.