5

Here is an interesting scenario. I have the following text sending from server:

I go to school by #option#

In iOS App, I have to convert it into:

I go to school by [UIButton]

The challenge is to measure the length of the previous text ("I go to school by "), in order to set the location (the origin) of the UIButton. The problem will be more problematic when the UILabel has multiple lines, and multiple appearance of UIButton, such as:

I go to school by #option#. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus fringilla, massa ut pharetra ultricies, elit nunc accumsan libero, ut elementum est justo sed lacus. Proin mi tellus, suscipit in magna a, auctor laoreet eros. #option# Quisque rhoncus ac mauris ac interdum. Etiam a odio augue. Mauris porttitor purus ac maximus faucibus. Suspendisse velit felis, varius lobortis turpis venenatis, tempor placerat magna. Integer in semper justo, ut pretium nunc. Aliquam laoreet vehicula finibus.#option#

Here are some of my solutions I can think of :

Method 1

NSString * testText = @"demo title";
CGSize stringSize = [testText sizeWithAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:17.0f]}];

This method can measure the size of a single line text, but for multiple lines text, the CGSize will be incorrect.

Method 2

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithString:@"hello"];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *textContainer = [[NSTextContainer alloc] init];

[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];

//Figure out the bounding rectangle
NSRect stringRect = [layoutManager boundingRectForGlyphRange:NSMakeRange(0, [layoutManager numberOfGlyphs]) inTextContainer:textContainer];

This is much more complex. (Still figuring out how to build the layout)

My question is: Any simpler method to achieve my goal?

Raptor
  • 53,206
  • 45
  • 230
  • 366
  • 2
    See http://stackoverflow.com/questions/21629784/make-a-clickable-link-in-an-nsattributedstring-for-a-uitextfield-or-uilabel for some ideas. – rmaddy Nov 16 '15 at 04:11
  • many ideas are nice. thanks. – Raptor Nov 16 '15 at 04:23
  • Do you really need to place a UIButton there? If there are any custom operations you need to do, you can use attributed string to open a URL with a custom URL scheme and then let your app handle it. Won't that be smoother and more efficient? – Harikrishnan Nov 16 '15 at 04:35
  • Why not use a web view? – matt Nov 16 '15 at 05:11
  • Method suggested by @HarikrishnanT is simplest method. You can use third party library tttattributedlabel – Ashish P. Nov 16 '15 at 05:13
  • Yes, I have to use `UIButton`. Designer gave me a specific `UIButton` image to hint users to click on it; hyperlinks are said too ugly. Can't use. – Raptor Nov 16 '15 at 05:13
  • @matt Performance of using web view is worse than native elements... – Raptor Nov 16 '15 at 05:14
  • You need to use only UILabel ? why can't you use UITextView instead. It's easy to get rect for every #option# within your text and you can add UIButton at every rect you got – harsha yarabarla Nov 16 '15 at 06:03

5 Answers5

3

Using UITextView you can solve your problem just add below method

-(void)addButtons{
    NSRange searchRange = NSMakeRange(0, [self.localTextView.text length]);
    NSLog(@"%d", self.localTextView.text.length);
    while(searchRange.location != NSNotFound){
        searchRange = [self.localTextView.text rangeOfString:@"#option#" options:NSCaseInsensitiveSearch range:searchRange];
        if (searchRange.location != NSNotFound)
        {
            NSLog(@"%d", searchRange.location);
            UITextPosition *beginning = self.localTextView.beginningOfDocument;
            UITextPosition *start = [self.localTextView positionFromPosition:beginning offset:searchRange.location];
            UITextPosition *end = [self.localTextView positionFromPosition:start offset:searchRange.length];
            UITextRange *range = [self.localTextView textRangeFromPosition:start toPosition:end];
            CGRect result1 = [self.localTextView firstRectForRange:range];
            NSLog(@"%f, %f", result1.origin.x, result1.origin.y);

            UIButton* btn = [UIButton buttonWithType:UIButtonTypeCustom];
            [btn addTarget:self action:@selector(doAction:) forControlEvents:UIControlEventTouchUpInside];
            [btn setTitle:@"Click" forState:UIControlStateNormal];
            [btn setBackgroundColor:[UIColor blackColor]];
            [btn setFrame:result1];
            [self.localTextView addSubview:btn];

            searchRange = NSMakeRange(searchRange.location + searchRange.length, self.localTextView.text.length - (searchRange.location + searchRange.length));
        }
    }
}

localTextView is IBOutlet taken from storyboard, finally just call [self addButtons]; in your viewDidAppear

-(void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    [self addButtons];
}

set the button appearance and action method as you wish i just made a black background with a title. Here is the Output which i got

enter image description here

harsha yarabarla
  • 506
  • 4
  • 11
2

For UITextView:

NSMutableAttributedString * fullStringValue = [[NSMutableAttributedString alloc] initWithString:@"I go to school by option"];
NSRange range = [[fullStringValue string] rangeOfString:@"option"];

[fullStringValue addAttribute: NSLinkAttributeName value: @"http://www.EnterUrlHere.com" range: NSMakeRange(range.location, range.length)];
yourTextView.attributedText = fullStringValue;

In this way, you can find multiple ranges and add attributes. Set dataDetectorTypes value of your UITextView to UIDataDetectorTypeLink or UIDataDetectorTypeAll to open URLs when clicked.

For UILabel:

Use TTTAttributedLabel.

The usage is clearly explained here.

Demo project available here.

pkc456
  • 8,350
  • 38
  • 53
  • 109
  • Thanks. The `UITextView` demo has some problems. 1. `NSMutableAttributedString` has no `rangeOfString`; 2. `str` on the last line is not defined (probably it should be `fullStringValue`); 3. it can't replace the content to a `UIButton` – Raptor Nov 16 '15 at 06:20
  • 2nd line should be `[[fullStringValue string] rangeOfString:@"option"];` – Raptor Nov 16 '15 at 06:24
  • 1
    Thanks @Raptor for making my answer bug free and more useful. – pkc456 Nov 16 '15 at 06:38
1
func addButtons(btnTitle: String, textView: UITextView) {
        for subview in textView.subviews {
            if subview is UIButton {
                // this is a button
                subview.removeFromSuperview()
            }
        }

        var searchRange = NSRange(location: 0, length: textView.text.count)
        if searchRange.location != NSNotFound {
            searchRange = NSString(string: textView.text).range(of: btnTitle, options: String.CompareOptions.caseInsensitive)
            if searchRange.location != NSNotFound {
                let beginning: UITextPosition = textView.beginningOfDocument
                let start: UITextPosition? = textView.position(from: beginning, offset: searchRange.location)
                var end: UITextPosition?
                if let start = start {
                    end = textView.position(from: start, offset: searchRange.length)
                }
                var range: UITextRange?
                if let start = start, let end = end {
                    range = textView.textRange(from: start, to: end)
                }
                var requiredFrame: CGRect?
                if let range = range {
                    requiredFrame = textView.firstRect(for: range)
                }
                let baseButton = BaseButton()
                baseButton.addTarget(self, action: #selector(handleButton(_:)), for: .touchUpInside)

                if let reqFrame = requiredFrame {
                    baseButton.frame = CGRect(x: reqFrame.origin.x, y: reqFrame.origin.y + 3, width: reqFrame.size.width, height: reqFrame.size.height) // result1 ?? CGRect.zero
                }

                textView.addSubview(baseButton)
            }
        }
    }

Here BaseButton is the custom button...you can use native or your own custom.

0

@vienvu's deleted answer is closest to solve the problem by making use of YYText library.

NSString *text = @"Test adding UIButton #option# inline";
NSMutableAttributedString *method2text = [[NSMutableAttributedString alloc] initWithString:text];
UIFont *font = [UIFont systemFontOfSize:20];
NSMutableAttributedString *attachment = nil;

NSRange range = [[method2text string] rangeOfString:@"#option#"];
if(range.location != NSNotFound) {
    UITextField *tf = [[UITextField alloc]initWithFrame:CGRectMake(0, 0, 100, 30)];
    tf.borderStyle = UITextBorderStyleRoundedRect;
    attachment = [NSMutableAttributedString yy_attachmentStringWithContent:tf contentMode:UIViewContentModeBottom attachmentSize:tf.frame.size alignToFont:font alignment:YYTextVerticalAlignmentCenter];
    [method2text replaceCharactersInRange:range withString:@""];
    [method2text insertAttributedString:attachment atIndex:range.location];

}

theLabel = [YYLabel new];
theLabel.userInteractionEnabled = YES;
theLabel.numberOfLines = 0;
theLabel.frame = CGRectMake(0, 100, 320, 320);
theLabel.center = self.view.center;
theLabel.attributedText = method2text;
[self.view addSubview:theLabel];

where theLabel is a YYLabel. I found that YYTextView cannot select the UI elements. Similar codes are workable for UIButton too.

Raptor
  • 53,206
  • 45
  • 230
  • 366
0

Using UIButton is very cumbersom. I suggest you check out the UITextViewDelegate's two methods:

- textView:shouldInteractWithTextAttachment:inRange:

and

- textView:shouldInteractWithURL:inRange:

By using these two you can easily track where user has tapped and based on the text/url tapped you can take appropriate actions. This has an advantage of allowing you to vary the font/size of the text without worrying about the positioning of the buttons.

Eimantas
  • 48,927
  • 17
  • 132
  • 168
  • I know it is very troublesome to use a `UIButton`. But the designer wants to do so. Thanks for your input – Raptor Nov 16 '15 at 08:28