swift中TextKit实现动态图文

TextKit基础知识可以去看看这篇文章,http://www.jianshu.com/p/3f445d7f44d6
本次demo如下

  • 第一版


    🌰
  • 加强版 可自动识别连接 点击连接跳转

第二版

第一版 源码,界面上放了一个textview

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var txtV:UITextView!
    var midV:UIView!
    
    var originalPos:CGPoint?
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        //属性字
//        let attributedString = NSMutableAttributedString(attributedString: txtV.attributedText!)
//        attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0,10))
//        txtV.attributedText = attributedString
        //Text Storage实现文字高亮
        self.txtV.text = ""
        let frame = self.txtV.bounds
        let textStrage = NSTextStorage()
        let layoutManager = NSLayoutManager()
        textStrage.addLayoutManager(layoutManager)
        let containner = NSTextContainer(size: frame.size)
        layoutManager.addTextContainer(containner)
        
        txtV.textStorage.replaceCharactersInRange(NSMakeRange(0, 0), withString: "但是所谓的自控力只是人的一种「自然而然」的行为表现,这种行为表现是因为有他内在的「对世界的认知,思考习惯,思维逻辑」等内部因素的驱动,所以那些在没有自制力的人看来十分困难的『 坚持投入的做有价值的事,推迟满足感,抑制住欲望』等,他才能够做到。甚至是轻而易举、自然而然的他就抵制住了诱惑,推迟了满足感。但是所谓的自控力只是人的一种「自然而然」的行为表现,这种行为表现是因为有他内在的「对世界的认知,思考习惯,思维逻辑」等内部因素的驱动,所以那些在没有自制力的人看来十分困难的『 坚持投入的做有价值的事,推迟满足感,抑制住欲望』等,他才能够做到。甚至是轻而易举、自然而然的他就抵制住了诱惑,推迟了满足感。但是所谓的自控力只是人的一种「自然而然」的行为表现,这种行为表现是因为有他内在的「对世界的认知,思考习惯,思维逻辑」等内部因素的驱动,所以那些在没有自制力的人看来十分困难的『 坚持投入的做有价值的事,推迟满足感,抑制住欲望』等,他才能够做到。甚至是轻而易举、自然而然的他就抵制住了诱惑,推迟了满足感。xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx但是所谓的自控力只是人的一种「自然而然」的行为表现,这种行为表现是因为有他内在的「对世界的认知,思考习惯,思维逻辑」等内部因素的驱动,所以那些在没有自制力的人看来十分困难的『 坚持投入的做有价值的事,推迟满足感,抑制住欲望』等,他才能够做到。甚至是轻而易举、自然而然的他就抵制住了诱惑,推迟了满足感。xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
        self._highlight()
        
        midV = UIView()
        midV.frame = CGRectMake(30, 30, 80, 80)
        midV.backgroundColor = UIColor.purpleColor()
        midV.layer.cornerRadius = 40
        midV.layer.masksToBounds = true
        midV.layer.shouldRasterize = true
        midV.layer.rasterizationScale = UIScreen.mainScreen().scale
        
        txtV.addSubview(midV)
        originalPos = midV.frame.origin
        let pan = UIPanGestureRecognizer(target: self, action: "handlePan:")
        self.midV.addGestureRecognizer(pan)
        _updateExclusionPaths()
    }
    
    private func _highlight() {
        txtV.textStorage.beginEditing()
        
        // 属性描述字典
        let attributesDict = [NSForegroundColorAttributeName:UIColor.redColor()]
        
        txtV.textStorage.setAttributes(attributesDict, range: NSMakeRange(0, 5))
        
        txtV.textStorage.endEditing()
    }
    
    
    private func _updateExclusionPaths() {
        var circleFrame = self.txtV.convertRect(midV.bounds, fromView: midV) // 坐标转换
        circleFrame.origin.x = circleFrame.origin.x - txtV.textContainerInset.left
        circleFrame.origin.y = circleFrame.origin.y - txtV.textContainerInset.top
        let circlePath = UIBezierPath(roundedRect: circleFrame, cornerRadius: 40)
        txtV.textContainer.exclusionPaths = [circlePath]
    }

    var orp:CGPoint!
    func handlePan(gesture:UIPanGestureRecognizer){
        let p = gesture.locationInView(self.txtV)
        if gesture.state == .Began{
            orp = p
        }else if gesture.state == .Changed{
            midV.frame.origin.x = originalPos!.x+p.x-orp.x
            midV.frame.origin.y = originalPos!.y+p.y-orp.y
        }else if gesture.state == .Ended{
            originalPos = p
        }
        self._updateExclusionPaths()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

第二版源码

import UIKit
import SafariServices

class ViewController: UIViewController {

    var txtV:UITextView!
    var midV:UIView!
    let textStrage = NSTextStorage()
    let layoutManager = NSLayoutManager()
    var containner:NSTextContainer!
    var originalPos:CGPoint?
    override func viewDidLoad() {
        super.viewDidLoad()
        //Text Storage实现文字高亮
        
        let frame = CGRectMake(0, 30, 300, 500)
        textStrage.addLayoutManager(layoutManager)
        containner = NSTextContainer(size: frame.size)
        layoutManager.addTextContainer(containner)

        txtV = UITextView(frame: frame, textContainer: containner)
        self.view.addSubview(txtV)
        self.txtV.text = ""
        txtV.editable = false
        let str = "https://zuber.im 但是所谓的自控力只是人的一种「自然而然」的行为表现,这种行为表现是因为有他内在的「对世界的认知,思考习惯,思维逻辑」等内部因素的驱动,所以那些在没有自制力的人看来十分困难的『 坚持投入的做有价值的事,推迟满足感,抑制住欲望』等,他才能够做到。甚至是轻而易举、自然而然的他就抵制住了诱惑,推迟了满足感。但是所谓的自控力只是人的一种「自然而然」的行为表现,这种行为表现是因为有他内在的「对世界的认知,思考习惯,思维逻辑」等内部因素的驱动,所以那些在没有自制力的人看来十分困难的『 坚持投入的做有价值的事,推迟满足感,抑制住欲望』等,他才能够做到。甚至是轻而易举、自然而然的他就抵制住了诱惑,推迟了满足感。但是所谓的自控力只是人的一种「自然而然」的行为表现,这种行为表现是因为有他内在的「对世界的认知,思考习惯,思维逻辑」等内部因素的驱动,所以那些在没有自制力的人 http://zuber.im 看来十分困难的『 坚持投入的做有价值的事,推迟满足感,抑制住欲望』等,他才能够做到。甚至是轻而易举、自然而然的他就抵制住了诱惑,推迟了满足感。xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx但 http://beyondvincent.com/2013/11/12/2013-11-12-121-brief-analysis-text-kit/#1 是所谓的自控力只是人的一种「自然而然」的行为表现,这种行为表现是因为有他内在的「对世界的认知,思考习惯,思维逻辑」等内部因素的驱动,所以那些在没有自制力的人看来十分困难的『 坚持投入的做有价值的事,推迟满足感,抑制住欲望』等,他才能够做到。甚至是轻而易举、自然而然的他就抵制住了诱惑,推迟了满足感。xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx http://zuber.im"
        
//        txtV.textStorage.replaceCharactersInRange(NSMakeRange(0, 0), withString: str)
        self.parseTextAndExtractActiveElements(str)
        
        textStrage.setAttributedString(self.addLinkAttribute(str))
        self._highlight()
        
        midV = UIView()
        midV.frame = CGRectMake(30, 30, 80, 80)
        midV.backgroundColor = UIColor.purpleColor()
        midV.layer.cornerRadius = 40
        midV.layer.masksToBounds = true
        midV.layer.shouldRasterize = true
        midV.layer.rasterizationScale = UIScreen.mainScreen().scale
        
        txtV.addSubview(midV)
        originalPos = midV.frame.origin
        let pan = UIPanGestureRecognizer(target: self, action: "handlePan:")
        self.midV.addGestureRecognizer(pan)
        _updateExclusionPaths()
        
        self.txtV.userInteractionEnabled = true
        let tap = UITapGestureRecognizer(target: self, action: "tapurl:")
        self.txtV.addGestureRecognizer(tap)
        
        print(self.reduceRightToURL(str))
        self.handleURLTap { (url) -> () in
            let safariViewController = SFSafariViewController(URL: url)
            self.presentViewController(safariViewController, animated: true, completion: nil)
        }
    }
    
    
    func handleURLTap(handler: (NSURL) -> ()) {
        urlTapHandler = handler
    }
    // MARK: - private properties
    private var urlTapHandler: ((NSURL) -> ())?
    
    private func _highlight() {
        txtV.textStorage.beginEditing()
        
        // 属性描述字典
        let attributesDict = [NSForegroundColorAttributeName:UIColor.redColor()]
        
        txtV.textStorage.setAttributes(attributesDict, range: NSMakeRange(0, 5))
        
        txtV.textStorage.endEditing()
    }
    
//    需要排除的区域
    private func _updateExclusionPaths() {
        var circleFrame = self.txtV.convertRect(midV.bounds, fromView: midV) // 坐标转换
        circleFrame.origin.x = circleFrame.origin.x - txtV.textContainerInset.left
        circleFrame.origin.y = circleFrame.origin.y - txtV.textContainerInset.top
        let circlePath = UIBezierPath(roundedRect: circleFrame, cornerRadius: 40)
        txtV.textContainer.exclusionPaths = [circlePath]
    }

    var orp:CGPoint!
    func handlePan(gesture:UIPanGestureRecognizer){
        let p = gesture.locationInView(self.txtV)
        
        
        if gesture.state == .Began{
            orp = p
        }else if gesture.state == .Changed{
            midV.frame.origin.x = originalPos!.x+p.x-orp.x
            midV.frame.origin.y = originalPos!.y+p.y-orp.y
        }else if gesture.state == .Ended{
            originalPos = p
        }
        self._updateExclusionPaths()
    }
    private lazy var activeElements: [ZZType: [(range: NSRange, element: ZZElement)]] = [
        .URL: []
    ]
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    private var selectedElement: (range: NSRange, element: ZZElement )?
}

extension  ViewController{

    
    // MARK: - touch events
    func tapurl(gesture: UITapGestureRecognizer) {
        let location = gesture.locationInView(self.txtV)
        
        switch gesture.state {
        case .Began, .Changed:
            if let element = elementAtLocation(location) {
                if element.range.location != selectedElement?.range.location || element.range.length != selectedElement?.range.length {
//                    updateAttributesWhenSelected(false)
                    selectedElement = element
//                    updateAttributesWhenSelected(true)
                }
            } else {
//                updateAttributesWhenSelected(false)
                selectedElement = nil
            }
        case .Cancelled, .Ended:
            if let element = elementAtLocation(location) {
                if element.range.location != selectedElement?.range.location || element.range.length != selectedElement?.range.length {
                    //                    updateAttributesWhenSelected(false)
                    selectedElement = element
                    //                    updateAttributesWhenSelected(true)
                }
                
                switch selectedElement!.element {
                case .URL(let url): urlTapHandler?(url)
                case .None: ()
                }
                
                let when = dispatch_time(DISPATCH_TIME_NOW, Int64(0.25 * Double(NSEC_PER_SEC)))
                dispatch_after(when, dispatch_get_main_queue()) {
                    //                self.updateAttributesWhenSelected(false)
                    self.selectedElement = nil
                }
            } else {
                //                updateAttributesWhenSelected(false)
                selectedElement = nil
            }
       
        default: ()
        }
    }
    
    
    private func elementAtLocation(location: CGPoint) -> (range: NSRange, element: ZZElement )?{
        
        let boundingRect = layoutManager.boundingRectForGlyphRange(NSRange(location: 0, length: self.textStrage.length), inTextContainer: containner)
        guard boundingRect.contains(location) else {
            return nil
        }
        print(location)
        let index = layoutManager.glyphIndexForPoint(location, inTextContainer:containner)
        print(index)
        for element in activeElements.map({ $0.1 }).flatten() {
            print("element \(element.range.location)")
            if index >= element.range.location && index <= element.range.location + element.range.length {
                return element
            }
        }
        
        return nil
    }
    

    
    /// add link attribute
    private func addLinkAttribute(str: String) ->NSMutableAttributedString{
        let mutAttrString = NSMutableAttributedString(string: str)
        var range = NSRange(location: 0, length: 0)
        var attributes = mutAttrString.attributesAtIndex(0, effectiveRange: &range)
        
        for (type, elements) in activeElements {
            
            switch type {
            case .URL: attributes[NSForegroundColorAttributeName] = UIColor.blueColor()
            case .None: ()
            }
            for element in elements {
                mutAttrString.setAttributes(attributes, range: element.range)
            }
        }
        
        return mutAttrString
    }
    
    private func parseTextAndExtractActiveElements(attrString: String) {
        let textString = attrString as NSString
        for word in textString.componentsSeparatedByString(" ") {
            let element = activeElement(word)
            switch element {
            case .URL(let url):
                //将匹配的连接的range放入数组
                activeElements[.URL]?.append((textString.rangeOfString(url.absoluteString), element))
            default: ()
            }
        }
    }
    //MARK: - 判断是否为URL
    private func reduceRightToURL(str: String) -> NSURL? {
        if let regex = try? NSRegularExpression(pattern: "(?i)https?://(?:www\\.)?\\S+(?:/|\\b)", options: [.CaseInsensitive]) {
            let nsStr = str as NSString
            let results = regex.matchesInString(str, options: [], range: NSRange(location: 0, length: nsStr.length))
            if let result = results.map({ nsStr.substringWithRange($0.range) }).first, url = NSURL(string: result) {
                return url
            }
        }
        return nil
    }
    
    //MARK: -返回匹配元素
    func activeElement(word: String) -> ZZElement {
        if let url = reduceRightToURL(word) {
            return .URL(url)
        }
        
        if word.characters.count < 2 {
            return .None
        }
        return .None
    }
}

enum ZZType {
    case URL
    case None
}

enum ZZElement{
    case URL(NSURL)
    case None
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,426评论 25 707
  • 没有什么不会老去 即使 每段青春 姗姗来迟 不过是想起那路过心上的句子 坟墓也不必如此血腥 指环莫非是祭器 别把我...
    渺渺一沙阅读 202评论 0 2
  • 好久没在简书上记录自己生活中的点点滴滴了,可能是因为脱单吧。这次主要当做回忆吧。这些天有所牵挂,也被牵挂,或许孤独...
    YuxinChao阅读 221评论 0 0