NSMutableAttributedString will not display attribute that begins at nonzero index - ios

UPDATE:
I created a really simple standalone project to demonstrate the bug. If anyone would like to pull same and see if they can spot where I've gone wrong, I'd sure appreciate it. There's not much code to look through. Public repo here:
https://github.com/reidnez/NSAttributedStringBugDemo
I'm having a very strange issue here: I have a tableview. Each cell has a title label with 1-3 words, and a keywords label with several CSV keywords. I also have a search bar. The requirement is that as the user types into the search bar, any partial matches on both the title and keywords for each cell are shown highlighted. Screenshots:
First image is A-Okay. In the second image, the "an" of the title label should be highlighted. But, as you can see, not so much...
This works perfectly fine on the "keywords" label, as you can see above. The attributed strings for both of these labels are created by a category I wrote (code below). The same method is called on both strings, and appears to behave the same from what the debugger is telling me. The UI tells a different story.
I have stepped through the debugger numerous times, and in all cases, the attributed string appears to have been configured correctly. I have also verified that something else is not calling [tableView reloadData] and that no other place in my code is overwriting the label's value. This is how matching on "an" for "Fang" looks in the debugger, just before the cell is returned at the end of cellForRowAtIndexPath:
(lldb) po customCell.entryTitleLabel.attributedText
F{
}an{
NSBackgroundColor = "UIDeviceRGBColorSpace 0.533333 0.835294 0.156863 1";
}g{
}
Looks good to me...that is exactly what I want. But when the cell renders, there are NO highlights to be seen! Weirder yet, as an experiment I tried setting the label to a completely arbitrary attributedString that I created right in cellForRow:
NSMutableAttributedString *fake = [[NSMutableAttributedString alloc] initWithString:#"Fang"];
[fake addAttribute:NSBackgroundColorAttributeName value:MATCH_TEXT_HILIGHT_COLOR range:NSMakeRange(1, 2)];
customCell.entryTitleLabel.attributedText = fake;
This, too fails. No highlighting at all...but I CAN highlight any substring in the range of {0, 1} to {0, fake.length} and it behaves as expected. Again, it seemingly refuses to highlight any substring that does not begin at index 0--but only for the title label.
Am I losing my mind? What am I missing?
Below is my category...but I am fairly confident the problem does not lie here, because it functions perfectly for the keywords string, and (again) the attributes appear to be set correctly just before the cell returns:
-(void)hilightMatchingSubstring:(NSString*)substring color:(UIColor*)hilightColor range:(NSRange)range
{
if ([self.string compare:substring options:NSCaseInsensitiveSearch] == NSOrderedSame) {
[self addAttribute:NSBackgroundColorAttributeName value:hilightColor range:NSMakeRange(0, self.length)];
return;
}
// Sanity check. Make sure a valid range has been passed so that we don't get out-of-bounds crashes. Default to return self wrapped in an attributed string with no attributes.
NSRange selfRange = NSMakeRange(0, self.length);
if (NSIntersectionRange(selfRange, range).length == 0) {
NSLog(#" \n\n\n*** Match range {%lu, %lu} does not intersect main string's range {%lu, %lu}. Aborting *** \n\n\n", (unsigned long)range.location, (unsigned long)range.length, (unsigned long)selfRange.location, (unsigned long)selfRange.length);
return;
}
if (substring.length > 0) {
NSRange movingRange = NSMakeRange(range.location, substring.length);
if (NSMaxRange(movingRange) > self.length) {
return;
}
NSString *movingString = [self.string substringWithRange:movingRange];
while (NSMaxRange(movingRange) < NSMaxRange(range)) {
if ([movingString compare:substring options:NSCaseInsensitiveSearch] == NSOrderedSame) {
[self addAttribute:NSBackgroundColorAttributeName value:hilightColor range:movingRange];
}
movingRange = NSMakeRange(movingRange.location + 1, substring.length);
movingString = [self.string substringWithRange:movingRange];
}
} // This is fine...string leaves properly attributed.
}

Thanks for writing this up... Thought I was going crazy too!
I came up with a workaround (read: hack) whilst we wait for something official from Apple.
NSDictionary *hackAttribute = [NSDictionary dictionaryWithObjectsAndKeys:
[UIColor clearColor], NSBackgroundColorAttributeName, nil];
NSMutableAttributedString *attributedText =
[[NSMutableAttributedString alloc] initWithString:#"some text..."];
[attributedAddressText setAttributes:hackAttribute range:NSMakeRange(0, attributedText.length)];
// Then set your other attributes as per normal
Hope that helps.

Related

How can I add different line spacing for different parts of NSAttributedString?

I want to have a single NSAttributedString contain multiple messages. If a single message has a long text and it wraps around, I want to have a line spacing of, say, 5. Because I have a single NSAttributedString contain multiple messages, I want to have a bigger line spacing between each message; let's say 20.
What I want
The 'I see' is one message. The 'I'd think it'd be both...' is one message, although it wraps down to two lines and 'Like a one way chat' is one message.
Notice how the line spacing between the 2nd and 3rd is smaller than the 1st and 2nd and between the 3rd and 4th.
What I've tried
I am appending a \n to the end of each message and I've tried using NSParagraphStyle which gives me control of the line spacing, but it seems to be all or nothing:
// index is the index of the group of messages as I iterate through them
// contentText is an NSMutableAttributedString
if index != messages.count - 1 {
let style = NSMutableParagraphStyle()
style.lineSpacing = 40.0
let lineReturn = NSMutableAttributedString(string: "\n")
contentText.appendAttributedString(lineReturn)
if index == 0 {
contentText.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(contentText.length-lineReturn.length, lineReturn.length))
} else {
contentText.addAttribute(NSParagraphStyleAttributeName, value: style, range: NSMakeRange(contentText.length-lineReturn.length-1, lineReturn.length+1))
}
}
If I add the line spacing to the beginning it will set the line spacing for the entire label.
if index == 0 {
let style = NSMutableParagraphStyle()
style.lineSpacing = 40.0
contentText.addAttribute(NSParagraphStyleAttributeName, value: style1, range: NSMakeRange(start, 1))
}
(This is really only my latest try.)
Thanks for any help! :)
Details
Have very basic custom markup in your English message so you can
parse out the different pieces
Instruct your translators to leave the markup in and translate the
rest Have a UIView that can serve as the container of this message
Break your English message up in pieces to separate the regular text
from the clickable text
For each piece create a UILabel on the container UIView
For the clickable pieces, set your styling, allow user interaction
and create your tap gesture recognizer
Do some very basic bookkeeping to place the words perfectly across
the lines
For Understand.
In the view controller's viewDidLoad I placed this:
[self buildAgreeTextViewFromString:NSLocalizedString(#"I agree to the #<ts>terms of service# and #<pp>privacy policy#",
#"PLEASE NOTE: please translate \"terms of service\" and \"privacy policy\" as well, and leave the #<ts># and #<pp># around your translations just as in the English version of this message.")];
I'm calling a method that will build the message. Note the markup I came up with. You can of course invent your own, but key is that I also mark the ends of each clickable region because they span over multiple words.
Here's the method that puts the message together -- see below. First I break up the English message over the # character (or rather #"#" string). That way I get each piece for which I need to create a label separately. I loop over them and look for my basic markup of <ts> and <pp> to detect which pieces are links to what. If the chunk of text I'm working with is a link, then I style a bit and set up a tap gesture recogniser for it. I also strip out the markup characters of course. I think this is a really easy way to do it.
Note some subtleties like how I handle spaces: I simply take the spaces from the (localised) string. If there are no spaces (Chinese, Japanese), then there won't be spaces between the chunks either. If there are spaces, then those automatically space out the chunks as needed (e.g. for English). When I have to place a word at the start of a next line though, then I do need to make sure that I strip of any white space prefix from that text, because otherwise it doesn't align properly.
- (void)buildAgreeTextViewFromString:(NSString *)localizedString
{
// 1. Split the localized string on the # sign:
NSArray *localizedStringPieces = [localizedString componentsSeparatedByString:#"#"];
// 2. Loop through all the pieces:
NSUInteger msgChunkCount = localizedStringPieces ? localizedStringPieces.count : 0;
CGPoint wordLocation = CGPointMake(0.0, 0.0);
for (NSUInteger i = 0; i < msgChunkCount; i++)
{
NSString *chunk = [localizedStringPieces objectAtIndex:i];
if ([chunk isEqualToString:#""])
{
continue; // skip this loop if the chunk is empty
}
// 3. Determine what type of word this is:
BOOL isTermsOfServiceLink = [chunk hasPrefix:#"<ts>"];
BOOL isPrivacyPolicyLink = [chunk hasPrefix:#"<pp>"];
BOOL isLink = (BOOL)(isTermsOfServiceLink || isPrivacyPolicyLink);
// 4. Create label, styling dependent on whether it's a link:
UILabel *label = [[UILabel alloc] init];
label.font = [UIFont systemFontOfSize:15.0f];
label.text = chunk;
label.userInteractionEnabled = isLink;
if (isLink)
{
label.textColor = [UIColor colorWithRed:110/255.0f green:181/255.0f blue:229/255.0f alpha:1.0];
label.highlightedTextColor = [UIColor yellowColor];
// 5. Set tap gesture for this clickable text:
SEL selectorAction = isTermsOfServiceLink ? #selector(tapOnTermsOfServiceLink:) : #selector(tapOnPrivacyPolicyLink:);
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:selectorAction];
[label addGestureRecognizer:tapGesture];
// Trim the markup characters from the label:
if (isTermsOfServiceLink)
label.text = [label.text stringByReplacingOccurrencesOfString:#"<ts>" withString:#""];
if (isPrivacyPolicyLink)
label.text = [label.text stringByReplacingOccurrencesOfString:#"<pp>" withString:#""];
}
else
{
label.textColor = [UIColor whiteColor];
}
// 6. Lay out the labels so it forms a complete sentence again:
// If this word doesn't fit at end of this line, then move it to the next
// line and make sure any leading spaces are stripped off so it aligns nicely:
[label sizeToFit];
if (self.agreeTextContainerView.frame.size.width < wordLocation.x + label.bounds.size.width)
{
wordLocation.x = 0.0; // move this word all the way to the left...
wordLocation.y += label.frame.size.height; // ...on the next line
// And trim of any leading white space:
NSRange startingWhiteSpaceRange = [label.text rangeOfString:#"^\\s*"
options:NSRegularExpressionSearch];
if (startingWhiteSpaceRange.location == 0)
{
label.text = [label.text stringByReplacingCharactersInRange:startingWhiteSpaceRange
withString:#""];
[label sizeToFit];
}
}
// Set the location for this label:
label.frame = CGRectMake(wordLocation.x,
wordLocation.y,
label.frame.size.width,
label.frame.size.height);
// Show this label:
[self.agreeTextContainerView addSubview:label];
// Update the horizontal position for the next word:
wordLocation.x += label.frame.size.width;
}
}
if you want to use gesture then use this method.
- (void)tapOnTermsOfServiceLink:(UITapGestureRecognizer *)tapGesture
{
if (tapGesture.state == UIGestureRecognizerStateEnded)
{
NSLog(#"User tapped on the Terms of Service link");
}
}
- (void)tapOnPrivacyPolicyLink:(UITapGestureRecognizer *)tapGesture
{
if (tapGesture.state == UIGestureRecognizerStateEnded)
{
NSLog(#"User tapped on the Privacy Policy link");
}
}
Hope this helps. I'm sure there are much smarter and more elegant ways to do this, but this is what I was able to come up with and it works nicely.
this answer display output like following screen shot...but you got idea from this answer.
Gotcha!
You need to play with baselineOffset attribute:
let contentText = NSMutableAttributedString(
string: "I see\nI'd think it`d be both a notification and a\nplace to see past announcements\nLike a one way chat.")
contentText.addAttribute(.baselineOffset, value: 10, range: NSRange(location: 0, length: 5))
contentText.addAttribute(.baselineOffset, value: -10, range: NSRange(location: 85, length: 20))
Result:
"I see
I'd think it`d be both a notification and a
place to see past announcements
Like a one way chat."

Old RPG scrolling text, but break up text blocks with (dot dot dot)

In old RPGs, the dialogue text would type on screen, and if there was more than could fit on the page there'd be a ... and you'd press to NEXT to continue reading.
I've got a lot of this already working. What I'm stuck on is I need a block of text to programmatically know how to break itself up from page to page using the ...
Normally, the easy route would be to just specify in the dialogue where the breaks should be, but for this specific project I have to allow it to take in a large block of text, then break it into the correct sizes for each page.
Anyone have any thoughts on how to do this? Just counting characters won't work because the font won't be monospaced.
I was having a hell of a time trying to search for this. Found this, but it didn't answer my question.
Much appreciated!
One solution could be:
separate all the words from the big text into an array
go through of it and search for the boundaries based on textbox height
You can implement it eg.:
CGFloat w = 200.0; // RPG textbox width. Get it from actual UI object.
CGFloat h = 150.0; // RPG textbox height. Get it from actual UI object.
NSArray *words = [yourRPGtext componentsSeparatedByString:#" "];
NSString *cur_txt = [words objectAtIndex:0];
int i = 1;
RPG_txt_pages = [[NSMutableArray alloc] init];
while (i < [words count]) {
NSString *next_txt = [cur_txt stringByAppendingFormat:#" %#",[words objectAtIndex:i]];
CGSize size = [next_txt sizeWithFont:yourRPGtextlable.font
constrainedToSize:CGSizeMake(w, 99999)
lineBreakMode:UILineBreakModeWordWrap];
if (size.height > h) {
cur_txt = [cur_txt stringByAppendingString:#"..."];
[RPG_txt_pages addObject:cur_txt];
cur_txt = [words objectAtIndex:i];
} else {
cur_txt = next_txt;
}
i++;
}
[RPG_txt_pages addObject:curText];
The key here is NSString's sizeWithFont method: here is the link to the docs.
IOS7 comment: sizeWithFont is deprecated you can use sizeWithAttributes. Here is an SO answer on this.
If you tell what IOS version are you using I'll modify this answer. Hope it helped!

How to put different headIndent (alignment) for every paragraph in UITextView

In my application i need to align all the paragraph differently.
like, first paragraph's headIndent is 0.0f then second's 10.0f and third's 3.0f.
i am giving all the paragraph style to textview.attributedText. and it took only one style.
Here whole text will come dynamically by Typing. means when User will type in text view at that time. so, there are no static string to do this.
I am placing all the characters in UITextView by this...
UIFont *fontBold = [UIFont fontWithName:#"Helvetica-Bold" size:15];
attributesHelveticaBold = #{NSFontAttributeName :fontBold};
UIFont *fontNormal = [UIFont fontWithName:#"HelveticaNeue-Light" size:15];
attributesNormal = #{NSFontAttributeName :fontNormal};
if (varBold== 1) {
[textView setTypingAttributes:attributesHelveticaBold];
}
else {
[textView setTypingAttributes:attributesNormal];
}
And i want to get this kind of result in text view
When i am typing the typing become slow too.
but i think i'll come over that issue but for now i stuck on this alignment problem.
how to do it when bullet point come and when different text come.
any kind of link, code, tutorial will be great help...
---------- Edit : ----------
Please have a look in Evernote's application.
I need to do the exactly same thing in my app. for alignment of second,third,etc line when bullet come.
-------- Edit after searching :-------
I searched too much for this but ain't find anything by googling.
So, now i am asking if anyone now about How to give any paragraph style or any attribute style to a paragraph and just leave it as it is on text view and then perform other paragraph style on second paragraph. at this time the first paragraph will not pass throw "shouldChangeTextInRange" method.
yes, it's quite confusing whatever i am saying.
so i explaining it in general...
if user set the text view's first paragraph's headIndent=7.0f then when user will type next paragraph and set the headIndent = 13.0f then first paragraph will stay as it is in textview and just running paragraph will come in a chapter (means in a method).
right now i am doing these thing in shouldChangeTextInRange method to do style for each paragraph.
varStaringPointOfString = 0;
varEndingPointOfString = 0;
NSArray *sampleArrToGetattrStr = [txtViewOfNotes.text componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
for (int i=0; i<[sampleArrToGetattrStr count]; i++)
{
NSString *strtostoreLength = [NSString stringWithFormat:#"%#",[sampleArrToGetattrStr objectAtIndex:i]];
varStaringPointOfString = (int)strtostoreLength.length + varEndingPointOfString;
if ([strtostoreLength hasPrefix:#"\t•\t"])
{
[[textView textStorage] addAttribute:NSParagraphStyleAttributeName value:paragraphStyleForBullet range:NSMakeRange(varEndingPointOfString, strtostoreLength.length)];
}
else
{
[[textView textStorage] addAttribute:NSParagraphStyleAttributeName value:paragraphStyleNormal range:NSMakeRange(varEndingPointOfString, strtostoreLength.length)];
}
varEndingPointOfString = varStaringPointOfString;
strtostoreLength =#"";
}
but from this the speed of typing is become very slow.
Try this:
NSMutableParagraphStyle *paragraphStyle = [[[NSMutableParagraphStyle alloc] init];
[paragraphStyle setFirstHeadLineHeadIndent:firstLineIndend]; //Only the first line
[paragraphStyle setHeadIndent:headIndent]; //The rest of the lines, except the first one
[yourAttributedString addAttribute:NSParagraphStyleAttributeName
value:paragraphStyle
range:paragraphRange];
For the bullet point, that's something different. You need to find where are the bullet point, and set another indent accordingly.

Why doesn't this properly color the substring in my UILabel?

I want a particular substring to be yellow. I don't want to break it up into multiple UILabels since that would make localizing the layout a nightmare. So I do this:
NSMutableAttributedString* instructions = [[[NSMutableAttributedString alloc] initWithString:self.o_instructionsLabel.text] autorelease];
NSRange range = [instructions.string rangeOfString:#"FOOBARBAZ"];
if (range.length > 0)
{
[instructions addAttribute:NSForegroundColorAttributeName value:[UIColor yellowColor] range:range];
self.o_instructionsLabel.attributedText = instructions;
}
However, the whole of the text in the label remains white. This seems to be how all the examples do it, I verified the range is correct, and when I dump the instructions object, I see the attributes inline where I guess they should be.
What am I doing wrong?
The solution was custom code in drawTextInRect: was ignoring the attributed string.

UILabel not getting what I write to it

Tricky to write the subject to this. I guess this is a basic question but I can't seem to find the answer.
The code itself shows what I wanna do and the UILabel don't show anything, the first line I add to it works fine, but not when I try to write out the array:
-(IBAction)getSongHistory:(id)sender {
[historyLabel setText:#"test write\n test write another line"];
NSArray *pastMusicArray = [pastSongs getHistory];
for(int t=2; t<[pastMusicArray count]; t++) {
NSString *tempRow = [pastMusicArray objectAtIndex:t];
//NSLog(#"%#", tempRow);
[historyLabel setText:tempRow];
[historyLabel setText:#"\n"];
}
}
The NSLog do put out the right stuff.
What is gong on here, that I am not seeing?
Seems to me that the problem is that you are setting the full text each time and the last setText: is with #"\n" which is an invisible string. Try appending instead of setting the text. Something like:
historyLabel.text = [NSString stringWithFormat:#"%#%#", historyLabel.text,#"TextToAppend"];
This will append #"TextToAppend" to the current text value in the label.
Update: Notice I'm using the text property rather than the setter.
historyLabel.text = #"Some Text";
is equivalent to
[historyLabel setText:#"Some Text"];
Using \n in a string should be fine, try to set numberOfLines property of the label to 0, which allow any number of lines in it.
This is the solution for my own problem. Hope that it can help someone else.
NSArray *pastMusicArray = [pastSongs getHistory];
musicHistory = [[NSMutableString alloc] init];
historyLabel.numberOfLines = 0;
for(int t=2; t<[pastMusicArray count]; t++) {
[musicHistory appendString:[[pastMusicArray objectAtIndex:t] capitalizedString]];
[musicHistory appendString:#"\n"];
}
historyLabel.text = musicHistory;

Resources