在社交类 APP 中 @、# 符号构成的标记文本已经形成了某种通用的意义:前者表示通知某位好友,而后者表示为某个话题或者分类。这些标记文本一般还都带有高亮显示和可点击的特点。接下来的我会创建一个 UITextView 的子类 AttrTextView 来实现上诉功能。
开始
上码的代码首先声明了一个 wordType 的枚举类型,该类用用于对标示文本进行类型标记。接着我们定义了自定义类型 AttrTextView,并且声明了三个属性。textString 表示原文本,attrString 进行属性设置后的文本,callBack 为 # 和 @ 标记文本的点击事件回调。
文本设置
定义好属性后,我们就需要考虑使用接口的实现了。正常情况下文本应该有以下属性需要设置:常规文本的字体、颜色;# 和 @ 标记文本各自对应的字体和颜色;点击事件设置以及回调函数。代码如下:
- public func setText(text: String,normalColor: UIColor,hashtagColor: UIColor,mentionColor: UIColor,normalFont: UIFont,hashTagFont: UIFont,mentionFont: UIFont,tapCallBack callBack: @escaping (String,wordType) -> Void) {
- self.callBack = callBack
- self.attrString = NSMutableAttributedString(string: text)
- self.textString = NSString(string: text)
- // Set initial font attributes for our string
- // 设置字体和文本颜色
- attrString?.addAttribute(NSFontAttributeName,value: normalFont,range: NSRange(location: 0,length: (textString?.length)!))
- attrString?.addAttribute(NSForegroundColorAttributeName,value: normalColor,length: (textString?.length)!))
- // Call a custom set Hashtag and Mention Attributes Function
- // 设置 #、@ 的高亮色等属性
- setAttrWithName(attrName: "Hashtag",wordPrefix: "#",color: hashtagColor,text: text,font: hashTagFont)
- setAttrWithName(attrName: "Mention",wordPrefix: "@",color: mentionColor,font: mentionFont)
- // Add tap gesture that calls a function tapRecognized when tapped
- // 添加手势
- let tapper = UITapGestureRecognizer(target: self,action: #selector(self.tapRecognized(tapGesture:)))
- addGestureRecognizer(tapper)
- }
上面代码中的 setAttrWithName 函数的目的是对 #、@ 标记文本的属性进行设置,代码如下:
- private func setAttrWithName(attrName: String,wordPrefix: String,color: UIColor,text: String,font: UIFont) {
- // Words can be separated by either a space or a line break
- // 将文本按照空格和 \n 键拆分为单词数组
- var words: [String] = []
- let wordtext: [String] = text.components(separatedBy: " ")
- for var word in wordtext {
- if word.hasPrefix("\n") {
- word = word.replacingOccurrences(of: "\n",with: "")
- }
- words.append(word)
- }
- // 便利数组,检查是否满足条件并进行属性设置
- for word in words.filter({$0.hasPrefix(wordPrefix)}) {
- let range = textString!.range(of: word)
- attrString?.addAttribute(NSForegroundColorAttributeName,value: color,range: range)
- attrString?.addAttribute(attrName,value: 1,range: range)
- attrString?.addAttribute("Clickable",range: range)
- attrString?.addAttribute(NSFontAttributeName,value: font,range: range)
- }
- self.attributedText = attrString
- }
点击事件的处理
文本点击的处理稍微有点麻烦,需要考虑多种情况:
没有点击在任何文本上
点击在普通文本
点击在标示文本,并且需要识别标示文本的类型
- func tapRecognized(tapGesture: UITapGestureRecognizer) {
- var wordString: String? // The String value of the word to pass into callback function
- var char: NSAttributedString! //The character the user clicks on. It is non optional because if the user clicks on nothing,char will be a space or " "
- var word: NSAttributedString? //The word the user clicks on
- var isHashtag: AnyObject?
- var isAtMention: AnyObject?
- // Gets the range of the character at the place the user taps
- // 检查用户点击字符的范围
- let point = tapGesture.location(in: self)
- let charPosition = closestPosition(to: point)
- guard let charRange = tokenizer.rangeEnclosingPosition(charPosition!,with: .character,inDirection: 1) else {
- return
- }
- let location = offset(from: beginningOfDocument,to: charRange.start)
- let length = offset(from: charRange.start,to: charRange.end)
- let attrRange = NSMakeRange(location,length)
- char = attributedText.attributedSubstring(from: attrRange)
- // If the user has not clicked on anything,exit the function
- if char.string == " "{
- print("User clicked on nothing")
- return
- }
- // Checks the character's attribute,if any
- // 检查属性标示
- isHashtag = char?.attribute("Hashtag",at: 0,longestEffectiveRange: nil,in: NSMakeRange(0,char!.length)) as AnyObject?
- isAtMention = char?.attribute("Mention",char!.length)) as AnyObject?
- // Gets the range of the word at the place user taps
- // 获得点击单词的范围
- let wordRange = tokenizer.rangeEnclosingPosition(charPosition!,with: .word,inDirection: 1)
- /*
- 单词的范围在下面两种情况下为 nil:
- 1. 点击在 "#" or "@" 标示上
- 2. 没有点击在任何字符上。但是这种情况在上面的代码中已经排除了,所有只剩下 1
- */
- if wordRange != nil {
- let wordLocation = offset(from: beginningOfDocument,to: wordRange!.start)
- let wordLength = offset(from: wordRange!.start,to: wordRange!.end)
- let wordAttrRange = NSMakeRange(wordLocation,wordLength)
- word = attributedText.attributedSubstring(from: wordAttrRange)
- wordString = word!.string
- } else {
- /*
- 右移12像素后再获取单词
- */
- var modifiedPoint = point
- modifiedPoint.x += 12
- let modifiedPosition = closestPosition(to: modifiedPoint)
- let modifedWordRange = tokenizer.rangeEnclosingPosition(modifiedPosition!,inDirection: 1)
- if modifedWordRange != nil {
- let wordLocation = offset(from: beginningOfDocument,to: modifedWordRange!.start)
- let wordLength = offset(from: modifedWordRange!.start,to: modifedWordRange!.end)
- let wordAttrRange = NSMakeRange(wordLocation,wordLength)
- word = attributedText.attributedSubstring(from: wordAttrRange)
- wordString = word!.string
- }
- }
- if let stringToPass = wordString {
- // 点击回掉函数
- if isHashtag != nil && callBack != nil {
- callBack!(stringToPass,wordType.hashtag)
- } else if isAtMention != nil && callBack != nil {
- callBack!(stringToPass,wordType.mention)
- }
- }
- }
上面的代码处理中,首先使用 .character 检查点击位置的字符,并对无效区域的点击进行了处理。这里之所以使用 .character 而不是后面的 .word 的原因是:后者会将 @、# 这些标示符丢弃,导致一只类似点击到无效区域的情形。当上诉检查通过也就是点击区域有效的时候,我们使用 .word,获取点击区域的单词。为了应对前面标示点击的情形,当区域无效的时候,我们右移12个像素后再获取单词。最后我们根据文本不同类型进行对应处理。
最后
最后我们看一下简单使用示例代码:
- let attrView = AttrTextView.init(frame: CGRect.init(x: 0,y: 64,width: view.bounds.size.width,height: view.bounds.size.height - 64),textContainer: nil)
- self.view.addSubview(attrView)
- attrView.setText(text: "#PHP 是不是世界上最好的语言? @all ",normalColor: .black,hashtagColor: .red,mentionColor: .blue,normalFont: UIFont.systemFont(ofSize: 10),hashTagFont: UIFont.systemFont(ofSize: 14),mentionFont: UIFont.systemFont(ofSize: 14)) { word,wordType in
- print(word)
- }