はじめに
特定の文字列に触れたことを検知したい時や色・フォントを変更したい時のメモです。今回は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が呼び出され任意の処理を実行することができます。
注意点
なぜわざわざ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)