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 6, 2021

let textDescription: String = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

To collapase content with Readmore
textLabel.appendReadmore(after: textDescription, trailingContent: .readmore)
To expand content with ReadLess
textLabel.appendReadLess(after: textDescription, trailingContent: .readless)

readmore:less

@LAPTop4iK
Copy link

LAPTop4iK commented Jun 12, 2021

Hi.
I liked the simplicity and clarity of your solution. But I'm just a beginner and a little confused. It seems that I do everything just like you, but I don't understand how the action is called. My label just doesn't respond to clicks. How does the setup work highlight "read more" and "read less" on tap?
Can I see your test(demo) project?

@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