I have many different strings that are inside UILabel and look like that one for example, but much longer:
[[France]] is a member state of the [[European Union]] as well as the [[Schengen Agreement]]. There are many friendly car and truck drivers. Drivers have to pay toll on motorways (except in [[Bretagne|Brittany]])
What I need to do is to make each of the substrings inside the double square brackets clickable with links to different pages. So when the user clicks on the part of a string with European Union it moves him to a page called European Union etc. What I was able to do so far is to make those substrings of UILabel clickable, but only when there is a single link to be clicked. However, when I am trying to add tap gesture for each of these substrings, it makes only the last substring clickable. Is there a way to add a different gesture to each different part of the string in UILabel? Is there maybe a different better way of approaching this? Or maybe it should work correctly and there is something wrong with the way I am adding the tap gestures to the UILabel?
This is how my extension looks like at the moment:
extension UITapGestureRecognizer {
func didTapAttributedString(_ string: String, in label: UILabel) -> Bool {
guard let text = label.text else {
return false
}
let range = (text as NSString).range(of: string)
return self.didTapAttributedText(label: label, inRange: range)
}
private func didTapAttributedText(label: UILabel, inRange targetRange: NSRange) -> Bool {
guard let attributedText = label.attributedText else {
assertionFailure("attributedText must be set")
return false
}
let textContainer = createTextContainer(for: label)
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
let textStorage = NSTextStorage(attributedString: attributedText)
if let font = label.font {
textStorage.addAttribute(NSAttributedString.Key.font, value: font, range: NSMakeRange(0, attributedText.length))
}
textStorage.addLayoutManager(layoutManager)
let locationOfTouchInLabel = location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let alignmentOffset = aligmentOffset(for: label)
let xOffset = ((label.bounds.size.width - textBoundingBox.size.width) * alignmentOffset) - textBoundingBox.origin.x
let yOffset = ((label.bounds.size.height - textBoundingBox.size.height) * alignmentOffset) - textBoundingBox.origin.y
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - xOffset, y: locationOfTouchInLabel.y - yOffset)
let characterTapped = layoutManager.characterIndex(for: locationOfTouchInTextContainer, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
let lineTapped = Int(ceil(locationOfTouchInLabel.y / label.font.lineHeight)) - 1
let rightMostPointInLineTapped = CGPoint(x: label.bounds.size.width, y: label.font.lineHeight * CGFloat(lineTapped))
let charsInLineTapped = layoutManager.characterIndex(for: rightMostPointInLineTapped, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
return characterTapped < charsInLineTapped ? targetRange.contains(characterTapped) : false
}
private func createTextContainer(for label: UILabel) -> NSTextContainer {
let textContainer = NSTextContainer(size: label.bounds.size)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
return textContainer
}
private func aligmentOffset(for label: UILabel) -> CGFloat {
switch label.textAlignment {
case .left, .natural, .justified:
return 0.0
case .center:
return 0.5
case .right:
return 1.0
@unknown default:
return 0.0
}
}}
And this is how I implement it:
public func addTapGestureToPartOfString(label: UILabel, stringToBeCalled: String) {
self.setUpDataForClickableLabel(string: stringToBeCalled, label: label)
let tapGesture = UITapGestureRecognizer.init(target: self, action: #selector(tappedOnLabel(_:)))
label.lineBreakMode = .byWordWrapping
label.isUserInteractionEnabled = true
tapGesture.numberOfTouchesRequired = 1
label.addGestureRecognizer(tapGesture)
}
@objc func tappedOnLabel(_ gesture: UITapGestureRecognizer) {
guard let label = self.labelToBeCalled,
let stringToBeCalled = self.stringToBeCalled,
let page = self.pageToBeCalled,
let allPages = self.pages else {
return
}
if gesture.didTapAttributedString(stringToBeCalled, in: label) {
let pageDetailViewController = PageDetailViewController(viewModel: PageDetailViewModel())
pageDetailViewController.populatePage(page: page, allPages: allPages)
pageDetailViewController.modalPresentationStyle = .fullScreen
UIApplication.topViewController()?.navigationController?.pushViewController(pageDetailViewController, animated: true)
}
}
And that's how I am addingTapGesture to each substring in ViewController. descriptionClickableValuesArray it's an array with the extracted square bracket values. checkIfClickable checks if a page with a related title is found and self.pageDescription is a label that is passed UILabel.
self.viewModel.descriptionClickableValuesArray?.forEach({ clickableValue in
if self.viewModel.checkIfClickable(string: clickableValue) {
self.viewModel.addTapGestureToPartOfString(label: self.pageDescription, stringToBeCalled: clickableValue)
self.viewModel.labelToBeCalled?.set(color: .systemBlue, on: [self.viewModel.stringToBeCalled!])
}
})