5

I have the following code and want to make parts of my text be clickable and call another UIViewController (not a website).

NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithString:@"testing it out @clickhere"];
NSInteger length = str.length;
[str addAttribute:NSForegroundColorAttributeName value:[UIColor bestTextColor] range:NSMakeRange(0,length)];

The NSMutableAttributedString gets set to a UILabel like so:

label.attributedText = str;

Whats the best way to do this? I can't seem to find a great answer.

An example of what I want is suppose I have a UILabel like so with the following text:

This is my label.  Click here to go to UIViewController1 and then go to UIViewController1 by this #tag.

I want the text "here" to be passed for the first click event and the word "#tag" to be passed to the same click event.

cdub
  • 24,555
  • 57
  • 174
  • 303
  • See if this helps: http://stackoverflow.com/questions/8811909/getting-the-word-touched-in-a-uilabel-uitextview/21577829#21577829 Also try this: http://stackoverflow.com/questions/15293426/how-to-create-uilabel-with-clickable-first-word – iOSAaronDavid Jan 19 '15 at 23:06
  • 1
    Why duplicate: http://stackoverflow.com/questions/28018707/add-a-tap-gesture-to-a-part-of-a-uilabel ? – Larme Jan 20 '15 at 09:42

4 Answers4

7

What if you used the value field to pass in the destination?

[attributedString addAttribute:NSLinkAttributeName
                         value:[@"destinationController1" stringByAppendingString:username]
                         range:range];

Then override the delegate method:

- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange
{
    if ([URL.scheme isEqualToString:@"destinationController1"]) {
        // Launch View controller
        return NO;
    }
    return YES;
}
scott
  • 1,194
  • 7
  • 18
  • 1
    The important part here is to use a `UITextView` rather than a `UILabel`.. And you might want to adjust some properties of the textview to make it behave more like a plain label, such as disabling editing, selecting and scrolling. – Daniel Rinser Jan 19 '15 at 23:23
  • Yes both in the .m file, but make sure you subscribe to the UITextViewDelegate in your .h file, so that the method is called in the first place. – scott Jan 20 '15 at 00:41
  • If gesture is there in my viewcontroller, then this will work or not? – Dimple Shah Jan 18 '17 at 11:04
  • 1
    don't add space while adding value in attributed text, otherwise shouldInteractWithURL will not detect space. And if you pass ant string value which doesn't contain any URL then compare URL.absoluteString rather than URL.scheme – Parthpatel1105 Apr 14 '18 at 05:46
3

My solution requires the use of a UITextView (which is significantly easier, and I urge that you use it instead).

Swift

class ViewController: UIViewController {
    @IBOutlet weak var textView:UITextView!;

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        let gestureRecognizer = UITapGestureRecognizer(target: self, action: "textViewTapped:");
        gestureRecognizer.numberOfTapsRequired = 1;
        gestureRecognizer.numberOfTouchesRequired = 1;
        self.textView.addGestureRecognizer(gestureRecognizer);
    }

    func textViewTapped(sender: UITapGestureRecognizer) {
        let wordTarget = "here";

        let word = UITextView.getWordAtPosition(sender.locationInView(self.textView), textView: self.textView);
        if word == wordTarget {
            let plainString = self.textView.attributedText.string;
            let substrings = NSMutableArray();
            let scanner = NSScanner(string: plainString);
            scanner.scanUpToString("#", intoString: nil);
            while !scanner.atEnd {
                var substring:NSString? = nil;
                scanner.scanString("#", intoString: nil);
                let space = " ";
                if scanner.scanUpToString(space, intoString: &substring) {
                    // If the space immediately followed the #, this will be skipped
                    substrings.addObject(substring!);
                }
                scanner.scanUpToString("#", intoString: nil);
                //Scan all characters before next #
            }
            println(substrings.description);
            //Now you got your substrings in an array, so use those for your data passing (in a segue maybe?)
            ...

        }
    }

}

extension UITextView {
    class func getWordAtPosition(position: CGPoint!, textView: UITextView!) -> String? {
        //Remove scrolloffset
        let correctedPoint = CGPointMake(position.x, textView.contentOffset.y + position.y);
        //Get location in text from uitextposition at a certian point
        let tapPosition = textView.closestPositionToPoint(correctedPoint);
        //Get word at the position, will return nil if its empty.
        let wordRange = textView.tokenizer.rangeEnclosingPosition(tapPosition, withGranularity: UITextGranularity.Word, inDirection: UITextLayoutDirection.Right.rawValue);
        return textView.textInRange(wordRange!);
    }
}

Objective-C

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(textViewTapped:)];
    gestureRecognizer.numberOfTouchesRequired = 1;
    gestureRecognizer.numberOfTapsRequired = 1;
    [self.textView addGestureRecognizer:gestureRecognizer];
}

- (void)textViewTapped:(UITapGestureRecognizer *)sender {
    NSString *wordTarget = @"here";

    NSString* word = [self getWordAtPosition:[sender locationInView:self.textView] textView:self.textView];
    if ([word isEqualToString:wordTarget]) {
        NSString *plainString = self.textView.attributedText.string;
        NSMutableArray* substrings = [[NSMutableArray alloc]init];
        NSScanner *scanner = [[NSScanner alloc]initWithString:plainString];
        [scanner scanUpToString:@"#" intoString:nil];
        while (![scanner isAtEnd]) {
            NSString* substring = nil;
            [scanner scanString:@"#" intoString:nil];
            NSString* space = @" ";
            if ([scanner scanUpToString:space intoString:&substring]) {
                [substrings addObject:substring];
            }
            [scanner scanUpToString:@"#" intoString:nil];
        }

        //Now you got your substrings in an array, so use those for your data passing (in a segue maybe?)
        ...

    }
}

- (NSString*)getWordAtPosition:(CGPoint)position textView:(UITextView *)textView {
    //remove scrollOffset
    CGPoint correctedPoint = CGPointMake(position.x, textView.contentOffset.y + position.y);
    UITextPosition *tapPosition = [textView closestPositionToPoint:correctedPoint];
    UITextRange *wordRange = [textView.tokenizer rangeEnclosingPosition:tapPosition withGranularity:UITextGranularityWord inDirection:UITextLayoutDirectionRight];
    return [textView textInRange:wordRange];
}

Basically you need to add a gesture recognizer to get the tap point in your textview. Then, you get the word using the category method provided in the extension area. After, you check what the word is (where we want the word "here"). Then, we collect the hashtags you have provided.

All you have to do is add a performSegueWithIdentifier method, and pass it accordingly.

Nate Lee
  • 2,842
  • 1
  • 24
  • 30
  • is there a way to do this but get the whole line instead of just the one word you click? – sabo Apr 21 '15 at 21:46
  • I believe so replace the `here` in `let wordTarget = "here";` with a sentence instead. – Nate Lee Apr 21 '15 at 23:51
  • I gave that a shot and it does not work. It only picks up one word so if i have to of the same word in the text then it will not be able to tell the difference. – sabo Apr 22 '15 at 19:06
  • textView.closestPositionToPoint(correctedPoint) is always returning nil – Gaurav Sharma Jan 28 '16 at 09:59
1

In addition to @Nate Lee answer, updated the extension for Swift 4.0 version:

extension UITextView {
    class func getWordAtPosition(position: CGPoint!, textView: UITextView!) -> String? {
    //Remove scrolloffset
    let correctedPoint = CGPoint(x: position.x, y: (textView.contentOffset.y + position.y))
    //Get location in text from uitextposition at a certian point
    let tapPosition = textView.closestPosition(to: correctedPoint)
    //Get word at the position, will return nil if its empty.
    let wordRange = textView.tokenizer.rangeEnclosingPosition(tapPosition!, with: .word, inDirection: UITextLayoutDirection.right.rawValue)
    return textView.text(in: wordRange!)
    }
}
Soumen
  • 2,070
  • 21
  • 25
0

Swift 3:

Don't check on the URL.scheme attribute. Returned nil for me.

Do this:

attributedString.addAttribute(NSLinkAttributeName, value: "openToViewController", range: range)

Then use the absoluteString attribute on the URL to check on that value to your view of choice:

  func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool{
if (URL.absoluteString == "openToViewController") {
  let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ViewController") as! UIViewController
  self.present(viewController, animated: true, completion: nil)
  return false
}
return true
}