See More label tap issue

I have created ReadMoreLabel class to add at the end of given line. I have few issues currently as we have 2 different language to manage (English and Japanese).

  1. Sometimes it’s hard to tap on Japanese language
  2. If we have next line character after just one character, ...ReadMore is not attached to label.

Here is the code for the same.

public final class ReadMoreLabel: UILabel {
    
    override public var text: String? {
        willSet(newValue) {
            self.originalText = newValue
        }
    }
    
    private var originalText:String?
    private var trailingText = "read_more".localized
    var didTapReadMore: (() -> Void)?
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    private func addTapGesture() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(tap:)))
        self.addGestureRecognizer(tapGesture)
        self.isUserInteractionEnabled = true
    }
    
    public func addTrailingText(
        trailingText: String = "...",
        textToAppend: String,
        fontOfTextToAppend: UIFont,
        colorOfTextToAppend: UIColor
    ) {
        let readMoreText = trailingText + textToAppend
        // dont do anything if its frame is not set
        guard self.frame != CGRect.zero, self.text?.isEmpty == false else{
            return
        }
        self.trailingText = textToAppend
        self.originalText = self.text
        let errorPadding = 4
        // first get the string that can be displayed in the required number of lines
        guard self.numberOfLines != 0,
              let lengthForVisibleString = self.visibleTextLength(),
              let unwrappedText = self.text, (lengthForVisibleString < unwrappedText.count),
              (lengthForVisibleString < unwrappedText.count), unwrappedText.count > (readMoreText.utf16.count + errorPadding)
        else {
            return
        }
        
        var startIndex = unwrappedText.index(unwrappedText.startIndex, offsetBy: lengthForVisibleString)
        var range = startIndex..<unwrappedText.endIndex
        // string without read more text that can be displayed
        let strTrimmedWithoutReadMore = unwrappedText.replacingCharacters(in: range, with: "")
        var startIndexOffset = strTrimmedWithoutReadMore.count - readMoreText.utf16.count - errorPadding
        if startIndexOffset < 0 {
            startIndexOffset = 0
        }
        startIndex = unwrappedText.index(unwrappedText.startIndex, offsetBy: startIndexOffset)
        let endIndex = unwrappedText.index(startIndex, offsetBy: readMoreText.utf16.count + errorPadding)
        range = startIndex..<endIndex
        
        var strTrimmedWithReadMode = strTrimmedWithoutReadMore

        // Check if the range is within the bounds of the string
        if startIndex >= strTrimmedWithoutReadMore.startIndex && endIndex <= strTrimmedWithoutReadMore.endIndex {
            strTrimmedWithReadMode = strTrimmedWithoutReadMore.replacingCharacters(in: range, with: "") + "..."
        } else {
            strTrimmedWithReadMode += "..."
        }
        let answerAttributed = NSMutableAttributedString(string: strTrimmedWithReadMode, attributes: [.font: self.font ?? UIFont.font(type: .regular, size: 12)])
        
        // string with characters trimmed for read more string
        let attributes: [NSAttributedString.Key: Any] = [
            .font: self.font ?? UIFont.font(type: .regular, size: 12),
            .underlineStyle: NSUnderlineStyle.single.rawValue
        ]
        
        // add read more attribtuted string
        let readMoreAttributed = NSMutableAttributedString(string: textToAppend, attributes: attributes)
        answerAttributed.append(readMoreAttributed)
        self.attributedText = answerAttributed
        addTapGesture()
    }
    
    @objc func handleTap(tap: UITapGestureRecognizer) {
        guard let unwrappedText = self.text, let range = unwrappedText.range(of: self.trailingText) else{
            return
        }
        let nsRange = NSRange(location: range.lowerBound.utf16Offset(in: unwrappedText), length: range.upperBound.utf16Offset(in: unwrappedText) - range.lowerBound.utf16Offset(in: unwrappedText))
        guard self.didTapAttributedText(locationFromTapGesture: tap.location(in: self), range: nsRange) else{
            return
        }
        self.text = originalText
        self.didTapReadMore?()
    }
    
    // returns the last index of the string that can be added
    private func visibleTextLength() -> Int? {
        guard let unwrappedText = self.text, let unwrappedFont = self.font, unwrappedText.isEmpty == false else {
            return nil
        }

        let lineBreakMode = NSLineBreakMode.byTruncatingTail
        let width = self.frame.size.width

        // Determine the target size as in how much can fit
        let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)

        let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: unwrappedFont]
        let attributedText = NSAttributedString(string: unwrappedText, attributes: attributes)

        let boundingRect = attributedText.boundingRect(with: size, options: .usesLineFragmentOrigin, context: nil)

        // Compute the number of lines based on the target size
        let totalNumberOfLines = Int(ceil(boundingRect.height / font.lineHeight))
        guard totalNumberOfLines > self.numberOfLines else {
            return unwrappedText.utf16.count
        }

        var index = unwrappedText.unicodeScalars.startIndex
        var prev = unwrappedText.unicodeScalars.startIndex
        _ = CharacterSet.whitespacesAndNewlines
        let endIndex = unwrappedText.unicodeScalars.endIndex
        // Iterate through the string and for each word check if it can be added to the required size of the string or not
        repeat {
            prev = index

            let unwrappedIndex = index

            let startIndex = unwrappedText.unicodeScalars.index(after: unwrappedIndex)
            _ = startIndex..<unwrappedText.unicodeScalars.endIndex

            if lineBreakMode == NSLineBreakMode.byCharWrapping {
                index = unwrappedText.index(after: unwrappedIndex)
            } else {
                 index = unwrappedText.unicodeScalars.index(after: unwrappedIndex)
            }

        } while (isHeightGreaterThanLabelHeight(indexOne: index, indexTwo: endIndex, text: unwrappedText, targetSize: size, attributes: attributes))

        return prev.utf16Offset(in: unwrappedText)
    }

    private func isHeightGreaterThanLabelHeight(indexOne: String.Index?, indexTwo: String.Index?, text: String, targetSize: CGSize, attributes: [NSAttributedString.Key: Any]) -> Bool {
        guard let unwrappedIndexOne = indexOne, let unwrappedIndexTwo = indexTwo else {
            return false
        }

        guard unwrappedIndexOne.utf16Offset(in: text) < unwrappedIndexTwo.utf16Offset(in: text) else {
            return false
        }

        let substring = String(text[...unwrappedIndexOne])
        let boundingRect = substring.boundingRect(with: targetSize, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)

        let totalNumberOfLines = Int(ceil(boundingRect.size.height / font.lineHeight))
        return totalNumberOfLines <= self.numberOfLines
    }
    
    private func didTapAttributedText(locationFromTapGesture: CGPoint, range: NSRange) -> Bool {
        guard let attributedText = self.attributedText else {
            return false
        }

        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: CGSize(width: bounds.width, height: .greatestFiniteMagnitude))
        let textStorage = NSTextStorage(attributedString: attributedText)

        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)

        textContainer.lineFragmentPadding = 0
        textContainer.lineBreakMode = .byWordWrapping
        textContainer.maximumNumberOfLines = numberOfLines

        let numberOfGlyphs = layoutManager.numberOfGlyphs
        var glyphRange = NSRange()

        for index in 0..<numberOfGlyphs {
            layoutManager.lineFragmentRect(forGlyphAt: index, effectiveRange: &glyphRange)
            let characterRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)

            if NSIntersectionRange(characterRange, range).length > 0 {
                let location = layoutManager.location(forGlyphAt: index)
                let boundingRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
                let adjustedRect = boundingRect.offsetBy(dx: 0, dy: location.y-8)

                if adjustedRect.contains(locationFromTapGesture) {
                    return true
                }
            }
        }

        return false
    }

}

Can someone help me to find out the issue or have a better solution for it?

Recognized by Mobile Development Collective

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật