0

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.

  • @HangarRash Thanks for your kind response. Majority of them are covering how to open the links, but I wish to open a view controller. Do you know if there is a way to define a link for the view controller to make it work as per existing topics? My thought was to make it like {ProjectName}://ViewControllers/TermsOfServiceViewController ... – noobietubie Mar 02 '23 at 19:01
  • Use those solutions to get notified when a specific link is tapped. Then you can open the appropriate view controller based on the tapped link. – HangarRash Mar 02 '23 at 19:10

2 Answers2

1
let vc = self.UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "viewController.storyboard.id") as! viewControllerFileName 

self.navigationController?.pushViewController(vc, animated: true)
1

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