iOS UITextView 点击文字、文字折行、富文本等使用
iOS 对于文本的处理已经有了很好的封装,对于富文本的需求也做了不少的工作。尤其是使用NSAttributedString,可以很好的完成大部分的工作。但是我早期做的项目中,有那么一个需求,就是当用户需要点击特定的文字,并完成相应的任务。比如:在一个英语测试的App里,有一个功能叫找错,就是一英文里面有一个或几个单词是错误的需要点击一下删除掉,这个时候就需要用到点击确定字母并删除的功能。那么我们就根据这个需求来完成下面的操作。
首先创建一个工程,然后命名为:TapTextView

然后在创建一个文件: TapTextKitView 继承与UITextView,并实现一个基本的功能,通过正则(不会的可以自己在网上查查,网上有很多教程)查找指定字符串的位置,并保存下来。代码如下:
class TapTextKitView: UITextView {private var linkTextArray: [TapLinkText]!private var tapLinkRange: [NSRange]!private var textValue: String {text ?? attributedText?.string ?? ""}override init(frame: CGRect, textContainer: NSTextContainer?) {super.init(frame: frame, textContainer: textContainer)showsVerticalScrollIndicator = falseshowsHorizontalScrollIndicator = falsecanCancelContentTouches = false delaysContentTouches = falseisEditable = false}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}private func reloadTextLayout() {guard let textAttibute = (attributedText ?? text?.attributed)?.mutableCopy() as? NSMutableAttributedString else{return} let pattern = linkTextArray?.reduce("", { (result, item) -> String inif let txt = item.linkText {return txt + "|" + result}return result})guard let patternValue = pattern else {return}let reg = try? NSRegularExpression(pattern: patternValue, options: .useUnicodeWordBoundaries)tapLinkRange = [NSRange]()if let list = reg?.matches(in: textValue, options: .withoutAnchoringBounds, range: .init(location: 0, length: textValue.count)) {for item in list {if let dict = parserReuslt(item: item,fromValue: textValue) {textAttibute.addAttributes(dict, range: item.range)}tapLinkRange.append(item.range)}}attributedText = textAttibute}private func parserReuslt(item: NSTextCheckingResult,fromValue: String) -> [NSAttributedString.Key: Any]? {let range = item.range;let text = fromValue.subStringRange(range: range)let model = linkTextArray.first { (item) -> Bool initem.linkText == text;}return model?.attibute}override func touchesBegan(_ touches: Set, with event: UIEvent?) {super.touchesBegan(touches, with: event)guard let point = touches.first?.location(in: self) else{return}let newPoint = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top)let index = layoutManager.characterIndex(for: newPoint, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)guard let range = tapLinkRange?.first(where: { (item) -> Bool initem.contains(index)}) else {return}let rangeRect = layoutManager.boundingRect(forGlyphRange: range, in: textContainer)if !rangeRect.contains(newPoint) {return}let text = textValue.subStringRange(range: range)let model = linkTextArray.first { (item) -> Bool initem.linkText == text}model?.isDidTap = truemodel?.tapAction?(range)reloadTextAttibuteLink()}}extension TapTextKitView {func addLinkTextItem(item: TapLinkText) {if linkTextArray == nil {linkTextArray = [TapLinkText]()}linkTextArray.append(item)}func addLinkTextItems(items: [TapLinkText]) {for subItem in items {addLinkTextItem(item: subItem)}}func reloadTextAttibuteLink() {reloadTextLayout()}
}
在这个里面有一个TapLinkText这个是一个对象,我们用他来保存我们要点击文字的一些属性,比如说文字的颜色,大小,是否有下滑线,下换线的颜色,是否不能换行,是否被点击,以及点击以后的样式等等。
我们看这个定义如下:
class TapLinkText: NSObject {var linkText: String?var tapAction: ((_ range: NSRange) -> Void)?var isSupportBreakLine = truevar defultAttibute: [NSAttributedString.Key: Any]?var didTapAttibute: [NSAttributedString.Key: Any]?var isDidTap = falsevar attibute: [NSAttributedString.Key: Any]? {isDidTap ? didTapAttibute : defultAttibute}
}
然后我们在ViewController文件里加上如下的测试代码:
class ViewController: UIViewController {override func viewDidLoad() {super.viewDidLoad()// Do any additional setup after loading the view.let tipLabel = UILabel(frame: .init(x: 0, y: 40, width: view.frame.width, height: 60))tipLabel.textAlignment = .centerview.addSubview(tipLabel)let tapView = TapTextKitView(frame: .init(x: 0, y: tipLabel.frame.maxY + 10, width: view.frame.width, height: 400), textContainer: nil)view.addSubview(tapView)tapView.font = UIFont.systemFont(ofSize: 18)let firstText = TapLinkText()firstText.linkText = "人应行善"firstText.defultAttibute = [.font: UIFont.systemFont(ofSize: 15),.foregroundColor: UIColor.blue]firstText.tapAction = {(range: NSRange) intipLabel.text = "点击了: " + firstText.linkText! + "索引:\(range.location)"}let secondText = TapLinkText()secondText.linkText = "《增广贤文》"secondText.isSupportBreakLine = falsesecondText.defultAttibute = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18),NSAttributedString.Key.foregroundColor: UIColor.red]secondText.didTapAttibute = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20),NSAttributedString.Key.foregroundColor: UIColor.green]secondText.tapAction = {(range: NSRange) intipLabel.text = "点击了: " + secondText.linkText! + "索引:\(range.location)"}tapView.addLinkTextItems(items: [firstText,secondText])tapView.text = "《增广贤文》简介:《增广贤文》的内容大致有这样几个方面:一是谈人及人际关系,二是谈命运,三是谈如何处世,四是表达对读书的看法。在《增广贤文》描述的世界里,人是虚伪的,人们为了一己之私变化无常,嫌贫爱富,趋炎附势,从而使世界布满了陷阱和危机。文中有很多强调命运和报应的内容,认为人的一切都是命运安排的,人应行善,才会有好的际遇。《增广贤文》有大量篇幅叙述如何待人接物,这部分内容是全文的核心。文中对忍让多有描述,认为忍让是消除烦恼祸患的方法。在主张自我保护、谨慎忍让的同时,也强调人的主观能动性,认为这是做事的原则。文中也不乏劝人向善“害人之心不可有,防人之心不可无。”"tapView.reloadTextAttibuteLink()}}
运行效果如下:

我们可以看到《增广贤文》是红色的,当我们点击这个时候变成了绿色的如下:

这样一个基本的点击文字功能就做好了。另外有一个属性isSupportBreakLine 这个是用来标志是否支持换行,也就是说,当前这一段能不能换行,比如上面那段“人应行善”,这段折行了。我如果不想让他折行该怎么实现呢。我们把前面的TapTextKitView在改成如下的代码:
class TapTextKitView: UITextView ,NSLayoutManagerDelegate{private var linkTextArray: [TapLinkText]!private var breakLineRange: [NSRange]!private var tapLinkRange: [NSRange]!private var textValue: String {text ?? attributedText?.string ?? ""}override init(frame: CGRect, textContainer: NSTextContainer?) {super.init(frame: frame, textContainer: textContainer)layoutManager.delegate = selfshowsVerticalScrollIndicator = falseshowsHorizontalScrollIndicator = falsecanCancelContentTouches = false delaysContentTouches = falseisEditable = false}required init?(coder: NSCoder) {fatalError("init(coder:) has not been implemented")}func layoutManager(_ layoutManager: NSLayoutManager, shouldBreakLineByWordBeforeCharacterAt charIndex: Int) -> Bool {breakLineRange?.contains { (item) -> Bool initem.location < charIndex && item.upperBound > charIndex} == false}private func reloadTextLayout() {guard let textAttibute = (attributedText ?? text?.attributed)?.mutableCopy() as? NSMutableAttributedString else{return} let pattern = linkTextArray?.reduce("", { (result, item) -> String inif let txt = item.linkText {return txt + "|" + result}return result})guard let patternValue = pattern else {return}let reg = try? NSRegularExpression(pattern: patternValue, options: .useUnicodeWordBoundaries)tapLinkRange = [NSRange]()if let list = reg?.matches(in: textValue, options: .withoutAnchoringBounds, range: .init(location: 0, length: textValue.count)) {for item in list {if let dict = parserReuslt(item: item,fromValue: textValue) {textAttibute.addAttributes(dict, range: item.range)}tapLinkRange.append(item.range)}}attributedText = textAttibute}private func parserReuslt(item: NSTextCheckingResult,fromValue: String) -> [NSAttributedString.Key: Any]? {let range = item.range;let text = fromValue.subStringRange(range: range)let model = linkTextArray.first { (item) -> Bool initem.linkText == text;}if model?.isSupportBreakLine == false {addForbiddenBreakLine(range: range)}return model?.attibute}override func touchesBegan(_ touches: Set, with event: UIEvent?) {super.touchesBegan(touches, with: event)guard let point = touches.first?.location(in: self) else{return}let newPoint = CGPoint(x: point.x - textContainerInset.left, y: point.y - textContainerInset.top)let index = layoutManager.characterIndex(for: newPoint, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)guard let range = tapLinkRange?.first(where: { (item) -> Bool initem.contains(index)}) else {return}let rangeRect = layoutManager.boundingRect(forGlyphRange: range, in: textContainer)if !rangeRect.contains(newPoint) {return}let text = textValue.subStringRange(range: range)let model = linkTextArray.first { (item) -> Bool initem.linkText == text}model?.isDidTap = truemodel?.tapAction?(range)reloadTextAttibuteLink()}}extension TapTextKitView {func addLinkTextItem(item: TapLinkText) {if linkTextArray == nil {linkTextArray = [TapLinkText]()}linkTextArray.append(item)}func addLinkTextItems(items: [TapLinkText]) {for subItem in items {addLinkTextItem(item: subItem)}}func addForbiddenBreakLine(range: NSRange) {if breakLineRange == nil {breakLineRange = [NSRange]()}breakLineRange.append(range)}func reloadTextAttibuteLink() {reloadTextLayout()}
}
首先它实现了一个代理NSLayoutManagerDelegate,这个是文本排版重要的工具类,如果有兴趣可以看看里面有很多使用的功能。最重要的实现下面的代理:
func layoutManager(_ layoutManager: NSLayoutManager, shouldBreakLineByWordBeforeCharacterAt charIndex: Int) -> Bool {breakLineRange?.contains { (item) -> Bool initem.location < charIndex && item.upperBound > charIndex} == false}
实现这个代理以后就可以实现是否要换行的功能了,我们测试一下,我们吧ViewController加上一句firstText.isSupportBreakLine = false 运行一下效果如下:

这样看起来这一段就不在折行了,那么这一个很实用的小功能也是一点小知识。对于Demo可以猛戳下面的链接。好了先到这里就结束了。每天进步一点点,积累起来就是一大步。
Demo链接链接
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
