在社交类 APP 中 @、# 符号构成的标记文本已经形成了某种通用的意义:前者表示通知某位好友,而后者表示为某个话题或者分类。这些标记文本一般还都带有高亮显示和可点击的特点。接下来的我会创建一个 UITextView 的子类 AttrTextView 来实现上诉功能。
开始
import UIKit
enum wordType{
case hashtag // #标示文本类型
case mention // @标示文本类型
}
//自定义视图用于高亮 # 和 @ 之后的文本(效果类似于微博、twitter),并添加点击事件
class AttrTextView: UITextView {
var textString: NSString?
var attrString: NSMutableAttributedString?
var callBack: ((String, wordType) -> Void)?
...
}
上码的代码首先声明了一个 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, range: NSRange(location: 0, 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, text: text, 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", value: 1, 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", at: 0, longestEffectiveRange: nil, in: NSMakeRange(0, 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!, with: .word, 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)
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。