How can I accurately detect if a link is clicked inside UILabels in Swift 4?

后端 未结 6 1142
暗喜
暗喜 2021-02-07 12:24

Edit

See my answer for a full working solution:

I managed to solve this myself by using a UITextView instead of a UILabel

6条回答
  •  半阙折子戏
    2021-02-07 12:53

    If you need a subclass of Label, solution may be something like one prepared in a playground (of cause some points should be optimized because this is just a draft):

    //: A UIKit based Playground for presenting user interface
    
    import UIKit
    import PlaygroundSupport
    
    extension String {
        // MARK: - String+RangeDetection
    
        func rangesOfPattern(patternString: String) -> [Range] {
            var ranges : [Range] = []
    
            let patternCharactersCount = patternString.count
            let strCharactersCount = self.count
            if  strCharactersCount >= patternCharactersCount {
    
                for i in 0...(strCharactersCount - patternCharactersCount) {
                    let from:Index = self.index(self.startIndex, offsetBy:i)
                    if let to:Index = self.index(from, offsetBy:patternCharactersCount, limitedBy: self.endIndex) {
    
                        if patternString == self[from..) -> NSRange? {
            let utf16view = self.utf16
            if let from = range.lowerBound.samePosition(in: utf16view),
                let to = range.upperBound.samePosition(in: utf16view) {
                return NSMakeRange(utf16view.distance(from: utf16view.startIndex, to: from),
                                   utf16view.distance(from: from, to: to))
            }
            return nil
        }
    
        func range(from nsRange: NSRange) -> Range? {
            guard
                let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
                let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
                let from = String.Index(from16, within: self),
                let to = String.Index(to16, within: self)
                else { return nil }
            return from ..< to
        }
    }
    
    final class TappableLabel: UILabel {
    
        private struct Const {
            static let DetectableAttributeName = "DetectableAttributeName"
        }
    
        var detectableText: String?
        var displayableContentText: String?
    
        var mainTextAttributes:[NSAttributedStringKey : AnyObject] = [:]
        var tappableTextAttributes:[NSAttributedStringKey : AnyObject] = [:]
    
        var didDetectTapOnText:((_:String, NSRange) -> ())?
    
        private var tapGesture:UITapGestureRecognizer?
    
        // MARK: - Public
    
        func performPreparation() {
            DispatchQueue.main.async {
                self.prepareDetection()
            }
        }
    
        // MARK: - Private
    
        private func prepareDetection() {
    
            guard let searchableString = self.displayableContentText else { return }
            let attributtedString = NSMutableAttributedString(string: searchableString, attributes: mainTextAttributes)
    
            if let detectionText = detectableText {
    
                var attributesForDetection:[NSAttributedStringKey : AnyObject] = [
                    NSAttributedStringKey(rawValue: Const.DetectableAttributeName) : "UserAction" as AnyObject
                ]
                tappableTextAttributes.forEach {
                    attributesForDetection.updateValue($1, forKey: $0)
                }
    
                for (_ ,range) in searchableString.rangesOfPattern(patternString: detectionText).enumerated() {
                    let tappableRange = searchableString.nsRange(from: range)
                    attributtedString.addAttributes(attributesForDetection, range: tappableRange!)
                }
    
                if self.tapGesture == nil {
                    setupTouch()
                }
            }
    
            text = nil
            attributedText = attributtedString
        }
    
        private func setupTouch() {
            let tapGesture = UITapGestureRecognizer(target: self, action: #selector(TappableLabel.detectTouch(_:)))
            addGestureRecognizer(tapGesture)
            self.tapGesture = tapGesture
        }
    
        @objc private func detectTouch(_ gesture: UITapGestureRecognizer) {
            guard let attributedText = attributedText, gesture.state == .ended else {
                return
            }
    
            let textContainer = NSTextContainer(size: bounds.size)
            textContainer.lineFragmentPadding = 0.0
            textContainer.lineBreakMode = lineBreakMode
            textContainer.maximumNumberOfLines = numberOfLines
    
            let layoutManager = NSLayoutManager()
            layoutManager.addTextContainer(textContainer)
    
            let textStorage = NSTextStorage(attributedString: attributedText)
            textStorage.addAttribute(NSAttributedStringKey.font, value: font, range: NSMakeRange(0, attributedText.length))
            textStorage.addLayoutManager(layoutManager)
    
            let locationOfTouchInLabel = gesture.location(in: gesture.view)
    
            let textBoundingBox = layoutManager.usedRect(for: textContainer)
            var alignmentOffset: CGFloat!
            switch textAlignment {
            case .left, .natural, .justified:
                alignmentOffset = 0.0
            case .center:
                alignmentOffset = 0.5
            case .right:
                alignmentOffset = 1.0
            }
            let xOffset = ((bounds.size.width - textBoundingBox.size.width) * alignmentOffset) - textBoundingBox.origin.x
            let yOffset = ((bounds.size.height - textBoundingBox.size.height) * alignmentOffset) - textBoundingBox.origin.y
            let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - xOffset, y: locationOfTouchInLabel.y - yOffset)
    
            let characterIndex = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    
            if characterIndex < textStorage.length {
                let tapRange = NSRange(location: characterIndex, length: 1)
                let substring = (self.attributedText?.string as? NSString)?.substring(with: tapRange)
    
                let attributeName = Const.DetectableAttributeName
                let attributeValue = self.attributedText?.attribute(NSAttributedStringKey(rawValue: attributeName), at: characterIndex, effectiveRange: nil) as? String
                if let _ = attributeValue,
                    let substring = substring {
                    DispatchQueue.main.async {
                        self.didDetectTapOnText?(substring, tapRange)
                    }
                }
            }
    
        }
    }
    
    
    class MyViewController : UIViewController {
        override func loadView() {
            let view = UIView()
            view.backgroundColor = .white
    
            let label = TappableLabel()
            label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
            label.displayableContentText = "Hello World! stackoverflow"
            label.textColor = .black
            label.isUserInteractionEnabled = true
    
            label.detectableText = "World!"
            label.didDetectTapOnText = { (value1, value2) in
                print("\(value1) - \(value2)\n")
            }
            label.performPreparation()
    
            view.addSubview(label)
            self.view = view
        }
    }
    // Present the view controller in the Live View window
    PlaygroundPage.current.liveView = MyViewController()
    

    demo:

提交回复
热议问题