Search code examples
iosswiftstringlabelnsattributedstring

Add GestureRecognizer to String not working properly


I have a Label in which I have two specific words that should be clickable. This is how it looks:

enter image description here

Nutungsbedigungen and Datenschutzrichtlinien should both be clickable. What I did to achieve this is this:

setUpLabel:

func setUpDocumentsLabel(){
    var textArray = [String]()
    var fontArray = [UIFont]()
    var colorArray = [UIColor]()
    textArray.append("Mit 'fortfahren' akzeptierst du die")
    textArray.append("Nutzungsbedingungen")
    textArray.append("und")
    textArray.append("Datenschutzrichtlinien.")

    fontArray.append(Fonts.regularFontWithSize(size: 13.0))
    fontArray.append(Fonts.boldFontWithSize(size: 13.0))
    fontArray.append(Fonts.regularFontWithSize(size: 13.0))
    fontArray.append(Fonts.boldFontWithSize(size: 13.0))

    colorArray.append(Colors.white)
    colorArray.append(Colors.white)
    colorArray.append(Colors.white)
    colorArray.append(Colors.white)

    self.documentsLabel.attributedText = getAttributedString(arrayText: textArray, arrayColors: colorArray, arrayFonts: fontArray)

    self.documentsLabel.isUserInteractionEnabled = true
    let tapgesture = UITapGestureRecognizer(target: self, action: #selector(tappedOnLabel(_ :)))
    tapgesture.numberOfTapsRequired = 1
    self.documentsLabel.addGestureRecognizer(tapgesture)
}

tappedAction:

@objc func tappedOnLabel(_ gesture: UITapGestureRecognizer) {
    guard let text = self.documentsLabel.text else { return }
    let nutzen = (text as NSString).range(of: "Nutzungsbedingungen")
    let daten = (text as NSString).range(of: "Datenschutzrichtlinien")

    if gesture.didTapAttributedTextInLabel(label: self.documentsLabel, inRange: nutzen) {

        let alertcontroller = UIAlertController(title: "Tapped on", message: "user tapped on Nutzungsbedingungen", preferredStyle: .alert)
        let alertAction = UIAlertAction(title: "OK", style: .default) { (alert) in

        }
        alertcontroller.addAction(alertAction)
        self.present(alertcontroller, animated: true)

    } else if gesture.didTapAttributedTextInLabel(label: self.documentsLabel, inRange: daten){

        let alertcontroller = UIAlertController(title: "Tapped on", message: "user tapped on Datenschutzrichtlinien", preferredStyle: .alert)
        let alertAction = UIAlertAction(title: "OK", style: .default) { (alert) in

        }
        alertcontroller.addAction(alertAction)
        self.present(alertcontroller, animated: true)

    }
}

getAttributedString:

func getAttributedString(arrayText:[String]?, arrayColors:[UIColor]?, arrayFonts:[UIFont]?) -> NSMutableAttributedString {

    let finalAttributedString = NSMutableAttributedString()

    for i in 0 ..< (arrayText?.count)! {

        let attributes = [NSAttributedString.Key.foregroundColor: arrayColors?[i], NSAttributedString.Key.font: arrayFonts?[i]]
        let attributedStr = (NSAttributedString.init(string: arrayText?[i] ?? "", attributes: attributes as [NSAttributedString.Key : Any]))

        if i != 0 {

            finalAttributedString.append(NSAttributedString.init(string: " "))
        }

        finalAttributedString.append(attributedStr)
    }

    return finalAttributedString
}

Problem:

The strings are not clickable on every character! The length of the clickable area varies from device:

For Datenschutzrichtlinien:

iPhone 11: clickable from D - t

iPhone SE: clickable from D - 2nd i

For Nutzungsbedingungen:

fine on every device but iPhone SE: only from N - 2nd n

I have no idea why this happens so if anyone can help me out here Im very grateful!

Update:

I am using this extension for UITapGestureRecognizer:

extension UITapGestureRecognizer {

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)

    return NSLocationInRange(indexOfCharacter, targetRange)
}

Solution

  • The specific problem you are hitting here is because your label has

    .textAlignment = .center
    

    but the didTapAttributedTextInLabel() code doesn't know that... it calculates positions based on "und Datenschutzrichtlinien." starting at the left edge of the label.

    You can try this to fix it - quick testing (on only one device) - appears to do the job.

    In your FirstLaunchViewController class, don't center the documents label:

    let documentsLabel: UILabel = {
        let v = UILabel()
        // don't do this
        //v.textAlignment = .center
        v.numberOfLines = 0
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    

    It appears you are using func getAttributedString(...) only once, and that is to format the attributed text for documentsLabel, so change it as follows:

    func getAttributedString(arrayText:[String]?, arrayColors:[UIColor]?, arrayFonts:[UIFont]?) -> NSMutableAttributedString {
    
        let finalAttributedString = NSMutableAttributedString()
    
        for i in 0 ..< (arrayText?.count)! {
    
            let attributes = [NSAttributedString.Key.foregroundColor: arrayColors?[i], NSAttributedString.Key.font: arrayFonts?[i]]
            let attributedStr = (NSAttributedString.init(string: arrayText?[i] ?? "", attributes: attributes as [NSAttributedString.Key : Any]))
    
            if i != 0 {
    
                finalAttributedString.append(NSAttributedString.init(string: " "))
            }
    
            finalAttributedString.append(attributedStr)
        }
    
        // add paragraph attribute
        let paragraph = NSMutableParagraphStyle()
        paragraph.alignment = .center
        let attributes: [NSAttributedString.Key : Any] = [NSAttributedString.Key.paragraphStyle: paragraph]
        finalAttributedString.addAttributes(attributes, range: NSRange(location: 0, length: finalAttributedString.length))
    
        return finalAttributedString
    }
    

    Now your didTapAttributedTextInLabel(...) func should detect the correct tap location relative to the "tappable" words.