Equal sized cells in NSSegmentedControl - objective-c

For my NSSegmentedControl, I use it do display a bar to control a NSTableView. I have code set up to control the size programmatically:
for (NSInteger i = 1; i <= numberOfSegments; i++) {
CGSize textSize = [[NSString stringWithFormat:#"Workspace %ld", (long)i] sizeWithAttributes:#{NSFontAttributeName: [NSFont systemFontOfSize:13.0f]}];
NSInteger segmentWidth = self.workspaceControl.frame.size.width / numberOfSegments;
if (textSize.width > segmentWidth) {
[self.workspaceControl setLabel:[NSString stringWithFormat:#"%ld", (long)i] forSegment:i - 1];
} else {
[self.workspaceControl setLabel:[NSString stringWithFormat:#"Workspace %ld", (long)i] forSegment:i - 1];
}
[self.workspaceControl setWidth:segmentWidth forSegment:i - 1];
}
This works, by a small problem occurs.
At the beginning (with one segment) it looks like this:
As I change the value, the right side gets clipped slightly.
And then back to one segment:
The constraints are as follows:
Im very puzzled by the clipping (probably because a couple pixels t0o large), but I have no idea how to fix it, or is there a better way to get evenly spaced cells?

In my testing the generated width constraints of the NSSegmentedControl segments appear to round to whole numbers despite setWidth(_ width: CGFloat, forSegment segment: Int) taking a CGFloat. Thus the total width of the segments can add up to more than the width of the control itself.
To get even segments pass segmentWidth through the floor. Also, set a manual width for the control and constrain it horizontally by the centerXAnchor instead of by the leading and trailing anchors to preserve its center-alignment.
Drawbacks: the width must be hard-coded and the left and right margins may not match your other views exactly.

Related

How to get pixel coordinates when CTRunDelegate callbacks are called

I have dynamic text drawn into a custom UIImageView. Text can contain combinations of characters like :-) or ;-), which I'd like to replace with PNG images.
I apologize for bunch of codes below.
Code that creates CTRunDelegate follows:
CTRunDelegateCallbacks callbacks;
callbacks.version = kCTRunDelegateVersion1;
callbacks.dealloc = emoticonDeallocationCallback;
callbacks.getAscent = emoticonGetAscentCallback;
callbacks.getDescent = emoticonGetDescentCallback;
callbacks.getWidth = emoticonGetWidthCallback;
// Functions: emoticonDeallocationCallback, emoticonGetAscentCallback, emoticonGetDescentCallback, emoticonGetWidthCallback are properly defined callback functions
CTRunDelegateRef ctrun_delegate = CTRunDelegateCreate(&callbacks, self);
// self is what delegate will be using as void*refCon parameter
Code for creating attributed string is:
NSMutableAttributedString* attString = [[NSMutableAttributedString alloc] initWithString:self.data attributes:attrs];
// self.data is string containing text
// attrs is just setting for font type and color
I've then added CTRunDelegate to this string:
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)attString, range, kCTRunDelegateAttributeName, ctrun_delegate);
// where range is for one single emoticon location in text (eg. location=5, length = 2)
// ctrun_delegate is previously created delegate for certain type of emoticon
Callback functions are defined like:
void emoticonDeallocationCallback(void*refCon)
{
// dealloc code goes here
}
CGFloat emoticonGetAscentCallback(void * refCon)
{
return 10.0;
}
CGFloat emoticonGetDescentCallback(void * refCon)
{
return 4.0;
}
CGFloat emoticonGetWidthCallback(void * refCon)
{
return 30.0;
}
Now all this works fine - I get callback functions called, and I can see that width, ascent and descent affect how text before and after detected "emoticon char combo" is drawn.
Now I'd like to draw an image at the spot where this "hole" is made, however I can't find any documentation that can guide me how do I get pixel (or some other) coordinates in each callback.
Can anyone guide me how to read these?
Thanks in advance!
P.S.
As far as I've seen, callbacks are called when CTFramesetterCreateWithAttributedString is called. So basically there's no drawing going on yet. I couldn't find any example showing how to match emoticon location to a place in drawn text. Can it be done?
I've found a solution!
To recap: issue is to draw text using CoreText into UIImageView, and this text, aside from obvious font type and color formatting, needs to have parts of the text replaced with small images, inserted where replaced sub-text was (eg. :-) will become a smiley face).
Here's how:
1) Search provided string for all supported emoticons (eg. search for :-) substring)
NSRange found = [self.rawtext rangeOfString:emoticonString options:NSCaseInsensitiveSearch range:searchRange];
If occurrence found, store it in CFRange:
CFRange cf_found = CFRangeMake(found.location, found.length);
If you're searching for multiple different emoticons (eg. :) :-) ;-) ;) etc.), sort all found occurrences in ascending order of it's location.
2) Replace all emoticon substrings (eg. :-)) you will want to replace with an image, with an empty space. After this, you must also update found locations to match these new spaces. It's not as complicated as it sounds.
3) Use CTRunDelegateCreate for each emoticon to add callback to newly created string (the one that does not have :-) but [SPACE] instead).
4) Callback functions should obviously return correct emoticon width based on image size you will use.
5) As soon as you will execute CTFramesetterCreateWithAttributedString, these callbacks will be executed as well, giving framesetter data which will be later used in creating glyphs for drawing in given frame path.
6) Now comes the part I missed: once you create frame for framesetter using CTFramesetterCreateFrame, cycle through all found emoticons and do following:
Get num of lines from frame and get origin of the first line:
CFArrayRef lines = CTFrameGetLines(frame);
int linenum = CFArrayGetCount(lines);
CGPoint origins[linenum];
CTFrameGetLineOrigins(frame, CFRangeMake(0, linenum), origins);
Cycle through all lines, for each emoticon, looking for glyph that contains it (based on the range.location for each emoticon, and number of characters in each glyph):
(Inspiration came from here: CTRunGetImageBounds returning inaccurate results)
int eloc = emoticon.range.location; // emoticon's location in text
for( int i = 0; i<linenum; i++ )
{
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
CFArrayRef gruns = CTLineGetGlyphRuns(line);
int grunnum = CFArrayGetCount(gruns);
for( int j = 0; j<grunnum; j++ )
{
CTRunRef grun = (CTRunRef) CFArrayGetValueAtIndex(gruns, j);
int glyphnum = CTRunGetGlyphCount(grun);
if( eloc > glyphnum )
{
eloc -= glyphnum;
}
else
{
CFRange runRange = CTRunGetStringRange(grun);
CGRect runBounds;
CGFloat ascent,descent;
runBounds.size.width = CTRunGetTypographicBounds(grun, CFRangeMake(0, 0), &ascent, &descent, NULL);
runBounds.size.height = ascent + descent;
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, runRange.location, NULL);
runBounds.origin.x = origins[i].x + xOffset;
runBounds.origin.y = origins[i].y;
runBounds.origin.y -= descent;
emoticon.location = CGPointMake(runBounds.origin.x + runBounds.size.width, runBounds.origin.y);
emoticon.size = CGPointMake([emoticon EmoticonWidth] ,runBounds.size.height);
break;
}
}
}
Please do not take this code as copy-paste-and-will-work as I had to strip lots of other stuff - so this is just to explain what I did, not for you to use it as is.
7) Finally I can create context and draw both text and emoticons at correct place:
if(currentContext)
{
CGContextSaveGState(currentContext);
{
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
CTFrameDraw(frame, currentContext);
}
CGContextRestoreGState(currentContext);
if( foundEmoticons != nil )
{
for( FoundEmoticon *emoticon in foundEmoticons )
{
[emoticon DrawInContext:currentContext];
}
}
}
And function that draws emoticon (I just made it to draw it's border and pivot point):
-(void) DrawInContext:(CGContext*)currentContext
{
CGFloat R = round(10.0 * [self randomFloat] ) * 0.1;
CGFloat G = round(10.0 * [self randomFloat] ) * 0.1;
CGFloat B = round(10.0 * [self randomFloat] ) * 0.1;
CGContextSetRGBStrokeColor(currentContext,R,G,B,1.0);
CGFloat pivotSize = 8.0;
CGContextBeginPath(currentContext);
CGContextMoveToPoint(currentContext, self.location.x, self.location.y - pivotSize);
CGContextAddLineToPoint(currentContext, self.location.x, self.location.y + pivotSize);
CGContextMoveToPoint(currentContext, self.location.x - pivotSize, self.location.y);
CGContextAddLineToPoint(currentContext, self.location.x + pivotSize, self.location.y);
CGContextDrawPath(currentContext, kCGPathStroke);
CGContextBeginPath(currentContext);
CGContextMoveToPoint(currentContext, self.location.x, self.location.y);
CGContextAddLineToPoint(currentContext, self.location.x + self.size.x, self.location.y);
CGContextAddLineToPoint(currentContext, self.location.x + self.size.x, self.location.y + self.size.y);
CGContextAddLineToPoint(currentContext, self.location.x, self.location.y + self.size.y);
CGContextAddLineToPoint(currentContext, self.location.x, self.location.y);
CGContextDrawPath(currentContext, kCGPathStroke);
}
Resulting image: http://i57.tinypic.com/rigis5.png
:-)))
P.S.
Here is result image with multiple lines: http://i61.tinypic.com/2pyce83.png
P.P.S.
Here is result image with multiple lines and with PNG image for emoticon:
http://i61.tinypic.com/23ixr1y.png
Are you drawing the text in a UITextView object? If so, then you can ask it's layout manager where the emoticon is drawn, specifically the -[NSLayoutManager boundingRectForGlyphRange:inTextContainer: method (also grab the text container of the text view).
Note that it expects the glyph range, not a character range. Multiple characters can make up a single glyph, so you will need to convert between them. Again, NSLayoutManager has methods to convert between character ranges and glyph ranges.
Alternatively, if you're not drawing inside a text view, you should create your own layout manager and text container, so you can do the same.
A text container describes a region on the screen where text will be drawn, typically it's a rectangle but it can be any shape:
A layout manager figures out how to fit the text within whatever shape the text container describes.
Which brings me to the other approach you could take. You can modify the text container object, adding a blank space where no text can be rendered, and put a UIImageView inside that blank space. Use the layout manager to figure out where the blank spaces should be.
Under iOS 7 and later, you can do this by adding "exclusion paths" to the text container, which is just an array of paths (rectangles probably) where each image is. For earlier versions of iOS you need to subclass NSTextContainer and override lineFragmentRectForProposedRect:atIndex:writingDirection:remainingRect:.

Calculating the exact size of a CGRect frame for a pdf based on UITextField

I am creating a pdf report that renders frames based on text in a series of UITextFields. Currently I am using a series of methods as below. The variables are used to keep track of the position for the next frame and are based on the length of the text in the current UITextField being considered and the assumption that when rendered the length of each line is around 97 characters on the pdf doc.
-(void) drawTextObservationComment;
{
//pdfLineHeight is the height of size 12 rendered text
pdfLineHeight = 15;
CGContextRef summaryContext = UIGraphicsGetCurrentContext();
UIFont *font = [UIFont fontWithName:#"arial" size:12];
CGContextSetFillColorWithColor (summaryContext, [UIColor blackColor].CGColor);
//pdfCurrentLine is the y-axis coordinate from which to begin the new frame
CGRect textRect = CGRectMake(60, pdfCurrentLine, 650, 300);
NSString *myString = self.observationComment.text;
[myString drawInRect:textRect withFont:font lineBreakMode:NSLineBreakByWordWrapping alignment:NSTextAlignmentLeft];
//checks for the no of carriage returns within the text field
NSInteger numberOfLines = [[myString componentsSeparatedByString:#"\n"] count];
//the new pdfCurrentLine value is the previous value
// + the no of new lines based on text length
// + the no lines based on the no of carriage returns within text field
// + 40 as standard gap
pdfCurrentLine = pdfCurrentLine + (([observationComment.text length]/97)*pdfLineHeight) + ((numberOfLines - 1) * pdfLineWidth) + 40;
}
This works to a certain extent, but I realise that it is not perfectly accurate. The rendered text in the CGRect frame is often not 97 characters (although it is usually around this figure give or take 10 chars). This depends on the text entered (the letter 'i' being thinner for example and therefore there may be more chars on a line where there are a lot of 'i's).
I would like to know if there is any way of accurately calculating exactly how many lines the rendered text will actually use, therefore allowing me to accurately calculate the exact position of the next frame. Or any other advice gratefully received.
Have you looked at the Documentation for NSString UIKit Additions?
There are methods in there that return the size of rendered text such as sizeWithFont:constrainedToSize and sizeWithFont:constrainedToSize:lineBreakMode:.

Is there a way to limit the width of text in UITextView

I have UITextView where I need to control where text start and end.
This because I use bubble as background image for UITextView and I need to display the text inside the bubble image.
So far,I have been able to control where text start using:
self.contentOffset = (CGPoint){.x = -10, .y = -10};
but I have no clue how to control where text end. Currently, by default it uses all UITextView width.
Will the bubble grow in height as the text stretches? Or will the bubble be limited to 1 line? Either way, I think you should use a different approach to deal with this problem.
Use -sizeWithFont:constainedToSize:lineBreakMode: on an NSString instance to figure out what the size is of the text. If the size is greater then the value allowed by you, stop allowing input (if that's your goal) or increase hight of the bubble and allow text to continue on next line.
You would typically perform these recalculations upon text input, which means you'd implement the UITextViewDelegate method -textView:shouldChangeTextInRange:replacementText: according to the above suggestions.
-- or --
Just limiting the size of the textView and setting the lineBreakMode & numberOfLines might also be sufficient for your purposes.
define #define TEXT_LENGTH 25 // Whatever your limit is
The UITextView calls this method whenever the user types a new character or deletes an existing character.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
NSUInteger newLength = (textView.text.length - range.length) + text.length;
if(newLength <= TEXT_LENGTH)
{
return YES;
} else {
NSUInteger mySpace = TEXT_LENGTH - (textView.text.length - range.length);
textView.text = [[[textView.text substringToIndex:range.location]
stringByAppendingString:[text substringToIndex: mySpace]]
stringByAppendingString:[textView.text substringFromIndex:(range.location + range.length)]];
return NO;
}
}
Use replacementText not replacementString in method, so careful with the different between replacementString and replacementText.

UILabel truncate text not working

I set the numberOfLines to 1 in the IB, but when I set the text to a long string, it doesn't truncate. If I set the numberOfLines to 2, the truncate works fine.What should I do to truncate a long string into a single line?
simple, set the following properties:
label.adjustsFontSizeToFitWidth = NO;
label.lineBreakMode = NSLineBreakByTruncatingTail;
If you're using attributed string and having set paragraph style to the attributed string: make sure you're passing paragraphStyle.lineBreakMode = .byTruncatingTail.
If using auto layout, in my case, there was a constraint missing. The UILabel grows its width if no constraint is set on its width/trailing. Once its width is limited, for instance to its superview, the truncation occurs.
You probably have a constraint on a label in that section that is making things go haywire.
Double check your constraints or remove them for that label or other controls in that section.
The storyboard option for a label: "Line Breaks:Truncate Tail" will to the work you are looking for.
If you set the label's autoshrink to "Fixed Font Size" in IB, you will always get a truncatation when the string width beyond the label width. I guess you happened to set that to "Minimum Font Scale" or "Minimum Font Font", which will lead a resizing when the string is too long.
(Xcode 4.5, other version of Xcode and IB may be different property name)
I make two functions, that will help you to do your work.
Basic:
This solution I made for task:
"minimize font to my min size of font and then put as much info, as possible, but not bigger then maximum width"
takeFineFont... function parameters:
(UIFont*)font - font of the your label (titleLabel.font)
(NSString*)string - text in your label (titleLabel.text)
(CGSize)limitStringSize - limit size.
limitStringSize.width - width limit of your label (Upper limit)
limitStringSize.height - height limit of your label (Lower limit)(actually, size of font)
-(UIFont*)takeFineFontSize:(UIFont*)font
forText:(NSString*)string
andLimit:(CGSize)limitStringSize{
UIFont* resultFont = [UIFont fontWithName:[font fontName] size:[font pointSize]];
if(limitStringSize.width != 0 && limitStringSize.height != 0){
CGSize currentSize = [string sizeWithFont:resultFont];
while(/* change font width with upper bound */
currentSize.width > limitStringSize.width
&&
/* change font height with lower bound */
currentSize.height > limitStringSize.height){
/*change height and take new width*/
currentSize.height -= 1;
currentSize.width = [string sizeWithFont:[resultFont fontWithSize:currentSize.height]].width;
}
resultFont = [resultFont fontWithSize:currentSize.height];
}
return resultFont;
}
-(double)takeFineWidthForFont:(UIFont*)font
forString:(NSString*)string
andLimit:(double)widthLimit{
return MIN([string sizeWithFont:font].width, widthLimit);
}
Suppose, that you have big string in UILabel* titleLabel
And you define somewhere:
#define maximumLengthOfYourLabel 300
#define minimumSizeOfFont 14
what you will do now? just do this peace of code:
-(void)updateTitleLabelWithBigText:(NSString*)string{
/*change text*/
self.titleLabel.text = string;
/*take pretty small font*/
self.titleLabel.font = [self takeFineFontSize:self.titleLabel.font
forText:self.titleLabel.text
andLimit:CGSizeMake(maximumLengthOfYourLabel,minimumSizeOfFont)
];
/*if your text still big, take minimal width and trunctate it*/
self.titleLabel.width = [self takeFineWidthForFont:self.titleLabel.font
forString:self.titleLabel.text
andLimit:maximumLengthOfYourLabel];
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
}
Ensure you are not calling sizeToFit() on your label. It will override the label and constraint settings.
set numberOfLines to 0, this will let you expand uilabel content vertially without truncating its width.
Perhaps this method can help you:
[myLabel sizeToFit];
The label won't be truncated but it will adjust the label size to fit in one line.

Performance issues when drawing many MKOverlays

When I draw a path with MKOverlays (sometimes with 5000+ individual MKPolylines), there is a very long wait for all of the overlays to be drawn, and every time the map view is scrolled to a new area, the overlays for that area have to be drawn, and there is another noticeable freeze-up.
My dilemma is that I have two sets of code which both draw the path correctly. The first draws the path as one long line, and draws very quickly. The second draws each line segment as an individual line, and takes a long long time.
Now, why would I even consider the second way? Because I have to analyze each individual line to see what color the line should be. For example, if the line's title property is "red", then I make the line red, but if it is "blue", then the line is blue. With the first technique, this kind of specificity is not possible (as far as I know, but maybe someone else knows differently?) because the path is just one big line, and accessing each individual segment is impossible. With the second way it is easy, but just takes a long time.
Here are my two sets of code:
First way (fast but can't access individual segments):
CLLocationCoordinate2D coords[sizeOverlayLat];
for(int iii = 0; iii < sizeOverlayLat; iii++) {
coords[iii].latitude = [[overlayLat objectAtIndex:iii] doubleValue];
coords[iii].longitude = [[overlayLong objectAtIndex:iii] doubleValue];
}
MKPolyline* line = [MKPolyline polylineWithCoordinates:coords count:sizeOverlayLat];
[mapViewGlobal addOverlay:line];
Second way (slow but I can draw each line with a specific color):
NSMutableArray* lines = [NSMutableArray new];
for(int idx = 1; idx < sizeOverlayLat; idx++) {
CLLocationCoordinate2D coords[2];
coords[0].latitude = [[overlayLat objectAtIndex:(idx - 1)] doubleValue];
coords[0].longitude = [[overlayLong objectAtIndex:(idx - 1)] doubleValue];
coords[1].latitude = [[overlayLat objectAtIndex:idx] doubleValue];
coords[1].longitude = [[overlayLong objectAtIndex:idx] doubleValue];
MKPolyline* line = [MKPolyline polylineWithCoordinates:coords count:2];
[line setTitle:[overlayColors objectAtIndex:idx]];
[lines addObject:line];
}
[mapViewGlobal addOverlays:lines];
My question is: Can I get the performance of the first way with the control over each line that the second way provides me?
You can definitely get such performance, but you would probably need to create your own overlay view.
In that view, you can draw polylines by calling CGAddLineToPoint repeatedly, while skipping parts using CGMoveToPoint. Do this separately for each color and you're done. So if you have 2 colors (red+blue), you would loop through your polygon twice, first drawing red (skipping blue pieces) and then drawing blue (skipping red pieces).