iOS开发实战 - 精确匹配文本中的links

日常开发中直接使用NSDataDetector匹配link会遇到匹配不准确的问题,让人头痛不已

比如:
1.link中不包含scheme
2.link中的scheme/ host含有大写;
3.link中包含中文字符;
4.link尾部如果紧跟中文字符的话(很多用户会这么做),会被当作link的一部分被匹配;

以上这些情况都会导致点击link无效,影响用户体验,于是我参考微信的效果做了一些优化

错误示例:

错误示例

正确示例:

正确示例

代码示例:

语言swift 5.0
组件YYLabel

方法一:正则算法

参考:Swift5.0 用正则表达式检测文本中的网页链接

import UIKit

class YYLinkLabel: YYLabel {

    func matchLinks() {
        guard let text = attributedText, text.length > 0 else {
            return
        }
        
        let regularString = "((http[s]{0,1}|ftp)://[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|(www.[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)"
        guard let regex = try? NSRegularExpression(pattern: regularString, options: .caseInsensitive) else {
            return
        }
        
        let results = regex.matches(in: text.string, options: [], range: NSRange(location: 0, length: text.length))
        if results.count > 0 {
            for result in results {
                let urlString = (text.string as NSString).substring(with: result.range)
                // 对匹配到的内容设置高亮
                (text as? NSMutableAttributedString)?.yy_setTextHighlight(result.range, color: UIColor.blue(), backgroundColor: .clear, tapAction: { (containerView, text, range, rect) in
                    HGPortal.transfer(from: nil, toURL: urlString)
                })
            }
        }
    }

}
方法二:在NSDataDetector基础上做了一些优化

缺陷:不适合已经编码过的URL后面紧跟中文/其他特殊字符的情况,目前没有找到解决办法,不推荐使用。如果你有解决办法,欢迎评论区交流。

import UIKit

class YYLinkLabel: YYLabel {
  
    func matchLinks() {
        guard let text = attributedText, text.length > 0 else {
            return
        }
        
        let regularRange = NSRange(location: 0, length: text.length)
        guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
            return
        }
        
        detector.enumerateMatches(in: text.string, options: [], range: regularRange) { (match, falgs, nil) in
            // ⚠️ 如果原始链接中不包含scheme,如:`https://`,则转换成url后会自动加上前缀`http://`
            guard let match = match, let url = match.url else {
                return
            }
            
            // 对url进行解码
            let urlString = url.absoluteString.removingPercentEncoding ?? ""
            // 获取链接末尾中文字符的数量
            let number = urlString.numberOfFootChineseCharactersCount()
            // 获取已经移除链接末尾的中文字符的字符串
            let realUrlString = urlString.stringOfRemoveFootChineseCharacters(number: number)
            
            guard !realUrlString.isContainsChineseCharacters() else {
                // 如果链接中含有中文字符,意味着这个链接无效,故不对其设置高亮
                return
            }
            
            // ⚠️ 这里不能拿urlString/realUrlString去计算range,因为urlString可能已经加了scheme
            let range = NSRange(location: match.range.location, length: match.range.length - number)
            
            // 对匹配到的内容设置高亮
            (text as? NSMutableAttributedString)?.yy_setTextHighlight(range, color: UIColor.blue(), backgroundColor: .clear, tapAction: { (containerView, text, range, rect) in
                // 点击跳转
                HGPortal.transfer(from: nil, toURL: realUrlString)
            })
        }
    }

}

private extension String {

    /// 是否包含中文字符(主要是为了判断链接中间是否包含中文字符)
    func isContainsChineseCharacters() -> Bool {
        var result = false
        for (_, c) in enumerated() {
            if isChineseCharacter(c) {
                result = true
                break
            }
        }
        return result
    }
    
    /// 获取末尾中文字符的个数
    func numberOfFootChineseCharactersCount() -> Int {
        var number = 0
        for (i, c) in reversed().enumerated() {
            if isChineseCharacter(c) {
                number = i + 1
            } else {
                break
            }
        }
        return number
    }
    
    /// 得到一个已经去除末尾的中文字符的字符串
    func stringOfRemoveFootChineseCharacters(number: Int) -> String {
        guard number > 0 else {
            return self
        }
        return String(prefix(utf16.count - number))
    }
    
    /// 判断是否是中文字符,这里也可以使用正则算法优化,提高效率
    private func isChineseCharacter(_ character: Character) -> Bool {
        if "\u{4E00}" <= character && character <= "\u{9FA5}" {
            return true
        }
        return false
    }

}

创作不易,转载请注明出处

你可能感兴趣的:(iOS开发实战 - 精确匹配文本中的links)