Search code examples
iosswiftattributesuikitnsattributedstring

Swift/UIKit Push to new View Controller when clicked inside a UILabel


I have a signup page in my app and I want users to be able to view terms of service (TermsOfServiceViewController) and privacy policy (PrivacyPolicyViewController) when user pressed text inside my UILabel.

My statement is as such: "By checking this box, you agree to our Terms of Service and our Privacy Policy". When user presses "Terms of Service" I want them to see TermsOfServiceViewController and when user presses "Privacy Policy", I want them to see PrivacyPolicyViewController.

Right now my code is like this:

override func viewDidLoad() {
    super.viewDidLoad()
    
    let termsOfServiceViewController = NavigationTapGestureRecognizer(target: self, action: #selector(termsOfServiceVCTapped))
    termsOfServiceViewController.viewController = self
    
    let privacyPolicyViewController = NavigationTapGestureRecognizer(target: self, action: #selector(privacyPolicyVCTapped))
    privacyPolicyViewController.viewController = self
    
    let string = NSMutableAttributedString(string: "By checking this box, you agree to our ")
    let attributedTermsOfService = NSMutableAttributedString(string: "Terms of Service", attributes: [NSAttributedString.Key.link: termsOfServiceViewController, NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue, NSAttributedString.Key.underlineColor: UIColor.appColor(LPColor.LightestPurple)!, NSAttributedString.Key.foregroundColor: UIColor.appColor(LPColor.LightestPurple)!])
    let additionalString = NSMutableAttributedString(string: " and our ")
    let attributedPrivacyPolicy = NSMutableAttributedString(string: "Privacy Policy", attributes: [NSAttributedString.Key.link: privacyPolicyViewController, NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue, NSAttributedString.Key.underlineColor: UIColor.appColor(LPColor.LightestPurple)!, NSAttributedString.Key.foregroundColor: UIColor.appColor(LPColor.LightestPurple)!])
    
    string.append(attributedTermsOfService)
    string.append(additionalString)
    string.append(attributedPrivacyPolicy)
    
    agreementLabel.attributedText = string
    
    agreementLabel.addGestureRecognizer(termsOfServiceViewController)
    agreementLabel.addGestureRecognizer(privacyPolicyViewController)
    agreementLabel.isUserInteractionEnabled = true
}

@objc func termsOfServiceVCTapped() {
        let vc = TermsOfServiceViewController.storyboardInstance(storyboardName: "Login") as! TermsOfServiceViewController
        navigationController?.pushViewController(vc, animated: true)
    }

@objc func privacyPolicyVCTapped() {
        let vc = PrivacyPolicyViewController.storyboardInstance(storyboardName: "Login") as! PrivacyPolicyViewController
        navigationController?.pushViewController(vc, animated: true)
    }

Right now this code is allowing tap on entire UILabel (I believe this is because I am adding addGestureRecognizer to entire agreementLabel) and it's only going to privacyPolicyViewController. Is there any way I can separate them from one another and only detect tap when user taps the actual text not the entire label?

Also, my NavigationTapGestureRecognizer looks like this:

class NavigationTapGestureRecognizer: UITapGestureRecognizer {
    var viewController: UIViewController?

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesEnded(touches, with: event!)

    }
}

If there are any suggestion that I can get, that would be fantastic. Thanks in advance.


Solution

  • So after researching a bit, I figured out the solution. Please see below:

    Create extension for UITapGestureRecognizer:

    extension UITapGestureRecognizer {
    
    func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
       guard let attributedText = label.attributedText else { return false }
    
       let mutableStr = NSMutableAttributedString.init(attributedString: attributedText)
       mutableStr.addAttributes([NSAttributedString.Key.font : label.font!], range: NSRange.init(location: 0, length: attributedText.length))
       
       // If the label have text alignment. Delete this code if label have a default (left) aligment. Possible to add the attribute in previous adding.
       let paragraphStyle = NSMutableParagraphStyle()
       paragraphStyle.alignment = .center
       mutableStr.addAttributes([NSAttributedString.Key.paragraphStyle : paragraphStyle], range: NSRange(location: 0, length: attributedText.length))
    
       // Create instances of NSLayoutManager, NSTextContainer and NSTextStorage
       let layoutManager = NSLayoutManager()
       let textContainer = NSTextContainer(size: CGSize.zero)
       let textStorage = NSTextStorage(attributedString: mutableStr)
       
       // 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)
     }
    
    }
    

    Then add attributes to string and UITapGestureRecognizer:

        class CreatePasswordViewController: UIViewController {
        
        @IBOutlet weak var agreementLabel: UILabel!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            agreementLabel.text = text
            self.agreementLabel.textColor = UIColor.gray
            let underlineAttributedString = NSMutableAttributedString(string: text)
            let openSansBoldFont = UIFont(name: "OpenSans-Bold", size: 14)
            
        let range1 = (text as NSString).range(of: "Terms of Service")
            underlineAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range1)
            underlineAttributedString.addAttribute(NSAttributedString.Key.font, value: openSansBoldFont, range: range1)
            underlineAttributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.purple, range: range1)
            
        let range2 = (text as NSString).range(of: "Privacy Policy")
            underlineAttributedString.addAttribute(NSAttributedString.Key.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range2)
            underlineAttributedString.addAttribute(NSAttributedString.Key.font, value: openSansBoldFont, range: range2)
            underlineAttributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.purple, range: range2)
            
            agreementLabel.attributedText = underlineAttributedString
            agreementLabel.isUserInteractionEnabled = true
            agreementLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapLabel(gesture:))))
        
        @objc func tapLabel(gesture: UITapGestureRecognizer) {
            let termsRange = (text as NSString).range(of: "Terms of Service")
            let privacyRange = (text as NSString).range(of: "Privacy Policy")
            
            if gesture.didTapAttributedTextInLabel(label: agreementLabel, inRange: termsRange) {
                let vc = TermsOfServiceViewController.storyboardInstance(storyboardName: "Login") as! TermsOfServiceViewController
                navigationController?.pushViewController(vc, animated: true)
            } else if gesture.didTapAttributedTextInLabel(label: agreementLabel, inRange: privacyRange) {
                let vc = PrivacyPolicyViewController.storyboardInstance(storyboardName: "Login") as! PrivacyPolicyViewController
                navigationController?.pushViewController(vc, animated: true)
            }
          }
        }
    

    Referenced this link: Tap on a part of text of UILabel