AliSoftware / OHAttributedLabel

UILabel that supports NSAttributedString
https://github.com/AliSoftware/OHAttributedLabel/wiki
1.51k stars 344 forks source link

How to make touchable basic markups? #113

Closed haashem closed 11 years ago

haashem commented 11 years ago

Hi thanks for developing this amazing API. Im interested in basicMarkupParser. so here is my scenario: Im developing a prose book and it has a lot of difficult word which the user needs to know its meaning. I need to parse these words with special tag like $word$ (for example this tag highlight the word).

so when the user touch it, an event raises and show its meaning in an alert view.

how can I make these words touchable using OHASBasicMarkupParser?

AliSoftware commented 11 years ago

You won't use OHASBasicMarkupParser directly for that, as it is designed to support the basic markup as seen in GitHub, but you can easily create your own parser by simply subclassing OHASMarkupParserBase. I especially designed it so it would be as simple as possible for anyone to add their own custom markup parsers so that's exactly what you should do!

To do this, simply create a subclass of OHASMarkupParserBase and override the +tagMappings class method. In this method, return an NSDictionary whose keys are a regular expression to match, and the associated values are a block that will take as parameters the matched string portion (as an NSAttributedString) and the regex match (as an NSTextCheckingResult) and that will return the transformedNSAttributedString`


You can use the OHASBasicMarkupParser as an example, especially the last key/Value pair of the build NSDictionary that add links when it finds text matching [text](link), as the principle will be quite the same for you. Simply adjust the regex to match \$(.*)\$ instead (*), and use the corresponding block to add a link to your NSAttributedString with the setLink:range: method


You can add any URL you want, so in your case you will probably add some custom URL scheme (say add the url "special:yourword" on the word yourword) and use the delegate method to catch taps on those words. You can use inspiration in my Demo project where I demonstrate this possibility in the "Custom Link" tab to show how to catch taps on custom links (in my example it is on words preceded by @ that I use as "mentions", but the principle remains the same in your case)

() be careful when building your RegEx, $ is a special character in regex, that's why I escape it with a backslash here, and don't forget to double your backslashes when putting it in your code, as I did in OHASBasicMarkupParser too… so you will probably end up with @"\$(._)\$" in your code._

AliSoftware commented 11 years ago

This would give you something like this, that I just typed here. (Disclaimer: I didn't check this code at all, just took the code in OHASBasicMarkupParser and adapted it right here from GitHub the returned dictionary of regular expressions to match your issue, I didn't test it is Xcode at all)

@implementation YourSpecialWordsMarkupParser

+(NSDictionary*)tagMappings
{
    return [NSDictionary dictionaryWithObjectsAndKeys:
        ^NSAttributedString*(NSAttributedString* str, NSTextCheckingResult* match)
            {
                NSRange wordRange = [match rangeAtIndex:1]; // extract the range of the word (index 1 because we want the text captured by the first set of parenthesis in the regex)
                if (wordRange.length>0)
                {
                    // Extract just the word (without the dollar signs) and make it mutable so we can add a link to it
                    NSMutableAttributedString* foundWord = [[str attributedSubstringFromRange:wordRange] mutableCopy];
                    NSURL* specialLink = [NSURL URLWithString:[NSString stringWithFormat:@"special:%@",word]]; // create a link like "special:yourword"
                    [foundString setLink:specialLink range:NSMakeRange(0, foundWord.length)]; // add the link
                    return MRC_AUTORELEASE(foundWord);
                } else {
                    return nil;
                }
            }, @"\\$(.+?)\\$", /* "$word$"

}

@end

Then add a delegate to your OHAttributedLabel and use the methods of OHAttributedLabelDelegate to catch when the links are tapped like I do in the CustomLinksViewController.m file of my Demo project.

haashem commented 11 years ago

thanks for your cooperation. still not reached any milestone! I have some problems:

I tried to use your example so I add your key, value with some rehearsal:

 ^NSAttributedString*(NSAttributedString* str, NSTextCheckingResult* match)
            {
                NSRange wordRange = [match rangeAtIndex:1]; // extract the range of the word (index 1 because we want the text captured by the first set of parenthesis in the regex)
                if (wordRange.length>0)
                {
                    // Extract just the word (without the dollar signs) and make it mutable so we can add a link to it
                    NSMutableAttributedString* foundWord = [[str attributedSubstringFromRange:wordRange] mutableCopy];
                    NSURL* specialLink = [NSURL URLWithString:[NSString stringWithFormat:@"special:%@",foundWord]]; // create a link like "special:yourword"
                    [foundWord setLink:specialLink range:NSMakeRange(0, foundWord.length)]; // add the link
                    return MRC_AUTORELEASE(foundWord);
                } else {
                    return nil;
                }
            }, @"\\$(.+?)\\$",

if I add this special link to the foundWord, the word wont be highlighted! but if I add a fancy or real link, the word would be highlighted but after touching it, nothing happens. (the delegate method wont be called!! how can i fix this issue? Screen Shot 2013-01-10 at 6 55 38 PM )

AliSoftware commented 11 years ago
  1. Try using "special://%@" instead of "special:%@" maybe to add the link
  2. Contrary to the very basic solution I gave you that directly use foundWord, don't forget to add percent escapes to your foundWord to be sure the generated URL is correct, even if the foundWord contain spaces, or special characters, etc.

All that about creating a valid NSURL is not related to OHAttributedLabel directly and it is up to you to choose the custom URL you want. Did you look at my example I use in my demo project?


About the delegate method not being called:

  1. Be sure you have userInteractionEnabled set to YES on your OHAttributedLabel instance so it catch taps and user interaction (if the link becomes highlighted when you tap on it you're OK)
  2. And of course, that's obvious but I prefer to remind it, check that you assigned an object (implementing the OHAttributedLabelDelegate protocol, of course) to the delegate property of your OHAttributedLabel, of course it must not be nil if you want the delegate's method to be called
haashem commented 11 years ago

I've uploaded my test with your API here: http://upload.ugm.ac.id/133OHAttributedLabel-master%202.zip all your recommendations I have conformed. I checked every thing but couldn't reach any results! I appreciate if you could check it.

  1. I need to show a popover above my special attributed words. so I need to find the coordinates of a substring. how do I find the bounding box for that word? Does OHAttributedLabel give me the rect of a particular link?
  2. why OHAttributedLabel uses UILabel not UITextView?

Screen Shot 2013-01-11 at 7 16 42 PM

AliSoftware commented 11 years ago

About your project provided in your ZIP file

  1. Your project does not compile (missing files) so I can't test it as is
  2. You seem to have modified the sources of OHAttributedLabel and OHASBasicMarker (instead of creating a subclass in your own project) — you are not supposed to modify my sources to do what you want to do, as I explained above already
  3. You also seem to have added some global variable SMCalloutView* calloutView… what the hell? You do know that global variables are evil and will likely to cause you problems (like multithreading issues and crashes) right?
  4. As I expected, the reason why the delegate method is not called is because the delegate is not set. You added your "$Some$" word on one OHAttributedLabel of my example project which does not have a delegate (contrary to the one on top for which I demonstrate its use). But I already explained exactly that to you in my comment above (point 2), so it seems you didn't even check that point…

For your first problem with your link on $xxx$ words:

I tried the code I gave you above in my project and with a simple breakpoint I realized that I created the link with [NSString stringWithFormat:"special:%@",foundWord] where foundWord is an NSAttributedString. So of course the link is invalid and specialLink is nil, and replacing with the NSString corresponding to this attributed string solves the problem in 2 seconds. Frankly you could have found that yourself using a simple breakpoint or some NSLog or whatever; I told you I didn't test my code and wrote it directly on GitHub without testing it on Xcode, so I expected you at least tried some easy debugging by yourself…

About your other questions:

  1. OHAttributedLabel does not provide the possibility to find coordinates of a substring when it has been laid out. OHAttributedLabel uses the CoreText framework to layout NSAttributedStrings, using a CTFrameSetter that lays out words for us. Layout out words of an NSAttributedString and knowing at which position to draw them is very complex, because you have to take multiple font sizes and styles into account, in addition to the text alignment and paragraph properties chosen. So there is no easy way to compute that rect, and I won't have time to add this soon, but feel free to search and find a solution yourself, probably using the functions provided by the CoreText framework, and suggest a Pull Request if you find a way
  2. It is not a subclass of UITextView because it is complex enough to draw attributed text using CoreText, but it would be way far more complex to make this NSAttributedText editable, managing the caret / insertion point, the selection of words, and much more (but once again, you're welcome to propose some solution for that)
haashem commented 11 years ago

wow thanks a lot for your help. I made a property for callOutView and synthesized it. I set a delegate for basicMarkupLabel: self.basicMarkupLabel.delegate = self; I corrected the special link: NSURL* specialLink = [NSURL URLWithString:[NSString stringWithFormat:@"%@",[foundWord string]]];

now everything works fine!

question: in basicDemoViewController even CustomViewController you haven't explicitly assigned objects to the OHAttributedLabelDelegate (if so I can't see clearly). but I see delegates are being called. how it happens?

AliSoftware commented 11 years ago

You mean you didn't even look in the XIB file?

Note: I'm closing this issue now that the original problem of detecting $xxx$ markup is solved.

haashem commented 11 years ago
  1. I have a strange problem. I have to put a very long text in my label, so I had to add it to an ScrollView. before that I could use CGRect linkRunRect = CTRunGetTypographicBoundsAsRect(run, line, lineOrigins[lineIndex]); to determine origin of links and show my popover on that. but after adding the label to the UIScrollView, the

-(void)drawActiveLinkHighlightForRect:(CGRect)rect

won't call and I have to touch the link for more until the event fires! (before adding the label to the UIScrollView, only with a tap the event fires.) why this happens?

  1. how can I change the color of links and remove their underline? and draw my custom background like the below image: Screen Shot 2013-01-15 at 6 45 47 PM Screen Shot 2013-01-14 at 10 31 06 PM
AliSoftware commented 11 years ago
  1. This is probably because of the delayedContentTouch of the scrollview (quite classic, not related to OHAttributedLabel itself), look in the Events Programming Guide and all
  2. To change the color of links and underlines, you have a property for that. I even demonstrate it in my Demo project using UIAppearance (just look in the headers of OHAttributedLabel really)
haashem commented 11 years ago

thanks for your sparks. touching delay solved! the solution is here: http://stackoverflow.com/questions/3642547/uibutton-touch-is-delayed-when-in-uiscrollview

haashem commented 11 years ago

another problem: OHAttributedLabel can't detect links for persian or arabic characters. for instance if we have: $الله$ this word wont show as a link: Screen Shot 2013-01-17 at 9 32 05 PM

even though your custom link markups if they persian or arabic characters: Screen Shot 2013-01-17 at 9 37 16 PM

AliSoftware commented 11 years ago

The automatic links detection and persian characters is not an issue with OHAttributedLabel but an issue with Apple's NSDataDetector class itself (OHAttributedLabel uses an instance of NSDataDetector and ask it to detect links) and Apple's RegEx engine. I have no control on this one.

The only hope is in the custom links creation, where I use an NSRegularExpression instance for which you can indicate the options parameter, maybe you can try adding the NSRegularExpressionUseUnicodeWordBoundaries option to the ones already listed in my code and check if it works better.

haashem commented 11 years ago

but NSData detector can detect this fancy link: Screen Shot 2013-01-18 at 10 25 36 AM

I want to add special links to my persian words (word: foundWord _ this kind of special links). so when foundWord is persian, the word wont be shown as a link! I used NSRegularExpressionUseUnicodeWordBoundaries but nothing happened. I need your help to fix this issue. maybe something is missed.

haashem commented 11 years ago

I checked and searched I guess thats not because of NSRegularExpression or NSDataDetector. I did something: [foundString setLink:[NSURL URLWithString:[linkString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]] range:NSMakeRange(0, foundString.length)];

it should solve the issue of unicode characters of in URL and always return a valid url. but the strange problem is when I debug the code in OHASBasciMarkupParser, debugger always jump over this line of this code and eve other code I write. would you check this:

            if (wordRange.length>0)
            {
                // Extract just the word (without the dollar signs) and make it mutable so we can add a link to it
                NSMutableAttributedString* foundWord = [[str attributedSubstringFromRange:wordRange] mutableCopy];

                NSURL* specialLink = [NSURL URLWithString:[NSString stringWithFormat:@"word:%@",[[foundWord string] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; // create a link like "special:yourword"
                NSLog(@"%@", specialLink.description);
                [foundWord setLink:specialLink range:NSMakeRange(0, foundWord.length)]; // add the link
                return MRC_AUTORELEASE(foundWord);
            } else {
                return nil;
            }
        }, @"\\$(.+?)\\$",
AliSoftware commented 11 years ago

You mean you didn't add your percent escapes already ?

I though you had, because I told you in my previous comments above already, so of course if you didn't follow my advice yet and add these escapes before that's totally normal, as I explained already above 8 days ago…

haashem commented 11 years ago

I think that was a bug of x-code because it always jumping from my code and didn't debug some of them. so I extracted another copy of OHAttributedLabel and used above code, and works well.

another problem: but when I click on the link, the highlighted link bound won't sit in its original position for persian word (for english characters it sits in the right position).

Screen Shot 2013-01-19 at 10 36 18 AM

haashem commented 11 years ago

performance problem:

I created a subclassed label of OHAttributedLabel and populated its text property with a very very long text (think a bout a chapter of book). then added this label to a scrollview. so when I click on a link it takes almost 5 second to highlight link background color and fire its delegate.

why this happens? does it needs multithreading? how should I fix this?

AliSoftware commented 11 years ago

1) I can't understand how you can continue using the same issue to report what is so far no less than 4 different problems since the beginning, the last question isn't even closely related to the issue we are in right now… you're creating a mess in my issue tracker tool there. What about other people that could have answered you or that could have found your question/issue/solutions if you had open a dedicated issue? Please use the issue tracker tool correctly.

2) No direct solution, any performance improvement is welcome. Feel free to improve my code (that's why it's open-source) and suggest a pull request when you have a solution to improve that.