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
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:
or
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:
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
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'