1

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!])
        }
    })

1 Answers1

0

The key is to add links attributes to attributed string at ranges you need, present that string with UITextView, then handle those links by UITextViewDelegate

NSAttributedString click event in UILabel using Swift