1

I have a text view with a string:

@"The best football player in the world is #OPTION, and the best basketball player is #OPTION?"

This is an example what I have to do. I want to replace #OPTION with a dropdown list, which means with custom view. It depends on the question, it can be only one #OPTION or more. Thanks in advance.

Edited: This is how it should look like

The string is from API and it appears in UITextView or UILabel

Fisfej
  • 84
  • 5
  • Not able to understand what are you trying to achieve ? can you explain in detail or show me some sample UI ? – CodeChanger Feb 08 '17 at 10:07
  • `UITextField` or `UITextView`? `UITextView` should be easier. How? By using a `NSAttributedString`, giving the `NSLinkAttributedName` to "#OPTION", and show your menu on `textView:shouldInteractWithURL:inRange:interaction:` of it's delegate method. – Larme Feb 08 '17 at 10:11
  • 1
    I have updated the question – Fisfej Feb 08 '17 at 10:30
  • You'll have to deal with Core Text engine to manage your text's layout. There's an awesome [tutorial](https://www.raywenderlich.com/4147/core-text-tutorial-for-ios-making-a-magazine-app) on Core Text. Despite the fact it shows how to embed images in text, I think you'll manage to modify it to embed dropdowns. – Dan Karbayev Feb 08 '17 at 10:24
  • going all the way down to Core Text shouldn't be necessary anymore, as in the mean time [TextKit](https://www.objc.io/issues/5-ios7/getting-to-know-textkit/) was released in iOS7 – vikingosegundo Feb 08 '17 at 10:47

1 Answers1

3

After understanding your needs, The main issue you are facing is not knowing where to add the select lists.

I have created 2 categories for your case, for UILabel and for UITextView, following these posts which contain the relevant answers for that:

How do I locate the CGRect for a substring of text in a UILabel?

Get X and Y coordinates of a word in UITextView

These categories find the CGRect for a string inside, which is where you should position your pickers.

The down-part of this for UILabel, is that it doesn't handle wordWrap line breaking mode well, and therefore it won't find the correct location, to handle this correctly, you should add line breaks when needed in case you use the UILabel.

UILabel:

extension UILabel
{
    func rectFor(string str : String, fromIndex: Int = 0) -> (CGRect, NSRange)?
    {
        // Find the range of the string
        guard self.text != nil else { return nil }

        let subStringToSearch : NSString = (self.text! as NSString).substring(from: fromIndex) as NSString

        var stringRange = subStringToSearch.range(of: str)

        if (stringRange.location != NSNotFound)
        {
            guard self.attributedText != nil else { return nil }

            // Add the starting point to the sub string
            stringRange.location += fromIndex

            let storage = NSTextStorage(attributedString: self.attributedText!)
            let layoutManager = NSLayoutManager()

            storage.addLayoutManager(layoutManager)

            let textContainer = NSTextContainer(size: self.frame.size)
            textContainer.lineFragmentPadding = 0
            textContainer.lineBreakMode = .byWordWrapping

            layoutManager.addTextContainer(textContainer)

            var glyphRange = NSRange()

            layoutManager.characterRange(forGlyphRange: stringRange, actualGlyphRange: &glyphRange)

            let resultRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in:textContainer)

            return (resultRect, stringRange)
        }

        return nil
    }
}

Usage for infinite searching all available substring (I recommend adding it in viewDidLayoutSubviews() in case you use auto-layout:

var lastFoundIndex : Int = 0

while let result = self.label.rectFor(string: "#OPTION", fromIndex: lastFoundIndex)
{
    let view : UIView = UIView(frame: result.0)
    view.backgroundColor = UIColor.red

    self.label.addSubview(view)

    lastFoundIndex = result.1.location + 1
}

And the same one for UITextView:

extension UITextView
{
    func rectFor(string str : String, fromIndex: Int = 0) -> (CGRect, NSRange)?
    {
        // Find the range of the string
        guard self.text != nil else { return nil }

        let subStringToSearch : NSString = (self.text! as NSString).substring(from: fromIndex) as NSString

        var stringRange = subStringToSearch.range(of: str)

        if (stringRange.location != NSNotFound)
        {
            guard self.attributedText != nil else { return nil }

            // Add the starting point to the sub string
            stringRange.location += fromIndex

            // Find first position
            let startPosition = self.position(from: self.beginningOfDocument, offset: stringRange.location)
            let endPosition = self.position(from: startPosition!, offset: stringRange.length)

            let resultRange = self.textRange(from: startPosition!, to: endPosition!)

            let resultRect = self.firstRect(for: resultRange!)

            return (resultRect, stringRange)
        }

        return nil
    }
}

Usage:

var lastFoundTextIndex : Int = 0

while let result = self.textView.rectFor(string: "#OPTION", fromIndex: lastFoundTextIndex)
{
    let view : UIView = UIView(frame: result.0)
    view.backgroundColor = UIColor.red

    self.textView.addSubview(view)

    lastFoundTextIndex = result.1.location + 1
}

In your case, textview gives the best results and uses included methods for that, the sample code uses a label & a text view, initialized in the code:

self.label.text = "The best football player in the world is\n#OPTION, and the best basketball player\n is #OPTION?"
self.textView.text = "The best football player in the world is #OPTION, and the best basketball player is #OPTION?"

And the output just adds views on top of the "#OPTION" strings:

Hope this helps

EDIT - Added Objective-C Variation:

Create 2 extensions - 1 for UITextView and 1 for UILabel:

UILabel:

@interface UILabel (UILabel_SubStringRect)

- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index;

@end

#import "UILabel+SubStringRect.h"

@implementation UILabel (UILabel_SubStringRect)

- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index
{
    if (string != nil)
    {
        NSString* subStringToSearch = [self.text substringFromIndex:index];
        NSRange stringRange = [subStringToSearch rangeOfString:string];

        if (stringRange.location != NSNotFound)
        {
            if (self.attributedText != nil)
            {
                stringRange.location += index;

                NSTextStorage* storage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
                NSLayoutManager* layoutManager = [NSLayoutManager new];

                [storage addLayoutManager:layoutManager];

                NSTextContainer* textContainer = [[NSTextContainer alloc] initWithSize:self.frame.size];
                textContainer.lineFragmentPadding = 0;
                textContainer.lineBreakMode = NSLineBreakByWordWrapping;

                [layoutManager addTextContainer:textContainer];

                NSRange glyphRange;

                [layoutManager characterRangeForGlyphRange:stringRange actualGlyphRange:&glyphRange];

                CGRect resultRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];

                return @{ @"rect" : [NSValue valueWithCGRect:resultRect], @"range" : [NSValue valueWithRange:stringRange] };
            }
        }
    }

    return nil;
}

@end

UITextView:

@interface UITextView (SubStringRect)

- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index;

@end

#import "UITextView+SubStringRect.h"

@implementation UITextView (SubStringRect)

- (NSDictionary*)rectForString:(NSString*)string fromIndex:(int)index
{
    if (string != nil)
    {
        NSString* subStringToSearch = [self.text substringFromIndex:index];
        NSRange stringRange = [subStringToSearch rangeOfString:string];

        if (stringRange.location != NSNotFound)
        {
            if (self.attributedText != nil)
            {
                stringRange.location += index;

                UITextPosition* startPosition = [self positionFromPosition:self.beginningOfDocument offset:stringRange.location];
                UITextPosition* endPosition = [self positionFromPosition:startPosition offset:stringRange.length];

                UITextRange* resultRange = [self textRangeFromPosition:startPosition toPosition:endPosition];
                CGRect resultRect = [self firstRectForRange:resultRange];

                return @{ @"rect" : [NSValue valueWithCGRect:resultRect], @"range" : [NSValue valueWithRange:stringRange] };
            }
        }
    }

    return nil;
}

@end

Usage Sample - UILabel:

int lastFoundIndex = 0;
NSDictionary* resultDict = nil;

do
{
    resultDict = [self.label rectForString:@"#OPTION" fromIndex:lastFoundIndex];

    if (resultDict != nil)
    {
        NSLog(@"result: %@", resultDict[@"rect"]);
        UIView* view = [[UIView alloc] initWithFrame:[resultDict[@"rect"] CGRectValue]];
        [view setBackgroundColor:[UIColor redColor]];

        [self.label addSubview:view];

        lastFoundIndex = (int)[resultDict[@"range"] rangeValue].location + 1;
    }
} while (resultDict != nil);

UITextView:

int lastFoundTextIndex = 0;
NSDictionary* resultTextDict = nil;

do
{
    resultTextDict = [self.textview rectForString:@"#OPTION" fromIndex:lastFoundTextIndex];

    if (resultTextDict != nil)
    {
        NSLog(@"result: %@", resultTextDict[@"rect"]);
        UIView* view = [[UIView alloc] initWithFrame:[resultTextDict[@"rect"] CGRectValue]];
        [view setBackgroundColor:[UIColor redColor]];

        [self.textview addSubview:view];

        lastFoundTextIndex = (int)[resultTextDict[@"range"] rangeValue].location + 1;
    }
} while (resultTextDict != nil);
Community
  • 1
  • 1
unkgd
  • 671
  • 3
  • 12
  • 1
    It looks really helpful post, but i need it for objective-c, i'm trying to understand and convert it by myself, but it seems to be so hard ! – Fisfej Feb 08 '17 at 14:22
  • @Fisnik, if you look into the posts I added, they each have an Objective-C variation for most of the extension, just create an extension class for UILabel / UITextView in objective c and variate the code – unkgd Feb 08 '17 at 14:29
  • @Fisnik, I've added an Objective-C variation to make it easier for you to use – unkgd Feb 08 '17 at 14:59
  • @unkgd Thanks for the great response. – Fisfej Jun 09 '17 at 01:18