How I make "...more" feature like instagram by using UILabel

73 views Asked by At

I am currently developing an iOS application and facing a challenge with UILabel. I wish to achieve two main objectives:

  1. Limit the display of text within a UILabel to only two lines.
  2. Append "...More" at the end of the text to indicate that there is additional content available.

Now I made a calculating logic below: but it take too much time to calculating so screen is slightly flicker.

import UIKit

extension UILabel {
    func getMaxHeight(font: UIFont, width: CGFloat) -> CGFloat {
        let maxSize = CGSize(width: width, height: CGFloat(MAXFLOAT))
        let text = (self.text ?? "") as NSString
        let textHeight = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil).height
        return textHeight
    }
    
    func getNumberOfLines(font: UIFont, width: CGFloat) -> Int {
        let maxSize = CGSize(width: width, height: CGFloat(MAXFLOAT))
        let text = (self.text ?? "") as NSString
        let textHeight = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil).height
        let lineHeight = font.lineHeight
        
        return Int(ceil(textHeight / lineHeight))
    }
    
    private func getNumberOfLines(text: String, font: UIFont, width: CGFloat) -> Int {
        let maxSize = CGSize(width: width, height: CGFloat(MAXFLOAT))
        let text = text as NSString
        let textHeight = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil).height
        let lineHeight = font.lineHeight
        
        return Int(ceil(textHeight / lineHeight))
    }
    
    func replaceEllipsis(with string: String, font: UIFont, width: CGFloat, maxLine: Int, highlight: String? = nil) {
        guard let text = self.text else { return }
        
        lineBreakMode = .byClipping
        if self.getNumberOfLines(font: font, width: width) <= maxLine {
            return
        }
        
        let totalNumberOfLine = getNumberOfLines(text: text, font: font, width: width)
        let charArray: [Character] = text.map { $0 }
        
        let chunkSize = charArray.count / totalNumberOfLine
        var resultArray: [String] = []
        
        for i in 0..<totalNumberOfLine {
            let startIndex = i * chunkSize
            let endIndex = min((i + 1) * chunkSize, charArray.count)
            let chunk = charArray[startIndex..<endIndex]
            let chunkString = chunk.reduce("") { partialResult, char in
                return partialResult + "\(char)"
            }
            resultArray.append(chunkString)
        }
        
        let textForTwo = resultArray[0..<maxLine+1].joined()
        var estimatedText = textForTwo
        
        var estimatedLine: Int = 10
        
        repeat {
            estimatedText.removeLast()
            self.text = estimatedText
            self.text?.append(string)
            estimatedLine = self.getNumberOfLines(font: font, width: width)
        } while estimatedLine > maxLine
        
        estimatedText.append(string)
        let attributedString = NSMutableAttributedString(string: estimatedText)
        
        if let highlight = highlight {
            let length = highlight.count
            
            let lastIndex = estimatedText.index(estimatedText.endIndex, offsetBy: -length)
            let startPoint = estimatedText.distance(from: estimatedText.startIndex, to: lastIndex)
            
            let range = NSRange(location: startPoint, length: length)
            
            attributedString.addAttribute(.foregroundColor, value: UIColor.gray, range: range)
            
            self.text = ""
            self.attributedText = attributedString
        }
    }
}
1

There are 1 answers

0
Abishek Thangaraj On

Please refer this for read more and read less.

import UIKit

class ContentCreateViewController: UIViewController {
  
    let readMoreText = " ...Read More"
    let readLessText = " ...Read Less"
    let text = "Are you looking to level up your UIImage manipulation skills in Swift? Today, let's dive into a handy extension that allows you to create UIImage instances with customized background colors. This extension can be incredibly useful for various applications, such as creating dynamic user interfaces or generating visually appealing graphics."

    lazy var labelContent: UILabel! = {
        
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.textColor = .black
        label.font = .systemFont(ofSize: 16)
        label.numberOfLines = 0
        label.text = text
        return label
        
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        addLabelContent()
        // Do any additional setup after loading the view.
    }
    
}

extension ContentCreateViewController {
    
    func addLabelContent(){
        
        view.backgroundColor = .white
        view.addSubview(labelContent)
        
        NSLayoutConstraint.activate([
            labelContent.topAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            labelContent.leadingAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
            labelContent.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            labelContent.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
        addReadMoreString()
    }
    
}

extension ContentCreateViewController{
    
    func addReadMoreString() {

        let lengthForString = text.count

        if lengthForString >= 30 {
            let lengthForVisibleString = 256
            let trimmedString = text.prefix(lengthForVisibleString)
            
            let readMoreLength = readMoreText.count
            let trimmedForReadMore = String(trimmedString.prefix(trimmedString.count - readMoreLength))
            
            let answerAttributed = NSMutableAttributedString(string: trimmedForReadMore, attributes: [.font: labelContent.font ?? UIFont.systemFont(ofSize: 16)])

            let readMoreAttributed = NSMutableAttributedString(string: readMoreText, attributes: [.font: UIFont.systemFont(ofSize: 16), .foregroundColor: UIColor.red])

            answerAttributed.append(readMoreAttributed)
            labelContent.attributedText = answerAttributed

            let readMoreGesture = UITapGestureRecognizer(target: self, action: #selector(readMoreDidClickedGesture(_:)))
            readMoreGesture.numberOfTapsRequired = 1
            labelContent.addGestureRecognizer(readMoreGesture)
            labelContent.isUserInteractionEnabled = true
            
        } else {
            print("No need for 'Read More'...")
        }
        
    }

    @objc func readMoreDidClickedGesture(_ gesture: UITapGestureRecognizer) {
        let readMoreRange = ((labelContent.text ?? "") as NSString).range(of: readMoreText)
        // comment for now
        let readLessRange = ((labelContent.text ?? "") as NSString).range(of: readLessText)
        
        if gesture.didTapAttributedTextInLabel(label: labelContent, inRange: readMoreRange) {
            let answerAttributed = NSMutableAttributedString(string: text, attributes: [.font: labelContent.font ?? UIFont.systemFont(ofSize: 16)])
            
            let readMoreAttributed = NSMutableAttributedString(string: readLessText, attributes: [.font: UIFont.systemFont(ofSize: 16), .foregroundColor: UIColor.red])
            
            answerAttributed.append(readMoreAttributed)
            labelContent.attributedText = answerAttributed
            print("Read More")
        }else if gesture.didTapAttributedTextInLabel(label: labelContent, inRange: readLessRange) {
            addReadMoreString()
            print("Read Less")
        } else {
            print("Tapped none")
        }
    }

}

extension UITapGestureRecognizer {

    func didTapAttributedTextInLabel(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 = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
        //(labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);
        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 = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
        // locationOfTouchInLabel.y - textContainerOffset.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)
    }
}

Output

enter image description here