In my segmented control, sometimes the title is wider than fits its segment. How can I make it truncate?
Let's say the title of segment 1 is Text overlaps and segment 2 is named ok.
How I want it to look:
[Text ov...| ok ]
What it looks like:
T[ext overla|ps ok ]
What I tried:
Setting the width of the segment programmatically setWidth:forSegment. Still overlapping, not truncating.
Studying the properties of UISegmentedControl
Do I have to truncate it myself, before setting the title of the segment?
You have to truncate it yourself.
There is no public API for setting the truncation. Even if you dig through the UISegmentedControl's private view hierarchy, find the labels, and set lineBreakMode to UILineBreakModeTailTruncation, it won't truncate the labels for you. (I tried.)
EDIT: I got this to work. It's not pretty, it might stop working in a future iOS release, and it might get you rejected from the App Store.
static void fixLineBreakMode(UIView *view)
{
if ([view respondsToSelector:#selector(setLineBreakMode:)]) {
[(id)view setLineBreakMode:UILineBreakModeTailTruncation];
[view setFrame:CGRectInset([view.superview bounds], 6, 0)];
} else {
for (UIView *subview in view.subviews)
fixLineBreakMode(subview);
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
fixLineBreakMode(self.segmentedControl);
}
I had the same challenge when I needed to populate a segmented control with x number of segments. Some titles were overhanging. I have tried to truncate the titles in middle if they were too long. I did something like:
NSString *s = #"This title is too long to fit";
NSMutableString *mS = [[NSMutableString alloc] init];
int len = [s length];
if (len > 10) {
for (int i = 0; i < len; i++) {
unichar ch = [s characterAtIndex:i];
if(i > 3 && i < 6){
[mS appendString:#"."];
} else {
[mS appendString:[NSString stringWithFormat: #"%C", ch]];
}
}
}
This is just to give you some idea. You can limit the number of dots in the middle, just count the number of dots appended and stop adding when e.g. it reaches 3. When you have the truncated string you can use that to set a segment title. The same logic for head, tail truncating.
Related
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."
I'm using a UITextView to display some text. In laying out the text, I enumerate the lines of text using the enumerateLineFragmentsForGlyphRange:withBlock: method.
NSInteger shrunkNumberOfLines = 3;
__block NSMutableString *shortenedText = [NSMutableString new];
__block NSInteger currentLine = 0;
__block BOOL needsTruncation = NO;
[detailsTableViewCell.descriptionTextView.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, text.length) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
if (currentLine < shrunkNumberOfLines) {
NSRange stringRange = ((glyphRange.length + glyphRange.location) <= text.length) ? glyphRange : NSMakeRange(glyphRange.location, (text.length - glyphRange.location));
NSString *appendString = [text substringWithRange:stringRange];
NSLog(#"%#", appendString);
[shortenedText appendString:appendString];
currentLine += 1;
} else {
needsTruncation = YES;
*stop = YES;
}
}];
However, I'm running into a weird bug: oftentimes, the text that gets displayed in the textview doesn't line up with the text that I see in that appendString.
For example, the text in the textfield might say something like:
President Obama offered a
blueprint for deeper American
engagement in the Middle East.
...but looking at my NSLog statements, those appendStrings are something like:
President Obama offered a blu
eprint for deeper American en
gagement in the Middle East.
I've tried a bunch of things - playing with hyphenationFactor, making sure that the textContainerInsets are correct, etc - but I can't figure this out. What's causing invalid line breaks in the enumerateLineFragmentsForGlyphRange:withBlock: method?
While I'm still not sure what caused the underlying issue above, I've at least found something that solves the symptom: https://stackoverflow.com/a/19603172/686902
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.
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!
I am creating a crossword puzzle-type app, and am using the following code to make the grid:
- (void) viewDidLoad{
[...]
//we are inside 2 loops (puzzleRow and i), 15 rows (puzzleRow), 15 cols (i)...
UITextField *inputBox = [[UITextField alloc] initWithFrame:letterFrame];
key = (puzzleRow * 100) + i;
[inputBox setTag:key];
[....]
}
Later, I want to reference the UITextField and get their value, check it against the correct answer, etc.
- (IBAction)checkSolutionButton:(id)sender {
int i = 1;
for (id obj in self.Puzzle) {
int len = [obj length];
for(int j = 0; j < len; j++){
NSRange range = NSMakeRange(j, 1);
NSString *answer = [obj substringWithRange:range];
int key = (i * 100) + j;
UIView *userGuessSquare = [self.view viewWithTag:key];
UITextField *userGuessTextField = [userGuessSquare viewWithTag:key];
NSString *guess = userGuessTextField.text;
NSLog(#"guess: %#", guess);
}
}
}
self.Puzzle is an NSArray of strings (ANSWER1..ANSWER2,ANSWER3..ANSWER4) constituting a crossword puzzle's rows, dots are black squares. The UIView are holders for the UITextField, each UIView and its UITextField has an identical key.
Here I am stuck. I get a warning Incompatible pointer types initializing UITextField with an expression type UIView I thought it was a subclass?
NSString *guess = userGuessTextField.text;
is a no go.
QUESTION:
How can I make a bunch of UITextField(s) and then access them and their values later?
(EDITED to include entire IBAction method).
Using the tags should be fine. To get the UITextField you should be doing this:
UITextField *userGuessTextField = (UITextField*) [self.view viewWithTag:key];
NSString *guess = userGuessTextField.text;
This will cast the UIView to the more specific UITextField, which is ok since you know you've used it. Make sure your tags start above 0 as well.