0

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)
}
Chris
  • 1,828
  • 6
  • 40
  • 108
  • What is `gesture.didTapAttributedTextInLabel` -- is it from this: https://stackoverflow.com/questions/1256887/create-tap-able-links-in-the-nsattributedstring-of-a-uilabel -- you should probably show us your exact function – Lou Franco Apr 29 '20 at 13:23
  • @LouFranco sorry. Yes that's exactly it – Chris Apr 29 '20 at 13:24
  • There are several implementations on that question -- which one are you using. Also, note that the comment are showing that there are many edge cases where it doesn't work. – Lou Franco Apr 29 '20 at 13:27
  • @LouFranco updated my question. do you know what the problem is with my code? – Chris Apr 29 '20 at 13:29
  • 1
    I'd highly recommend to use a `UITextView` instead. It would be much easier (and recommended from Apple): https://developer.apple.com/videos/play/wwdc2018/221/?time=152 – Larme Apr 29 '20 at 14:04
  • @Larme I tried it with a `textVIew` before but couldn't get the result I wanted... ( picture 1) any tips ? – Chris Apr 29 '20 at 14:16
  • The reason it happens is because this code is incredibly hard to get right -- you have to know all of the details of how a UILabel exactly places the letters -- which is very hard, and maybe impossible (how will iOS 14 do it?). UITextView supports this directly -- what is your exact problem with it? – Lou Franco Apr 29 '20 at 15:02
  • 1
    @Chris - 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. – DonMag Apr 29 '20 at 15:18

1 Answers1

3

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.

DonMag
  • 69,424
  • 5
  • 50
  • 86
  • once again.. Don for the rescue. Still remember the first time you helped me out with the `collectionView` :D thanks again man ! – Chris Apr 29 '20 at 15:45