iOS UITextView or UILabel with clickable links to actions [duplicate]
Asked Answered
U

10

79

I want to make a UILabel or UITextView with some text with 2 clickable links in it. Not links to webpages but I want to link those 2 links with actions like i would do with an UIButton. All the examples i've seen are links to webviews but I dont want that. As well, the text will be translated in other languages so the positions have to be dynamic.

Want to make this:

enter image description here

Unpromising answered 12/12, 2013 at 11:0 Comment(8)
You can simply overlap transparent buttons over those underlined text.Remise
you can also go for gestureFormal
I can't overlap transparent buttons over the text because it will be translated in other languages, so the positions changes.Unpromising
Why don't you want to use a web view?Chill
Because the text isn't much on the terms pages and I want it snappy for the user. And as well, just want to know if it is possible. I might have to do web views in the end tho.Unpromising
i am not sure ..can we achive by clculating height of text in labe and after doing some calculation we can set frameFormal
may this url help you liquidx.net/blog/2009/07/07/…Formal
@MichaelKoper In what way would a UIWebView that presents a small HTML string such as the one shown in your question not be 'snappy'?Mar
R
103

I needed to solve this exact same problem: very similar text with those two links in it, over multiple lines, and needing it to be able to be translated in any language (including different word orders, etc). I just solved it, so let me share how I did it.

Initially I was thinking that I should create attributed text and then map the tap's touch location to the regions within that text. While I think that is doable, I also think it's a much too complicated approach.

This is what I ended up doing instead:

SUMMARY:

  • Have very basic custom markup in your English message so you can parse out the different pieces
  • Instruct your translators to leave the markup in and translate the rest
  • Have a UIView that can serve as the container of this message
  • Break your English message up in pieces to separate the regular text from the clickable text
  • For each piece create a UILabel on the container UIView
  • For the clickable pieces, set your styling, allow user interaction and create your tap gesture recognizer
  • Do some very basic bookkeeping to place the words perfectly across the lines

DETAIL:

In the view controller's viewDidLoad I placed this:

[self buildAgreeTextViewFromString:NSLocalizedString(@"I agree to the #<ts>terms of service# and #<pp>privacy policy#", 
                                                     @"PLEASE NOTE: please translate \"terms of service\" and \"privacy policy\" as well, and leave the #<ts># and #<pp># around your translations just as in the English version of this message.")];

I'm calling a method that will build the message. Note the markup I came up with. You can of course invent your own, but key is that I also mark the ends of each clickable region because they span over multiple words.

Here's the method that puts the message together -- see below. First I break up the English message over the # character (or rather @"#" string). That way I get each piece for which I need to create a label separately. I loop over them and look for my basic markup of <ts> and <pp> to detect which pieces are links to what. If the chunk of text I'm working with is a link, then I style a bit and set up a tap gesture recogniser for it. I also strip out the markup characters of course. I think this is a really easy way to do it.

Note some subtleties like how I handle spaces: I simply take the spaces from the (localised) string. If there are no spaces (Chinese, Japanese), then there won't be spaces between the chunks either. If there are spaces, then those automatically space out the chunks as needed (e.g. for English). When I have to place a word at the start of a next line though, then I do need to make sure that I strip of any white space prefix from that text, because otherwise it doesn't align properly.

- (void)buildAgreeTextViewFromString:(NSString *)localizedString
{
  // 1. Split the localized string on the # sign:
  NSArray *localizedStringPieces = [localizedString componentsSeparatedByString:@"#"];
  
  // 2. Loop through all the pieces:
  NSUInteger msgChunkCount = localizedStringPieces ? localizedStringPieces.count : 0;
  CGPoint wordLocation = CGPointMake(0.0, 0.0);
  for (NSUInteger i = 0; i < msgChunkCount; i++)
  {
    NSString *chunk = [localizedStringPieces objectAtIndex:i];
    if ([chunk isEqualToString:@""])
    {
      continue;     // skip this loop if the chunk is empty
    }
    
    // 3. Determine what type of word this is:
    BOOL isTermsOfServiceLink = [chunk hasPrefix:@"<ts>"];
    BOOL isPrivacyPolicyLink  = [chunk hasPrefix:@"<pp>"];
    BOOL isLink = (BOOL)(isTermsOfServiceLink || isPrivacyPolicyLink);
    
    // 4. Create label, styling dependent on whether it's a link:
    UILabel *label = [[UILabel alloc] init];
    label.font = [UIFont systemFontOfSize:15.0f];
    label.text = chunk;
    label.userInteractionEnabled = isLink;

    if (isLink)
    {
      label.textColor = [UIColor colorWithRed:110/255.0f green:181/255.0f blue:229/255.0f alpha:1.0];
      label.highlightedTextColor = [UIColor yellowColor];
      
      // 5. Set tap gesture for this clickable text:
      SEL selectorAction = isTermsOfServiceLink ? @selector(tapOnTermsOfServiceLink:) : @selector(tapOnPrivacyPolicyLink:);
      UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self
                                                                                   action:selectorAction];
      [label addGestureRecognizer:tapGesture];
      
      // Trim the markup characters from the label:
      if (isTermsOfServiceLink) 
        label.text = [label.text stringByReplacingOccurrencesOfString:@"<ts>" withString:@""];
      if (isPrivacyPolicyLink)  
        label.text = [label.text stringByReplacingOccurrencesOfString:@"<pp>" withString:@""];
    }
    else
    {
      label.textColor = [UIColor whiteColor];
    }
    
    // 6. Lay out the labels so it forms a complete sentence again:
    
    // If this word doesn't fit at end of this line, then move it to the next
    // line and make sure any leading spaces are stripped off so it aligns nicely:

    [label sizeToFit];
    
    if (self.agreeTextContainerView.frame.size.width < wordLocation.x + label.bounds.size.width)
    {
      wordLocation.x = 0.0;                       // move this word all the way to the left...
      wordLocation.y += label.frame.size.height;  // ...on the next line

      // And trim of any leading white space:
      NSRange startingWhiteSpaceRange = [label.text rangeOfString:@"^\\s*"
                                                          options:NSRegularExpressionSearch];
      if (startingWhiteSpaceRange.location == 0)
      {
        label.text = [label.text stringByReplacingCharactersInRange:startingWhiteSpaceRange
                                                         withString:@""];
        [label sizeToFit];
      }
    }

    // Set the location for this label:
    label.frame = CGRectMake(wordLocation.x,
                             wordLocation.y,
                             label.frame.size.width,
                             label.frame.size.height);
    // Show this label:
    [self.agreeTextContainerView addSubview:label];
    
    // Update the horizontal position for the next word:
    wordLocation.x += label.frame.size.width;
  }
}

And here are my methods that handle the detected taps on those links.

- (void)tapOnTermsOfServiceLink:(UITapGestureRecognizer *)tapGesture
{
  if (tapGesture.state == UIGestureRecognizerStateEnded)
  {
    NSLog(@"User tapped on the Terms of Service link");
  }
}


- (void)tapOnPrivacyPolicyLink:(UITapGestureRecognizer *)tapGesture
{
  if (tapGesture.state == UIGestureRecognizerStateEnded)
  {
    NSLog(@"User tapped on the Privacy Policy link");
  }
}

I'm sure there are much smarter and more elegant ways to do this, but this is what I was able to come up with and it works nicely.

Here's how it looks in the app:

Simulator screenshot of the end result

Reproof answered 21/3, 2014 at 5:43 Comment(10)
Nice solution. What if I want to underline the privacy and terms text rather than making it colourful?Territory
The best way to underline words is to set an attributed string. You can specify the underline in the attributed string, and there are some different underline options to chose from. What is nice about this, as compared to drawing the underline yourself for example, is that the underline will be neatly broken on low-hanging letters like the "g", "j" and "y", etc.Reproof
For example, to set a nice thick underline, you would do something like this: NSMutableAttributedString *underlinedString = [[NSMutableAttributedString alloc] initWithString:label.text]; [underlinedString addAttribute:(NSString *)kCTUnderlineStyleAttributeName value:[NSNumber numberWithInt:kCTUnderlineStyleThick] range:(NSRange){prefix.length, word.length}]; label.attributedText = underlinedString;Reproof
While you type that in, you'll see a lot of different options pop up for the underline style when you start typing "kCTUnderline" -- just pick the style you want from the list: dashed, solid, double, thick, etc.Reproof
SUMMARY for underline: follow same code as I posted in solution, but replace the logic for colouring a word with setting the underline through NSMutableAttributedString.Reproof
Your code really helped me. But if I use it in custom cell then tap gesture does not work. How I can do it in custom cell?Territory
make sure to set userInteractionEnabled = YES on parent label tooTaxiway
You should really just use github.com/TTTAttributedLabel/TTTAttributedLabelCointreau
This is way, way too complicated. Simply use a text view and attributed text, as outlined in @kgaidis' answer below. (This is an old thread so maybe this was the best answer at the time but there is a much easier way now.)Neom
What is the agreeTextContainerView?Rhapsodic
L
46

How I implement custom text actions (like a button) for UITextView:

The key principles:

  1. Use NSAttributedString as a way of defining a link to tap.
  2. Use UITextViewDelegate to catch the press of the link.

Define a URL string:

private let kURLString = "https://www.mywebsite.com"

Add a link to your attributed string:

let originalText = "Please visit the website for more information."
let attributedOriginalText = NSMutableAttributedString(string: originalText)

let linkRange = attributedOriginalText.mutableString.range(of: "website")
attributedOriginalText.addAttribute(.link, value: kURLString, range: linkRange)

Assign attributed string to a text view:

textView.attributedText = attributedOriginalText

Implement UITextViewDelegate (this is really the key piece a prevents the URL from opening some website and where you can define your custom action instead):

func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
    if (URL.absoluteString == kURLString) {
        // Do whatever you want here as the action to the user pressing your 'actionString'
    }
    return false
}

You can also customize how your link looks:

textView.linkTextAttributes = [
    NSAttributedStringKey.foregroundColor.rawValue : UIColor.red,
    NSAttributedStringKey.underlineStyle.rawValue : NSUnderlineStyle.styleSingle]

How I implement custom actions for UILabel:

I usually end up using TTTAttributedLabel.

Liberality answered 16/12, 2015 at 19:17 Comment(3)
This is the best answer IMO. Very simpleFunky
Important to note: links in text views are interactive only if the text view is selectable but noneditable. That is, if the value of the UITextView selectable property is YES and the isEditable property is NO.Heavensent
Can this be done to UIButton?Schauer
C
23

Here is a complete example made in Swift 2 without pods.

import UIKit

class SomeViewController: UIViewController, UITextViewDelegate {
  @IBOutlet weak var terms: UITextView!

  let termsAndConditionsURL = "http://www.example.com/terms";
  let privacyURL = "http://www.example.com/privacy";

  override func viewDidLoad() {
    super.viewDidLoad()

    self.terms.delegate = self
    let str = "By using this app you agree to our Terms and Conditions and Privacy Policy"
    let attributedString = NSMutableAttributedString(string: str)
    var foundRange = attributedString.mutableString.rangeOfString("Terms and Conditions")
    attributedString.addAttribute(NSLinkAttributeName, value: termsAndConditionsURL, range: foundRange)
    foundRange = attributedString.mutableString.rangeOfString("Privacy Policy")
    attributedString.addAttribute(NSLinkAttributeName, value: privacyURL, range: foundRange)
    terms.attributedText = attributedString
  }

  func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
    if (URL.absoluteString == termsAndConditionsURL) {
      let myAlert = UIAlertController(title: "Terms", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
      myAlert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
      self.presentViewController(myAlert, animated: true, completion: nil)
    } else if (URL.absoluteString == privacyURL) {
      let myAlert = UIAlertController(title: "Conditions", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
      myAlert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
      self.presentViewController(myAlert, animated: true, completion: nil)
    }
    return false
  }
}
Chindwin answered 13/4, 2016 at 16:30 Comment(0)
D
10

Check this UILabel class ,this will surely help you . I did this same thing by using this .

TTTAttributedLabel

Destruction answered 12/12, 2013 at 11:27 Comment(0)
G
5

Here is a translated version of the accepted answer to C# for Xamarin for anyone who will find it useful:

        var str = "Or, #<li>log in# to see your orders."; 
        var strParts = str.Split('#');
        var ptWordLocation = new PointF (0, 0);

        if (strParts.Length > 1) {
            //Loop the parts of the string
            foreach (var s in strParts) {
                //Check for empty string
                if (!String.IsNullOrEmpty (s)) {
                    var lbl = new UILabel ();
                    lbl.Font = lbl.Font.WithSize (15);
                    lbl.TextColor = cpAppConstants.TextColorMessage;
                    lbl.UserInteractionEnabled = s.Contains ("<li>");
                    lbl.Text = s.Replace ("<li>", "");

                    if (s.Contains ("<li>")) {
                        lbl.TextColor = UIColor.FromRGB (200, 95, 40);

                        //Set tap gesture for this clickable text:
                        var gesture = new UITapGestureRecognizer ();
                        gesture.AddTarget(() => buildLoginLabel_onTap(gesture));
                        lbl.AddGestureRecognizer (gesture);
                    }

                    lbl.SizeToFit ();

                    //Lay out the labels so it forms a complete sentence again
                    if (vw.Frame.Width < ptWordLocation.X + lbl.Bounds.Size.Width) {
                        ptWordLocation.X = 0f;
                        ptWordLocation.Y += lbl.Frame.Size.Height;
                        lbl.Text.Trim ();
                    }

                    lbl.Frame = new RectangleF (ptWordLocation.X, ptWordLocation.Y, lbl.Frame.Size.Width, lbl.Frame.Size.Height);
                    vw.AddSubview (lbl);

                    //Update the horizontal width
                    ptWordLocation.X += lbl.Frame.Size.Width;
                }
            }
        }
Godfather answered 13/2, 2015 at 15:38 Comment(0)
M
2

My solution for clickable links to action is this,

myLabel.automaticLinkDetectionEnabled = YES;
myLabel.urlLinkTapHandler = ^(KILabel *myLabel, NSString *string, NSRange range) {
            [self attemptOpenURL:[NSURL URLWithString:string]];
            NSLog(@"URL tapped %@", string);
        };

Check this UILabel class too, this will help you.

https://github.com/Krelborn/KILabel

Montgolfier answered 18/4, 2016 at 20:10 Comment(1)
I use this library, but these cases have not handled in that libraries; such as: hashtag break on special characters, string with starting word "@" but it's not a real tag. etc etcBritteny
M
1

Click Here to know how to set Listener for textView

and Add

     UITapGestureRecognizer *listener = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapAction:)];

Write the action you want to do within the

 - (void)tapAction:(UITapGestureRecognizer *)sender
{
}

Add the listener to the view by

      [self.view addGestureRecognizer:listener];
Multiplicity answered 12/12, 2013 at 13:12 Comment(1)
Don't forget to set userInteractionEnabled to YESMultiplicity
M
1

I used Erik's solution but needed to do it with Swift. After converting I found a small problem where if you have a lot of text (more than a single line) before you got to a link then it wasn't getting wrapped properly so I added a function to fit the text.

func setText(newText:String){

    // 1. Split the localized string on the # sign:
    let localizedStringPieces:NSArray = newText.componentsSeparatedByString("#")

    // 2. Loop through all the pieces:
    var msgChunkCount:Int = localizedStringPieces.count

    var wordLocation:CGPoint = CGPointMake(0.0, 0.0)

    for (var i:Int = 0; i < msgChunkCount; i++){

        let chunk:String = localizedStringPieces[i] as! String

        if chunk == ""{
            continue;     // skip this loop if the chunk is empty
        }

        // 3. Determine what type of word this is:
        let isTermsOfServiceLink:Bool = chunk.hasPrefix("<ts>")
        let isPrivacyPolicyLink:Bool  = chunk.hasPrefix("<pp>")
        let isLink:Bool = (Bool)(isTermsOfServiceLink || isPrivacyPolicyLink)


        var remainingText:String = chunk

        while count(remainingText)>0{

            // 4. Create label, styling dependent on whether it's a link:
            let label:UILabel = UILabel()
            label.font = UIFont.systemFontOfSize(methodFontSize)
            label.text = remainingText
            label.userInteractionEnabled = isLink

            if (isLink){
                label.textColor = UIColor(red: 110/255, green: 181/255, blue: 229/255, alpha: 1.0)
                label.highlightedTextColor = UIColor.yellowColor()

                // 5. Set tap gesture for this clickable text:
                var selectorAction:Selector =  isTermsOfServiceLink ? "tapOnTermsOfServiceLink" : "tapOnPrivacyPolicyLink"

                let tapGesture:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: selectorAction)

                label.addGestureRecognizer(tapGesture)

                // Trim the markup characters from the label:
                if (isTermsOfServiceLink){
                    label.text = label.text?.stringByReplacingOccurrencesOfString("<ts>", withString: "", options: nil, range: nil)
                }
                if (isPrivacyPolicyLink){
                    label.text = label.text?.stringByReplacingOccurrencesOfString("<pp>", withString: "", options: nil, range: nil)
                }
            }else{
                label.textColor = UIColor.whiteColor()
            }

            // If this chunk of text doesn't fit at end of this line, then move it to the next
            // line and make sure any leading spaces are stripped off so it aligns nicely:

            label.sizeToFit()

            let labelHeight = label.frame.size.height

            var leftOverText:String = fitLabelToWidth(label, width: self.textContainer.frame.size.width - wordLocation.x)

            // if we can't fit anything onto this line then drop down
            if label.text == "" {
                //drop to a new line
                wordLocation.x = 0.0                       // move this word all the way to the left...

                wordLocation.y += labelHeight;  // ...on the next line.  (Have to use a constant here because if label has no text it also has no height)

                // refit the text
                label.text = remainingText
                leftOverText = fitLabelToWidth(label, width: self.textContainer.frame.size.width - wordLocation.x)

                //NB WE ARE ASSUMING HERE THAT AFTER DROPPING DOWN AT LEAST SOME OF THIS TEXT WILL FIT
                // IF THIS ISN'T THE CASE THEN THE LINE WOULD ALWAYS BE TOO BIG AND WE WOULD NEVER BE ABLE TO FIT IT ON ANYWAY!
            }

            // Set the location for this label:
            label.frame = CGRectMake(wordLocation.x, wordLocation.y, label.frame.size.width, label.frame.size.height)

            // Show this label:
            self.textContainer.addSubview(label)

            // Update the horizontal position for the next word:
            wordLocation.x += label.frame.size.width;

            // update our remaining text and get ready to go again
            remainingText = leftOverText
        }

    }

}

// fit the text label (formatted externally) to the desired with, chopping off text to make it so
// return the remaining text that didn't make the cut as a string
func fitLabelToWidth(label:UILabel, width:CGFloat)->String{
    let startingText:String = label.text!
    println("Trying to fit ::\(startingText)::")


    // if the string is null then we are done
    if startingText == ""{
        return ""
    }

    // if this fits already then we are done
    label.sizeToFit()
    if label.frame.size.width <= width{
        return ""
    }

    // so now we have to loop round trying to get this to fit
    var cutRange:Range<String.Index> = Range<String.Index>(start: startingText.startIndex, end: startingText.startIndex)
    var searchRange:Range<String.Index>

    var startSearchIndex:String.Index = startingText.startIndex
    var lastSearchIndex:String.Index = startSearchIndex

    var testText:String = ""
    var lastText:String = ""
    label.text = testText
    label.sizeToFit()

    while label.frame.size.width <= width{

        // store off the last used text as this might be as far as we go
        lastText = testText
        lastSearchIndex = startSearchIndex

        // set up the search range so we look for spaces missing out any previous ones
        searchRange = Range<String.Index>(start: startSearchIndex, end: startingText.endIndex)

        // cut out a range with the next occurrence of spaces
        cutRange = startingText.rangeOfString(" ", options: NSStringCompareOptions.CaseInsensitiveSearch, range: searchRange, locale: nil)!

        // get some text from the start of the string to our cut point (start)
        testText = startingText.substringToIndex(cutRange.startIndex)

        // move the search start to the point after the end of the spaces we just found
        startSearchIndex = cutRange.endIndex

        // try this in our label to see if it sizes ok
        label.text = testText
        label.sizeToFit()


    }

    // we leave the while when the string gets too big
    label.text = lastText
    label.sizeToFit()

    return startingText.substringFromIndex(lastSearchIndex)

}
Mender answered 2/6, 2015 at 10:48 Comment(0)
B
1

You can use below code to add tap gesture on UILable :-

Step 1:

Delegate "UIGestureRecognizerDelegate" to your viewcontroller.h 

for example: 
  @interface User_mail_List : UIViewController<UIGestureRecognizerDelegate>

Step 2:

//create you UILable
UILabel *title_lbl= [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 30)];
[title_lbl setText:@"u&me"];
[title_lbl setUserInteractionEnabled:YES];
[yourView addSubview:title_lbl];

Step 3:

UITapGestureRecognizer *tap= [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(Prof_lbl_Pressed:)];//your action selector
[tap setNumberOfTapsRequired:1];
title_lbl.userInteractionEnabled= YES;
[title_lbl addGestureRecognizer:tap];

Step 4:

-(void)Prof_lbl_Pressed:(id)sender{
   //write your code action
}

thanks,

Bascomb answered 27/2, 2016 at 8:53 Comment(0)
P
0

You can Use multiple overlapping UILabel with userInteractionEnabled = YES on it & add a UITapGestureRecognizer on that label with different bold fonts.

Here is one such example of doing it.

Something like this can also be tried.

If you want a working solution of this then you can try "Fancy-Label". Search in that link for text "Here’s my implementation" & Click it. You will get ready to use product. Don't forget to click the "Switch" button on the app that you run using the above sample.

I hope that help you a lot.

Peso answered 12/12, 2013 at 11:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.