如何准确检测是否在Swift 4中的UILabels内部点击了链接?

Dan*_*ray 12 xcode nsattributedstring uilabel ios swift4

编辑

请参阅我的答案以获得完整的解决方案:

我设法通过使用UITextView而不是a 来解决这个问题UILabel.我写了一个类,使UITextView行为像一个UILabel但具有完全准确的链接检测.


我已经成功设置了链接的样式而没有使用问题,NSMutableAttributedString但我无法准确地检测到哪个字符已被点击.我已经尝试了这个问题中的所有解决方案(我可以转换为Swift 4代码),但没有运气.

以下代码有效,但无法准确检测单击了哪个字符并获取了链接的错误位置:

func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
    // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
    let layoutManager = NSLayoutManager()
    let textContainer = NSTextContainer(size: CGSize.zero)
    let textStorage = NSTextStorage(attributedString: label.attributedText!)

    // Configure layoutManager and textStorage
    layoutManager.addTextContainer(textContainer)
    textStorage.addLayoutManager(layoutManager)

    // Configure textContainer
    textContainer.lineFragmentPadding = 0.0
    textContainer.lineBreakMode = label.lineBreakMode
    textContainer.maximumNumberOfLines = label.numberOfLines
    let labelSize = label.bounds.size
    textContainer.size = labelSize

    // Find the tapped character location and compare it to the specified range
    let locationOfTouchInLabel = self.location(in: label)
    let textBoundingBox = layoutManager.usedRect(for: textContainer)
    let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x, y: (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y)
    let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
    let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
    print(indexOfCharacter)
    return NSLocationInRange(indexOfCharacter, targetRange)
}
Run Code Online (Sandbox Code Playgroud)

小智 10

如果你不介意重写你的代码,你应该使用UITextView而不是UILabel.

您可以轻松地通过设置检测链路UITextViewdataDetectorTypes贯彻委托函数来获取你的点击网址.

func textView(_ textView: UITextView, shouldInteractWith URL: URL, 
    in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool
Run Code Online (Sandbox Code Playgroud)

https://developer.apple.com/documentation/uikit/uitextviewdelegate/1649337-textview


Dan*_*ray 5

我设法通过使用UITextView而不是来解决此问题UILabel。我本来不想使用a,UITextView因为我需要元素的行为像a UILabel和a UITextView可能会导致滚动问题,并且它的预期用途是将其作为可编辑文本。我编写的以下类的UITextView行为类似,UILabel但具有完全准确的点击检测并且没有滚动问题:

import UIKit

class ClickableLabelTextView: UITextView {
    var delegate: DelegateForClickEvent?
    var ranges:[(start: Int, end: Int)] = []
    var page: String = ""
    var paragraph: Int?
    var clickedLink: (() -> Void)?
    var pressedTime: Int?
    var startTime: TimeInterval?

    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
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        startTime = Date().timeIntervalSinceReferenceDate
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let clickedLink = clickedLink {
            if let startTime = startTime {
                self.startTime = nil
                if (Date().timeIntervalSinceReferenceDate - startTime <= 0.2) {
                    clickedLink()
                }
            }
        }
    }

    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)
            var count = 0
            for range in ranges {
                if index >= range.start && index < range.end {
                    clickedLink = {
                        self.delegate?.clickedLink(page: self.page, paragraph: self.paragraph, linkNo: count)
                    }
                    return self
                }
                count += 1
            }
        }
        clickedLink = nil
        return nil
    }
}
Run Code Online (Sandbox Code Playgroud)

函数hitTestget被多次调用,但绝不会导致问题,因为clickedLink()每次单击只会调用一次。我尝试禁用isUserInteractionEnabled不同的视图,但是这样做并没有帮助,也没有必要。

要使用该类,只需将其添加到中UITextView。如果您autoLayout在Xcode编辑器中使用,请在编辑器中将其禁用Scrolling EnabledUITextView以避免出现布局警告。

Swift包含与xib文件一起使用的代码的文件中(在我的情况下,该类为的类UITableViewCell,您需要为可点击的textView设置以下变量:

  • ranges -每个可点击链接的开始和结束索引,带有 UITextView
  • page-一个String以确定包含的页面或视图UITextView
  • paragraph-如果您有多个可点击的按钮UITextView,请为每个按钮分配一个数字
  • delegate -将点击事件委托给您能够处理的地方。

然后,您需要为您创建一个协议delegate

protocol DelegateName {
    func clickedLink(page: String, paragraph: Int?, linkNo: Int?)
}
Run Code Online (Sandbox Code Playgroud)

传入的变量为clickedLink您提供了您需要知道单击哪个链接的所有信息。


Pvt*_*ker 5

我想避免发布答案,因为它更多的是对 Dan Bray 自己的答案的评论(由于缺乏代表而无法发表评论)。不过,我仍然认为值得分享。


为了方便起见,我对丹·布雷的答案做了一些小的(我认为是)改进:

  • 我发现用范围和内容设置 textView 有点尴尬,所以我用textLink存储链接字符串及其各自目标的字典替换了该部分。实现 viewController 只需要设置它来初始化 textView。
  • 我在链接中添加了下划线样式(保留界面生成器中的字体等)。请随意在此处添加您自己的样式(例如蓝色字体颜色等)。
  • 我重新设计了回调的签名,使其更易于处理。
  • 请注意,我还必须将其重命名为,delegate因为linkDelegateUITextViews 已经有一个委托了。

文本视图:

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<UITouch>, with event: UIEvent?) {
    startTime = Date().timeIntervalSinceReferenceDate
  }

  override func touchesEnded(_ touches: Set<UITouch>, 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
  }
}
Run Code Online (Sandbox Code Playgroud)

代表:

import Foundation

protocol LinkTextViewDelegate {
  func didTap(text: String, withLink link: String, inTextView textView: LinkTextView)
}
Run Code Online (Sandbox Code Playgroud)

实现视图控制器:

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

最后但并非最不重要的一点是非常感谢 Dan Bray,这毕竟是他的解决方案!