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 commentsBe the first to share your opinion
Ali Hassan
a year 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
4 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