首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >如何准确检测在Swift 4中的UILabels内是否点击了链接?

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

Stack Overflow用户
提问于 2018-05-24 17:08:56
回答 6查看 5.1K关注 0票数 14

编辑

有关完整的工作解决方案,请参阅我的答案:

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

我已经设法样式的链接没有问题使用NSMutableAttributedString,但我不能准确地检测到哪个字符已被点击。我已经尝试了this question中的所有解决方案(我可以将其转换为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)
}
EN

回答 6

Stack Overflow用户

回答已采纳

发布于 2018-05-31 21:59:30

通过使用UITextView而不是UILabel,我设法解决了这个问题。我最初不想使用UITextView,因为我需要元素的行为像UILabel,而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
    }
}

函数hitTest get被多次调用,但这从来不会导致问题,因为每次单击clickedLink()都只会被调用一次。我试着为不同的视图禁用isUserInteractionEnabled,但是没有效果,也没有必要。

要使用这个类,只需将它添加到您的UITextView。如果您在Xcode编辑器中使用autoLayout,那么在编辑器中为UITextView禁用Scrolling Enabled,以避免布局警告。

在包含与xib文件配套的代码的Swift文件中(在我的示例中,是UITableViewCell的类),您需要为可单击的textView设置以下变量:

  • ranges -每个带有UITextView
  • page的可点击链接的开始和结束索引-用于标识包含UITextView
  • paragraph的页面或视图的number
  • delegate -如果您有多个可点击的UITextView,请为每个可点击的are分配一个are,以便将点击事件委派到您可以处理它们的任何位置。

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

protocol DelegateName {
    func clickedLink(page: String, paragraph: Int?, linkNo: Int?)
}

传递到clickedLink中的变量为您提供了所需的所有信息,以便知道哪个链接被单击了。

票数 8
EN

Stack Overflow用户

发布于 2018-05-27 15:17:51

如果您不介意重写代码,那么您应该使用UITextView而不是UILabel

您可以通过设置UITextViewdataDetectorTypes来轻松地检测链接,并实现委托函数来检索您单击的urls。

func textView(_ textView: UITextView, shouldInteractWith URL: URL, 
    in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool

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

票数 11
EN

Stack Overflow用户

发布于 2018-06-15 18:19:01

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

为了方便起见,我对Dan Bray的回答做了一些小的(我认为是)改进:

  • 我发现用范围和东西来设置textView有点笨拙,所以我用一个存储链接字符串和它们各自的目标的textLink字典替换了这一部分。实现viewController只需要设置它来初始化textView。
  • 我给链接添加了下划线样式(保留界面构建器中的字体等)。

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<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
  }
}

代理:

import Foundation

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

实现viewController:

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

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

票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/50505334

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档