Codementor Events

Hyperlink Label

Published Jan 03, 2019

Clickable Label is very popular in iOS, especially in Login, Register screen. You can easily see some text like this:

By register, I agree to Terms of Service and Private Policy

This is how I make this label.

Define your texts

Make sure the text you need to make clickable is exacly same to the full text.

let termText = "By register, I agree to ... Terms of Service and Private Policy"
let term = "Terms of Service"
let policy = "Private Policy"

Format the Label

let termLabel = UILabel()
let formattedText = String.format(strings: [term, policy],
                                    boldFont: UIFont.boldSystemFont(ofSize: 15),
                                    boldColor: UIColor.blue,
                                    inString: termText,
                                    font: UIFont.systemFont(ofSize: 15),
                                    color: UIColor.black)
termLabel.attributedText = formattedText
termLabel.numberOfLines = 0
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTermTapped))
termLabel.addGestureRecognizer(tap)
termLabel.isUserInteractionEnabled = true
termLabel.textAlignment = .center

String.format is an extension from my code collection. This is the full function.

extension String {
    static func format(strings: [String],
                    boldFont: UIFont = UIFont.boldSystemFont(ofSize: 14),
                    boldColor: UIColor = UIColor.blue,
                    inString string: String,
                    font: UIFont = UIFont.systemFont(ofSize: 14),
                    color: UIColor = UIColor.black) -> NSAttributedString {
        let attributedString =
            NSMutableAttributedString(string: string,
                                    attributes: [
                                        NSAttributedStringKey.font: font,
                                        NSAttributedStringKey.foregroundColor: color])
        let boldFontAttribute = [NSAttributedStringKey.font: boldFont, NSAttributedStringKey.foregroundColor: boldColor]
        for bold in strings {
            attributedString.addAttributes(boldFontAttribute, range: (string as NSString).range(of: bold))
        }
        return attributedString
    }
}

Handle Label Tap Gesture

I get the tap location in the Label and check if this location belongs to term or policy text range.

@objc func handleTermTapped(gesture: UITapGestureRecognizer) {
    let termString = termText as NSString
    let termRange = termString.range(of: term)
    let policyRange = termString.range(of: policy)

    let tapLocation = gesture.location(in: termLabel)
    let index = termLabel.indexOfAttributedTextCharacterAtPoint(point: tapLocation)

    if checkRange(termRange, contain: index) == true {
        handleViewTermOfUse()
        return
    }

    if checkRange(policyRange, contain: index) {
        handleViewPrivacy()
        return
    }
}

Supported code

  • Check if a range contain an index
func checkRange(_ range: NSRange, contain index: Int) -> Bool {
    return index > range.location && index < range.location + range.length
}
  • Get index from a point in UILabel
extension UILabel {
    func indexOfAttributedTextCharacterAtPoint(point: CGPoint) -> Int {
        assert(self.attributedText != nil, "This method is developed for attributed string")
        let textStorage = NSTextStorage(attributedString: self.attributedText!)
        let layoutManager = NSLayoutManager()
        textStorage.addLayoutManager(layoutManager)
        let textContainer = NSTextContainer(size: self.frame.size)
        textContainer.lineFragmentPadding = 0
        textContainer.maximumNumberOfLines = self.numberOfLines
        textContainer.lineBreakMode = self.lineBreakMode
        layoutManager.addTextContainer(textContainer)

        let index = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        return index
    }
}

Demo

And result is:

You can download the source code here

Conclusion

You can make a custom UILabel to be easier to reuse. I leave that for you. If you have any issues with this, let me know.

Enjoy coding.

Discover and read more posts from Ky Nguyen
get started
post comments5Replies
Ali Hassan
2 years ago

thanks Nguyen for this piece of useful content

Wolfgang Neikes
3 years ago

Great example, thank you for that!

There’s only one thing I would like to mention / ask. In your „checkRange“ function you are testing for index < range.location + range.length, but IMHO this will always return false.

If you change it to index <= range.location + range.length it should work as expected.

Am I wrong?

Miles Montana
5 years ago

I have had problems with special fonts an alignment.
So, here is an optimized indexOfAttributedTextCharacterAtPoint procedure:
func indexOfAttributedTextCharacterAtPoint(point: CGPoint) -> Int {
assert(self.attributedText != nil, “This method is developed for attributed string”)
guard let attributedString = self.attributedText else { return -1 }

    let mutableAttribString = NSMutableAttributedString(attributedString: attributedString)
    // Add font so the correct range is returned for multi-line labels
    mutableAttribString.addAttributes([NSAttributedString.Key.font: font], range: NSRange(location: 0, length: attributedString.length))
    
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.alignment = textAlignment
    mutableAttribString.addAttributes([.paragraphStyle: paragraphStyle], range: NSMakeRange(0, mutableAttribString.string.count))
    
    let textStorage = NSTextStorage(attributedString: mutableAttribString)
    
    let layoutManager = NSLayoutManager()
    textStorage.addLayoutManager(layoutManager)
    
    let textContainer = NSTextContainer(size: frame.size)
    textContainer.lineFragmentPadding = 0
    textContainer.maximumNumberOfLines = numberOfLines
    textContainer.lineBreakMode = lineBreakMode
    textContainer.size = bounds.size
    
    layoutManager.addTextContainer(textContainer)
    
    let index = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    return index
}
Show more replies