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

后端 未结 6 1068
暗喜
暗喜 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:32

    I wanted to avoid posting an answer since it's more a comment on Dan Bray's own answer (can't comment due to lack of rep). However, I still think it's worth sharing.


    I made some small (what I think are) improvements to Dan Bray's answer for convenience:

    • I found it a bit awkward to setup the textView with the ranges and stuff so I replaced that part with a textLink dict which stores the link strings and their respective targets. The implementing viewController only needs to set this to initialize the textView.
    • I added the underline style to the links (keeping the font etc. from interface builder). Feel free to add your own styles here (like blue font color etc.).
    • I reworked the callback's signature to make it more easy to be processed.
    • Note that I also had to rename the delegate to linkDelegate since UITextViews do have a delegate already.

    The TextView:

    import UIKit
    
    class LinkTextView: UITextView {
      private var callback: (() -> Void)?
      private var pressedTime: Int?
      private var startTime: TimeInterval?
      private var initialized = false
      var linkDelegate: LinkTextViewDelegate?
      var textLinks: [String : String] = Dictionary() {
        didSet {
            initialized = false
            styleTextLinks()
        }
      }
    
      override func awakeFromNib() {
        super.awakeFromNib()
        self.textContainerInset = UIEdgeInsets.zero
        self.textContainer.lineFragmentPadding = 0
        self.delaysContentTouches = true
        self.isEditable = false
        self.isUserInteractionEnabled = true
        self.isSelectable = false
        styleTextLinks()
      }
    
      private func styleTextLinks() {
        guard !initialized && !textLinks.isEmpty else {
            return
        }
        initialized = true
    
        let alignmentStyle = NSMutableParagraphStyle()
        alignmentStyle.alignment = self.textAlignment        
    
        let input = self.text ?? ""
        let attributes: [NSAttributedStringKey : Any] = [
            NSAttributedStringKey.foregroundColor : self.textColor!,
            NSAttributedStringKey.font : self.font!,
            .paragraphStyle : alignmentStyle
        ]
        let attributedString = NSMutableAttributedString(string: input, attributes: attributes)
    
        for textLink in textLinks {
            let range = (input as NSString).range(of: textLink.0)
            if range.lowerBound != NSNotFound {
                attributedString.addAttribute(.underlineStyle, value: NSUnderlineStyle.styleSingle.rawValue, range: range)
            }
        }
    
        attributedText = attributedString
      }
    
      override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        startTime = Date().timeIntervalSinceReferenceDate
      }
    
      override func touchesEnded(_ touches: Set, with event: UIEvent?) {
        if let callback = callback {
            if let startTime = startTime {
                self.startTime = nil
                if (Date().timeIntervalSinceReferenceDate - startTime <= 0.2) {
                    callback()
                }
            }
        }
      }
    
      override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        var location = point
        location.x -= self.textContainerInset.left
        location.y -= self.textContainerInset.top
        if location.x > 0 && location.y > 0 {
            let index = self.layoutManager.characterIndex(for: location, in: self.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
            for textLink in textLinks {
                let range = ((text ?? "") as NSString).range(of: textLink.0)
                if NSLocationInRange(index, range) {
                    callback = {
                        self.linkDelegate?.didTap(text: textLink.0, withLink: textLink.1, inTextView: self)
                    }
                    return self
                }
            }
        }
        callback = nil
        return nil
      }
    }
    

    The delegate:

    import Foundation
    
    protocol LinkTextViewDelegate {
      func didTap(text: String, withLink link: String, inTextView textView: LinkTextView)
    }
    

    The implementing viewController:

    override func viewDidLoad() {
      super.viewDidLoad()
      myLinkTextView.linkDelegate = self
      myLinkTextView.textLinks = [
        "click here" : "https://wwww.google.com",
        "or here" : "#myOwnAppHook"
      ]
    }
    

    And last but not least a big thank you to Dan Bray, who's solution this is after all!

提交回复
热议问题