Skip to content

Instantly share code, notes, and snippets.

@Catherine-K-George
Created June 6, 2021 09:49
Show Gist options
  • Save Catherine-K-George/3bffbf1433b34b630b7bc572d6af9f6d to your computer and use it in GitHub Desktop.
Save Catherine-K-George/3bffbf1433b34b630b7bc572d6af9f6d to your computer and use it in GitHub Desktop.
UILabel extension with ReadMore/ ReadLess action
import UIKit
enum TrailingContent {
case readmore
case readless
var text: String {
switch self {
case .readmore: return "...Read More"
case .readless: return " Read Less"
}
}
}
extension UILabel {
private var minimumLines: Int { return 4 }
private var highlightColor: UIColor { return .red }
private var attributes: [NSAttributedString.Key: Any] {
return [.font: self.font ?? .systemFont(ofSize: 18)]
}
public func requiredHeight(for text: String) -> CGFloat {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: frame.width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = minimumLines
label.lineBreakMode = NSLineBreakMode.byTruncatingTail
label.font = font
label.text = text
label.sizeToFit()
return label.frame.height
}
func highlight(_ text: String, color: UIColor) {
guard let labelText = self.text else { return }
let range = (labelText as NSString).range(of: text)
let mutableAttributedString = NSMutableAttributedString.init(string: labelText)
mutableAttributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: range)
self.attributedText = mutableAttributedString
}
func appendReadmore(after text: String, trailingContent: TrailingContent) {
self.numberOfLines = minimumLines
let fourLineText = "\n\n\n"
let fourlineHeight = requiredHeight(for: fourLineText)
let sentenceText = NSString(string: text)
let sentenceRange = NSRange(location: 0, length: sentenceText.length)
var truncatedSentence: NSString = sentenceText
var endIndex: Int = sentenceRange.upperBound
let size: CGSize = CGSize(width: self.bounds.width, height: CGFloat.greatestFiniteMagnitude)
while truncatedSentence.boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: attributes, context: nil).size.height >= fourlineHeight {
if endIndex == 0 {
break
}
endIndex -= 1
truncatedSentence = NSString(string: sentenceText.substring(with: NSRange(location: 0, length: endIndex)))
truncatedSentence = (String(truncatedSentence) + trailingContent.text) as NSString
}
self.text = truncatedSentence as String
self.highlight(trailingContent.text, color: highlightColor)
}
func appendReadLess(after text: String, trailingContent: TrailingContent) {
self.numberOfLines = 0
self.text = text + trailingContent.text
self.highlight(trailingContent.text, color: highlightColor)
}
}
@Catherine-K-George
Copy link
Author

Catherine-K-George commented Jun 13, 2021

Hi,

You've to add a tap gesture to the label to catch the tap and an extension UITapGesture to know which part of the label has tapped.

Step 1: Enable UILabel user interaction.

textLabel.isUserInteractionEnabled = true

Step 2: Add Tap gesture to UILabel and set action

@IBAction func didTapLabel(_ sender: UITapGestureRecognizer) { }

Step 3: Add an extension UITapGestureRecognizer to identify which part of the label has tapped, readmore/readless.

extension UITapGestureRecognizer {

    func didTap(label: UILabel, inRange targetRange: NSRange) -> Bool {
        
        // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: CGSize.zero)
        let textStorage = NSTextStorage(attributedString: label.attributedText!)

        // Configure layoutManager and textStorage
        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)

        // Configure textContainer
        textContainer.lineFragmentPadding = 0.0
        textContainer.lineBreakMode = label.lineBreakMode
        textContainer.maximumNumberOfLines = label.numberOfLines
        let labelSize = label.bounds.size
        textContainer.size = labelSize

        // Find the tapped character location and compare it to the specified range
        let locationOfTouchInLabel = self.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)
        return NSLocationInRange(indexOfCharacter, targetRange)
    }

}

Step 4: Handle the readmore/readless in tap action

 @IBAction func didTapLabel(_ sender: UITapGestureRecognizer) {
        guard let text = textLabel.text else { return }

        let readmore = (text as NSString).range(of: TrailingContent.readmore.text)
        let readless = (text as NSString).range(of: TrailingContent.readless.text)
        if sender.didTap(label: textLabel, inRange: readmore) {
            textLabel.appendReadLess(after: textDescription, trailingContent: .readless)
        } else if  sender.didTap(label: textLabel, inRange: readless) {
            textLabel.appendReadmore(after: textDescription, trailingContent: .readmore)
        } else { return }
        
    }

Try this!

https://github.com/Catherine-K-George/Readmore-Readless.git

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment