Swift-UILabel implements user agreement graphics and text mixing

foreword

I recently changed jobs and was a bit busy, so I had less and less time to take notes. Regarding the article in the RxSwift column, the author will take time to record it later. Today I mainly share a requirement that the author encountered, and decided to encapsulate it during the implementation process, hoping to help friends in need. \

Requirement: Realize the effect inside the red frame

image.png

Require:

  • The button supports clicking to select and invert
  • Protocol Support Click
  • support newline
  • Also support clicks when truncated
  • Graphic Center Keep Consistent

analyze:

The first 3 points are not difficult, just use the button label to combine them. The latter two points require the use of rich text, because the truncation situation is not fixed, for example:

image.png

or

image.png

Both of these are not easy to handle, and the processing of truncation cannot be achieved with the button.

accomplish

process pictures

Pictures are easier to deal with. Convert the picture into rich text and splicing it with the content behind it. It should be noted here that the center point of the picture should be aligned with the text behind it. The alignment can be handled by baseline offset (the offset is the following text)

            let baselineOffset = (largeFont.lineHeight - normalFont.lineHeight)/2.0 + (largeFont.descender - normalFont.descender)
            muAttributedString.addAttributes([NSAttributedString.Key.font: normalFont,
                                              NSAttributedString.Key.baselineOffset: baselineOffset],
                                             range: NSRange(location: normalImgAttrString.length, length: muAttributedString.length - normalImgAttrString.length))
复制代码

get copy area

Usually what we want is to use this method directly to get

layoutManager.boundingRect(forGlyphRange: , in: )
复制代码

This method can solve the first case, but it cannot handle the later truncation case.

Looking at the API of layoutManager, I found that there is a method to deal with truncation

open func enumerateEnclosingRects(forGlyphRange glyphRange: NSRange, withinSelectedGlyphRange selectedRange: NSRange, in textContainer: NSTextContainer, using block: @escaping (CGRect, UnsafeMutablePointer<ObjCBool>) -> Void)
复制代码

Enumerates the enclosing rectangle of the glyphRange in the textContainer. If a selection extent is given in the second argument, the returned rectangle will be used correctly to draw the selection.

From the official notes, it can be found that all areas of the text in a given range are enumerated. In this way, all text ranges can be covered by the first rectangle and the last rectangle, as shown in the figure below:

image.png

Implementation code:

func rectFor(string str : String, fromIndex: Int = 0) -> (CGRect, CGRect)?
    {
        // Find the range of the string
        guard self.text != nil else { return nil }
        let subStringToSearch : NSString = (self.text! as NSString).substring(from: fromIndex) as NSString
        var stringRange = subStringToSearch.range(of: str)
        if (stringRange.location != NSNotFound)
        {
            guard self.attributedText != nil else { return nil }
            // Add the starting point to the sub string
            stringRange.location += fromIndex
            let storage = NSTextStorage(attributedString: self.attributedText!)
            let layoutManager = NSLayoutManager()
            storage.addLayoutManager(layoutManager)
            let textContainer = NSTextContainer(size: self.frame.size)
            textContainer.lineFragmentPadding = 0
            textContainer.lineBreakMode = .byWordWrapping
            layoutManager.addTextContainer(textContainer)
            var glyphRange = NSRange()
            
            layoutManager.characterRange(forGlyphRange: stringRange, actualGlyphRange: &glyphRange)
            var firstWordRect = true
            var rect1 = CGRectZero
            var rect2 = CGRectZero
            layoutManager.enumerateEnclosingRects(forGlyphRange: glyphRange, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 1), in: textContainer) { wordRect, isStop in
                if firstWordRect {
                    rect1 = wordRect
                    firstWordRect = false
                }
                
                rect2 = wordRect
            }
            
            return (rect1, rect2)
        }
        return nil
    }
复制代码

click processing

@objc
    private func click(_ gesture: UITapGestureRecognizer) {
        
        guard let dict = callBackMap else {
            return
        }
        
        let point = gesture.location(in: self)
        for key in dict.keys {
            let (rect1, rect2) = rectFor(string: key)!
            let imgString = selected ? selectedImgAttrString : normalImgAttrString
            
            if containsPoint(minRect: rect1, maxRect: key == imgString.string ? CGRect(x: 0, y: 0, width: largeFont.pointSize, height: largeFont.pointSize):rect2, point: point) {
                
                if key == imgString.string {
                    selected = !selected
                }
                if let callBack = dict[key] {
                    callBack()
                }
            }
        }
        
    }
复制代码

Summarize

The author encapsulates an extension of UILabel, which is convenient to use. The complete demo has been placed on github, interested friends can download it and have a look

Precautions:

It is best to keep the size of the picture and largefont consistent, otherwise the baseline offset needs to be handled specially, and this situation is annotated in the code

image.png


renew

question:

If there are multiple repeated agreements, the first one can be clicked, and the subsequent repeated agreements do not support clicks

solve:

1. Text color: When adding a protocol, traverse all the text to get all the ranges of the string, so as to modify the text color.
2. Click processing: also traverse all the ranges of the string until the clicked position falls within the range area, or the query fails without responding

Periodic summary

When the author encounters problems, he will try his best to solve them, and he also welcomes friends to raise the problems they encounter, so that we can learn and make progress together.
Thanks to my little partner season_zhu for the tip, the demo link has been updated.
Support pod use:
pod 'UILabelImageText'

Guess you like

Origin juejin.im/post/7224532739885187130