LoginSignup
2
4

More than 3 years have passed since last update.

UILabelで任意の文字列タップを検出する

Posted at

はじめに

特定の文字列に触れたことを検知したい時や色・フォントを変更したい時のメモです。今回はUILabelのカスタムクラスを作成する感じで実装しました。

TL;DR

PartiallyLinkLabel.swift
protocol PartiallyLinkLabelProtocol: class {
    func partiallyLinkLabelLinkTextDidTap()
}

class PartiallyLinkLabel: UILabel {
    private var partTextRang: NSRange?
    private weak var viewProtocol: PartiallyLinkLabelProtocol?

    @objc private func textDidTap(_ gesture: UITapGestureRecognizer) {
        if attributedTextDidTap(gesture) {
            viewProtocol?.partiallyLinkLabelLinkTextDidTap()
        }
    }

    func set(_ viewProtocol: PartiallyLinkLabelProtocol, defaultColor: UIColor, partColor: UIColor, text: String, partText: String) {
        self.isUserInteractionEnabled = true
        self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.textDidTap(_:))))
        self.viewProtocol = viewProtocol
        let attrText = NSMutableAttributedString()
        let partRang = NSString(string: text).range(of: partText)
        let aboveText = text.prefix(partRang.location)
        let belowText = text.suffix(text.length - (partRang.location + partRang.length))
        let aboveAttributeString = NSAttributedString(string: String(aboveText), attributes: [.foregroundColor: defaultColor])
        let belowAttributeString = NSAttributedString(string: String(belowText), attributes: [.foregroundColor: defaultColor])
        let partAttributeString = NSAttributedString(string: partText, attributes: [.foregroundColor: partColor])
        // defaultの.foregroundColorは.darkGrayにしたいのでそれぞれ分割して追加
        attrText.append(aboveAttributeString)
        attrText.append(partAttributeString)
        attrText.append(belowAttributeString)
        self.partTextRang = partRang
        self.attributedText = attrText
    }

    private func attributedTextDidTap(_ gesture: UITapGestureRecognizer) -> Bool {
        guard let partRang = partTextRang, let attrText = self.attributedText else {
            return false
        }
        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: CGSize.zero)
        let textStorage = NSTextStorage(attributedString: attrText)
        let labelSize = self.bounds.size

        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)

        textContainer.lineFragmentPadding = 0.0
        textContainer.lineBreakMode = self.lineBreakMode
        textContainer.maximumNumberOfLines = self.numberOfLines
        textContainer.size = labelSize

        let locationOfTouchInLabel = gesture.location(in: self)
        let textBoundingBox = layoutManager.usedRect(for: textContainer)
        let textContainerOffset = CGPoint(x: (labelSize.width - textBoundingBox.size.width) * 0.5, y: (labelSize.height - textBoundingBox.size.height) * 0.5)
        // NSLayoutManagerからcharacterIndexを取得する際にはtextが表示されているboundsが使用されるため、offset分マイナスして調整する
        let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, y: locationOfTouchInLabel.y - textContainerOffset.y)
        let indexOfCharacter = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)

        return NSLocationInRange(indexOfCharacter, partRang)
    }
}

Usage

ViewController.swift
class ViewController: UIViewController {

    @IBOutlet private weak var termsOfServiceLabel: PartiallyLinkLabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        termsOfServiceLabel.set(self, defaultColor: .darkGray, partColor: .red, text: "決済をしたと同時に利用規約に同意したとみなします", partJaText: "利用規約")
    }
}

extension ViewController: PartiallyLinkLabelProtocol {
    func partiallyLinkLabelLinkTextDidTap() {
        // TODO: `利用規約` tapped event.
    }
}

結果

上手く表示されました。利用規約をタップすることでProtocolMethodが呼び出され任意の処理を実行することができます。
スクリーンショット 2020-02-18 22.28.55.png

注意点

なぜわざわざTouchを検出したCGPointからtextContainerのOffSet分値を小さくすると上手くいくのか疑問だったが、コメントに残した通りNSLayoutManagerからcharacterIndexを取得する際にはtextが表示されているbounds(余白を除く幅)が使用されるため、offset分マイナスして調整することでタップ位置が正しく検出することができる感じだった。

PartiallyLinkLabel.swift
   let locationOfTouchInLabel = gesture.location(in: self)
   let textBoundingBox = layoutManager.usedRect(for: textContainer)
   let textContainerOffset = CGPoint(x: (labelSize.width -    textBoundingBox.size.width) * 0.5, y: (labelSize.height -    textBoundingBox.size.height) * 0.5)
   // NSLayoutManagerからcharacterIndexを取得する際にはtextが表示されてい   るboundsが使用されるため、offset分マイナスして調整する
   let locationOfTouchInTextContainer = CGPoint(x:    locationOfTouchInLabel.x - textContainerOffset.x, y:    locationOfTouchInLabel.y - textContainerOffset.y)
   let indexOfCharacter = layoutManager.characterIndex(for:    locationOfTouchInTextContainer, in: textContainer,    fractionOfDistanceBetweenInsertionPoints: nil)
2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4