Как я могу точно определить, была ли нажата ссылка внутри UILabels в Swift 4? - PullRequest
0 голосов
/ 24 мая 2018

Редактировать

Смотрите мой ответ для полного рабочего решения:

Мне удалось решить это самостоятельно, используя UITextView вместо 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)
}

Ответы [ 5 ]

0 голосов
/ 15 июня 2018

Я хотел бы не публиковать ответ, так как это скорее комментарий к собственному ответу Дэна Брея (не может комментировать из-за отсутствия повторения).Тем не менее, я все еще думаю, что стоит поделиться.


Я внес несколько небольших (как мне кажется,) улучшений в ответ Дана Брея для удобства:

  • Я нашел его немногонеудобно настраивать textView с диапазонами и прочим, поэтому я заменил эту часть на textLink dict, в котором хранятся строки ссылок и их соответствующие цели.Реализующему viewController нужно только установить это для инициализации textView.
  • Я добавил стиль подчеркивания для ссылок (сохранение шрифта и т. Д. От конструктора интерфейса).Не стесняйтесь добавлять свои собственные стили здесь (например, синий цвет шрифта и т. Д.).
  • Я переработал подпись обратного вызова, чтобы сделать ее более удобной для обработки.
  • Обратите внимание, что мне также пришлось переименоватьот delegate до linkDelegate, поскольку в UITextViews уже есть делегат.

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

И, наконец, большое, но не менее важное, спасибо Дэну Брею, который все-таки решил эту проблему!

0 голосов
/ 02 июня 2018

Если вам нужен подкласс Label, решение может быть чем-то наподобие того, которое подготовлено на игровой площадке (потому что некоторые точки должны быть оптимизированы, потому что это всего лишь черновик):

//: A UIKit based Playground for presenting user interface

import UIKit
import PlaygroundSupport

extension String {
    // MARK: - String+RangeDetection

    func rangesOfPattern(patternString: String) -> [Range<Index>] {
        var ranges : [Range<Index>] = []

        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..<to] {
                        ranges.append(from..<to)
                    }
                }
            }
        }

        return ranges
    }

    func nsRange(from range: Range<String.Index>) -> 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<String.Index>? {
        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:

enter image description here

0 голосов
/ 29 мая 2018

Вы можете использовать библиотеку MLLabel.MLLabel является подклассом UIlabel.Библиотека имеет класс MLLinkLabel, который является подклассом MLLabel.Это означает, что вы можете использовать его вместо UIlabel (даже в построителе интерфейса просто перетащите UILabel и измените его класс на MLLinkLabel)

MLLinkLabel может помочь вам, и это очень просто.Вот пример:

    label.didClickLinkBlock = {(link, linkText, label) -> Void in

        //Here you can check the type of the link and do whatever you want.
        switch link!.linkType {
        case .email:
            break
        case .none:
             break
        case .URL:
             break
        case .phoneNumber:
             break
        case .userHandle:
             break
        case .hashtag:
             break
        case .other:
             break
        }

    }

вы можете проверить библиотеку в GitHub https://github.com/molon/MLLabel

Вот скриншот из одного из моих приложений, в котором я использовал MLLabel.

enter image description here

0 голосов
/ 31 мая 2018

Мне удалось решить эту проблему, используя 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.Если вы используете autoLayout в редакторе Xcode, то отключите Scrolling Enabled для UITextView в редакторе, чтобы избежать предупреждений макета.

В файле Swift, который содержит код дляваш xib файл (в моем случае это класс для UITableViewCell, вам нужно установить следующие переменные для вашего интерактивного textView:

  • ranges - начальный и конечный индекс каждого интерактивного кликассылка с UITextView
  • page - String для идентификации страницы или представления, содержащего UITextView
  • paragraph - Если у вас есть несколько кликабельных UITextView, назначьте каждому из них номер
  • delegate - чтобы делегировать события щелчка там, где вы сможете их обработать.

Затем вам нужно создать протокол дляyour delegate:

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

Переменные, переданные в clickedLink, дают вам всю необходимую информацию, чтобы узнать, по какой ссылке щелкнули.

0 голосов
/ 27 мая 2018

Если вы не против переписать свой код, вы должны использовать UITextView вместо UILabel.

. Вы можете легко обнаружить ссылку, установив UITextView s dataDetectorTypes и реализоватьфункция делегата для извлечения ваших кликов по URL.

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

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

...