How would I elegantly achieve multiline text that includes a combination of links, plain text, and formatted words? Right now, all I can think of is breaking each word into a token and displaying/positioning it differently (UILabel, UIButton, etc), however, this is terribly inefficient. Here is an example of what I am trying to achieve from the comments feature in Instagram.
Notice, formatted link to user name, indented multiline text, and possibility of inline links to hashtags.
Edit Sep 14
So I was able to implement something very similar using FTCoreTextView
It was simple to import/implement. I am using the code as a static library in my ARC project.
The following are the core components:
I use a method to set the style for my cell's FTCoreTextView
(NSArray *)getCoreTextStyle{
NSMutableArray *result = [NSMutableArray array];
FTCoreTextStyle *defaultStyle = [FTCoreTextStyle new];
defaultStyle.name = FTCoreTextTagDefault; //though the default name is already set to FTCoreTextTagDefault
defaultStyle.font = [UIFont fontWithName:#"Helvetica Neue" size:14.f];
defaultStyle.textAlignment = FTCoreTextAlignementLeft;
[result addObject:defaultStyle];
FTCoreTextStyle *linkStyle = [defaultStyle copy];
linkStyle.name = FTCoreTextTagLink;
linkStyle.color = [UIColor blueColor];//[del colorWithHexString:#"3aa98"];
linkStyle.font = [UIFont fontWithName:#"Helvetica Neue" size:14.f];
[result addObject:linkStyle];
return result;
}
Then in your cell you would set the text for the view, assign a delegate, etc. (My FTCoreTextView overrides a plain IBOutlet for a UIView)
[myFTTView setDelegate:self];
[myFTTView addStyles:[self getCoreTextStyle]];
[myFTTView setText:[self parseLinks:text]];
[myFTTView setNeedsDisplay];
Parse links is a method i separately define to search for hashtags and usernames and then set a style when they are found (this code is rough and leaves much to be improved. I am considering switching to CoreTextHyperlink which already has code search for hashtags and #usernames.)
-(NSString *) parseLinks: (NSString *)cap{
NSArray *split = [cap componentsSeparatedByString:#" "];
NSMutableArray *words = [[NSMutableArray alloc] initWithArray:split];
for (NSString *word in split){
if([word hasPrefix:#"#"] || [word hasPrefix:#"#"]){
NSString *reformat = [NSString stringWithFormat:#"%#%#|%#%#", hashlink,word, word, endhashlink];
[words replaceObjectAtIndex:[split indexOfObject:word] withObject:reformat];
}
}
return [words componentsJoinedByString:#" "];
}
Last but not least, the method called after a tag/username is clicked
- (void)coreTextView:(FTCoreTextView *)acoreTextView receivedTouchOnData:(NSDictionary *)data {
NSString *key = [data objectForKey:#"url"];
if([key hasPrefix:#"#"]){
//do something for tag
}
else{
//do something for user.
}
return;
}
Two ways I can think of:
use CoreText with NSAttributedStrings. Difficult to achieve, especially for the links (you'll have to register a listener somewhere, compute on which "link" you "clicked", ...), but provides the best performance.
use a UIWebView and write the HTML on-the-fly. Less performant, but much more flexible.
Related
I am dividing string and and storing it in splitArray and want to return it.
But I am getting conflicting array on the first line
- (NSArray *)subdividingString:(NSString *)string {
NSArray *splitArray = [string componentsSeparatedByString:#" "];
return splitArray;
}
First: there is nothing wrong with the code, but you are most likely having another issue (e.g. where you call subdividingString:).
Second: You shouldn't introduce a method that is exactly doing what another one is doing already. Just use
NSString *mystring = #"some string";
NSArray *chunks = [mystring componentsSeparatedByString:#" "];
I would like to add words to the Apple Dictionary used by UITextChecker, and I am aware of the learnWords: method. It's documentation, and my experience with it, suggest that it only serves to ignore words that are learned. What I would like, and what seems obvious, would be to have learned words also provided as guesses and completions provided by the UITextChecker. Is that possible?
Here's the code I'm starting with:
NSString *newWord = [NSString stringWithFormat:#"%#", #"xyz"];
if ([UITextChecker hasLearnedWord:newWord]) {
NSLog(#"skipped %#", newWord);
} else {
[UITextChecker learnWord:newWord];
NSLog(#"learning %#", newWord);
}
A user might enter "xyj" into a search field, which would prompt this code to run:
// Fake User Input
NSString *word = #"xyj";
UITextChecker *checker = [[UITextChecker alloc] init];
NSRange range = NSMakeRange(0, word.length);
NSArray *guesses = [checker guessesForWordRange:range inString:word language:#"en_US"];
for (NSString *guess in guesses) {
if ([guess isEqualToString:#"xyz"]) {
NSLog(#"Success, iOS suggested xyz!");
}
}
I've got an NSAttributedString that looks like
"Somestring bold %# template %f %d blah blah blah"
I want to be able to replace the format template parts just like in [NSString stringWithFormat:...] but keeping the styling, so that the replaced strings match the styling around them (all bold in my example above).
Is there a way to iterate through each format % placeholder one by one so I can use a list of arguments to fill the string?
I don't want to build my own % implementation because I know theres a million and one different formats.
Or is there an easier solution that I'm overlooking?
Edit:
I'll explain a bit of the complete solution I'm solving:
To make it possible for my team to attribute localized strings I've already got a way to write
"key"="test //italic %#// **bold** __underline %d__";
If a specifier is between attributed tags I want that part to be attributed. Currently I can create an attributed string as seen above, the next part is to take care of the specifiers left over.
I'm doing it in the order of Parse attributes -> Apply arguments..., I could easily solve it the other way but I don't want format arguments to mess with the attributes
Big thanks to #Rick for pointing me in the right direction.
His post recommends first going over the arguments and pre-escaping any string or character objects before applying the format strings. This brought me back to another problem I was having earlier, in trying to iterate an argument list of different types (NSString, int etc), like NSLog does. I think I found that it is impossible (or at least really difficult) to do this, and the reason NSLog can is that it knows what types to expect through the format specifiers (%#, %i etc).
I realize I can actually get the same effect not by escaping the arguments, but by escaping the format string itself.
Example:
format: "test //italic %#//"
args: "text // more text"
Steps:
First replace all instances of // with //-TAG-//
Apply the arguments
Determine where styling applies between //-TAG-//
Obviously //-TAG-// can still be written in the arguments to mess up the styling, however depending on what you use as a replacement, the chances of this happening are essentially zero.
I'm doing it in the order of Parse attributes -> Apply arguments..., I
could easily solve it the other way but I don't want format arguments
to mess with the attributes
Why not simply add an escape-character? From what I understand you run the risk of the example you provided getting messed up if the first string contains a double slash?
"key"="test //italic %#// **bold** __underline %d__";
if %# is text // more text that would screw up the formatting.
If so, then simply parse every vararg of the type NSString and char to make sure that they don't contain any of the characters you reserved for your attributes. If they do, add some escape char before which you remove upon parsing the attributes.
The above example would look like this after applying the arguments:
"key"="test //italic text \/\/ more text// **bold** __underline 34__";
After which you parse the attributes, same way as before but you ignore characters preceded by \ and make sure to remove the \.
It's a bit of effort but I bet it's a lot less than implementing your own printf-style parser.
Here is working code:
#import <Foundation/Foundation.h>
#interface NSAttributedString (AttributedFormat)
- (instancetype)initWithFormat:(NSAttributedString *)attrFormat, ...;
- (instancetype)initWithFormat:(NSAttributedString *)attrFormat arguments:(va_list)arguments;
#end
#implementation NSAttributedString (AttributedFormat)
- (instancetype)initWithFormat:(NSAttributedString *)attrFormat, ... {
va_list args;
va_start(args, attrFormat);
self = [self initWithFormat:attrFormat arguments:args];
va_end(args);
return self;
}
- (instancetype)initWithFormat:(NSAttributedString *)attrFormat arguments:(va_list)arguments {
NSRegularExpression *regex;
regex = [[NSRegularExpression alloc] initWithPattern: #"(%.*?[#%dDuUxXoOfeEgGccsSpaAF])"
options: 0
error: nil];
NSString *format = attrFormat.string;
format = [regex stringByReplacingMatchesInString: format
options: 0
range: NSMakeRange(0, format.length)
withTemplate: #"\0$1\0"];
NSString *result = [[NSString alloc] initWithFormat:format arguments:arguments];
NSMutableArray *f_comps = [format componentsSeparatedByString:#"\0"].mutableCopy;
NSMutableArray *r_comps = [result componentsSeparatedByString:#"\0"].mutableCopy;
NSMutableAttributedString *output = [[NSMutableAttributedString alloc] init];
__block int consumed_length = 0;
[attrFormat enumerateAttributesInRange:NSMakeRange(0, attrFormat.length) options:0 usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
NSMutableString *substr = [NSMutableString string];
while(f_comps.count > 0 && NSMaxRange(range) >= consumed_length + [(NSString *)f_comps[0] length]){
NSString *f_str = f_comps[0];
NSString *r_str = r_comps[0];
[substr appendString:r_str];
[f_comps removeObjectAtIndex:0];
[r_comps removeObjectAtIndex:0];
consumed_length += f_str.length;
}
NSUInteger idx = NSMaxRange(range) - consumed_length;
if(f_comps.count > 0 && idx > 0) {
NSString *f_str = f_comps[0];
NSString *leading = [f_str substringToIndex:idx];
[substr appendString:leading];
NSString *trailing = [f_str substringFromIndex:idx];
[f_comps replaceObjectAtIndex:0 withObject:trailing];
[r_comps replaceObjectAtIndex:0 withObject:trailing];
consumed_length += idx;
}
[output appendAttributedString:[[NSAttributedString alloc] initWithString:substr attributes:attrs]];
}];
return [self initWithAttributedString:output];
}
#end
Usage example:
NSMutableAttributedString *fmt = [[NSMutableAttributedString alloc] initWithString:#"test: "];
[fmt appendAttributedString: [[NSAttributedString alloc] initWithString: #"Some%%string"
attributes: #{
NSFontAttributeName: [UIFont systemFontOfSize:17]
}]];
[fmt appendAttributedString: [[NSAttributedString alloc] initWithString: #"bold %# template %.3f %d"
attributes: #{
NSFontAttributeName: [UIFont boldSystemFontOfSize:20],
NSForegroundColorAttributeName: [UIColor redColor]
}]];
[fmt appendAttributedString: [[NSAttributedString alloc] initWithString: #"%# blah blah blah"
attributes: #{
NSFontAttributeName: [UIFont systemFontOfSize:16],
NSForegroundColorAttributeName: [UIColor blueColor]
}]];
NSAttributedString *result = [[NSAttributedString alloc] initWithFormat:fmt, #"[foo]", 1.23, 56, #"[[bar]]"];
Result:
Maybe this still have some bugs, but it should work in most cases.
(%.*?[#%dDuUxXoOfeEgGccsSpaAF])
This regex matches "Format Specifiers". specifier begin with % and end with listed characters. and may have some modifiers between them. It's not perfect, for example this illegal format "%__s" should be ignored but my regex matches this whole string. but as long as the specifier is legal one, it should works.
My code matches it, and insert delimiters around the specifiers:
I'm %s.
I'm <delimiter>%s<delimiter>.
I use \0 as a delimiter.
I'm \0%s\0.
then interpolate it.
I'm \0rintaro\0.
then split the format and the result with the delimiter:
f_comps: ["I'm ", "%s", "."]
r_comps: ["I'm ", "rintaro", "."]
Here, total string length of f_comps is exact the same as original attributed format. Then, iterate the attributes with enumerateAttributesInRange, we can apply the attributes to the results.
I'm sorry but it's too hard to explain the jobs inside enumerateAttributesInRange, with my poor English skills :)
i have implemented a search bar that searching trough an array of countries(presented in a picker view), the problem is that the user need to type the full country name that it will find it and i want him to be able to type even one letter and it will show the first country that starts with that letter and if types another than it sorts even further etc etc.
Anyone have any ideas??
for(int x = 0; x < countryTable.count; x++){
NSString *countryName = [[countryTable objectAtIndex:x]objectForKey:#"name"];
if([searchedStr isEqualToString:countryName.lowercaseString]){
[self.picker selectRow:i inComponent:0 animated:YES];
flag.image = [UIImage imageNamed:[[countryTable objectAtIndex:i]objectForKey:#"flag"]];
}
}
There's a method on NSArray called filteredArrayUsingPredicate: and a method on NSString called hasPrefix:. Together they do what you need...
NSString *userInput = //... user input as lowercase string. don't call this countryName, its confusing
NSPredicate *p = [NSPredicate predicateWithBlock:^BOOL(id element, NSDictionary *bind) {
NSString countryName = [[element objectForKey:#"name"] lowercaseString];
return [countryName hasPrefix:userInput];
}];
NSArray *filteredCountries = [countryTable filteredArrayUsingPredicate:p];
If you're on iOS 8 or OS X Yosemite, you can do:
NSString *country = countryName.lowercaseString; //"england"
NSString *needle = #"engl";
if (![country containsString:needle]) {
NSLog(#"Country string does not contain part (or whole) of searched country");
} else {
NSLog(#"Found the country!");
}
Else, if on versions below iOS 8:
NSString *country = countryName.lowercaseString; //"england"
NSString *needle = #"engl";
if ([country rangeOfString:needle].location == NSNotFound) {
NSLog(#"Country string does not contain part (or whole) of searched country");
} else {
NSLog(#"Found the country!");
}
Lastly, just iterate through all possible countries and apply this to them all. There might exist more robust solutions out there (like danh's solution with some smaller modifications), but this is by far the easiest to start with.
This is my first question to Stack Overflow. I have been using this site for a while and have used it's resources to figure out answers to my programming questions but I'm afraid I can't find the answer I'm looking for this time.
I've created these five strings:
//List five items from the book and turn them into strings
//1 Josh the Trucker
NSString *stringJosh = #"Josh the Trucker";
//2 The Witch from the Remote Town
NSString *stringWitch = #"The Witch from the Remote Town";
//3 Accepting the curse rules "Willingly and Knowingly"
NSString *stringRules = #"Accepting the curse rules, --Willingly and Knowingly--";
//4 Josh's time left to live--Five Days Alive Permitted
NSString *stringFiveDays = #"Josh's time left to live--Five Days Alive Permitted";
//5 The Fire Demon Elelmental
NSString *stringDemon = #"The Fire Demon Elelmental";
Then, I've put them in an array:
//Create an array of five items from the book
NSArray *itemsArray = [[NSArray alloc] initWithObjects:
stringJosh,
stringWitch,
stringRules,
stringFiveDays,
stringDemon,
nil];
Then, I created this mutable string where I need to loop through the array and append the items to a UIlabel.
NSMutableString *itemsString = [[NSMutableString alloc] initWithString:
#"itemsArray"];
Here's the loop, which displays the items in the console log.
for (int i=0; i<5; i++)
{
NSLog(#"Book Item %d=%#", i, itemsArray[i]);
}
My question is, how do I append these items into the UIlabel?
These functions are in my appledelegate.
In my viewDidAppear function (flipsideViewController) I have:
label8.text =""----thats where the looped info needs to go.
How do I do this?
I feel I need to put them together and append where the NSLog should be...but how do I transfer that info to the textlabel?
I hope I explained myself.
We haven't done ANY append examples, I guess this is where I need to get answers from the "wild"
This is the wildest coding environment I know so I'm hoping I can find some direction here.
Thanks for taking a look!
Once you have all your strings that you want to concatenate in NSArray you can combine them with single call (with whatever separator you want):
NSString *combinedString = [itemsArray componentsJoinedByString:#" "];
If you need more complex logic you can use NSMutableString to create result you want while iterating array, i.e.:
NSMutableString *combinedString = [NSMutableString string];
[itemsArray enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL *stop) {
[combinedString appendFormat:#"Book Item %d=%# ", idx, obj];
}];
Note also that it is better to iterate through collections using fast enumeration or block enumeration rather than using plain index-based for loop.
NSMutableString *labelText = [NSMutableString string];
int i = 0;
for (NSString *item in itemsArray)
[labelText appendFormat:#"Book Item %d=%#\n", i++, item];
label8.text = labelText;
DO this
UILabel *mainlabel;
mainlabel.text = [origText stringByAppendingString:get];
Add your text to mainlabel.. orig text is mutable string or else in forloop just append array object at index text to label.put above line of code in forloop