Efficiently draw line numbers + cursor with UITextView - ios

I'm trying to implement something like code-editor. I base my work on CYRTextVIew. Everything works fine except for line numbers and cursor drawing.
First of all it has a bug, which doesn't erase previous cursor position, which results in drawing multiple ones, looks like so:
Here is the code related to that:
- (void)drawRect:(CGRect)rect
{
// Drag the line number gutter background. The line numbers them selves are drawn by LineNumberLayoutManager.
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect bounds = self.bounds;
CGFloat height = MAX(CGRectGetHeight(bounds), self.contentSize.height) + 200;
// Set the regular fill
CGContextSetFillColorWithColor(context, self.gutterBackgroundColor.CGColor);
CGContextFillRect(context, CGRectMake(bounds.origin.x, bounds.origin.y, self.lineNumberLayoutManager.gutterWidth, height));
// Draw line
CGContextSetFillColorWithColor(context, self.gutterLineColor.CGColor);
CGContextFillRect(context, CGRectMake(self.lineNumberLayoutManager.gutterWidth, bounds.origin.y, 0.5, height));
if (_lineCursorEnabled)
{
self.lineNumberLayoutManager.selectedRange = self.selectedRange;
NSRange glyphRange = [self.lineNumberLayoutManager.textStorage.string paragraphRangeForRange:self.selectedRange];
glyphRange = [self.lineNumberLayoutManager glyphRangeForCharacterRange:glyphRange actualCharacterRange:NULL];
self.lineNumberLayoutManager.selectedRange = glyphRange;
[self.lineNumberLayoutManager invalidateDisplayForGlyphRange:glyphRange];
}
[super drawRect:rect];
}
I changed one line to this:
[self.lineNumberLayoutManager invalidateDisplayForGlyphRange:NSMakeRange(0, self.text.length)];
And it fixed that. But never mind: both approaches are painfully slow when it comes to UITextView scrolling. It is completely not suitable for production, since working with UITextView is uncomfortable. I also would love to add "error" functionality later, so I could draw red cursor in the places where error occurs.
So the question is, what is the best and the most efficient way to achieve that type of functionality?
Why I told about the bug? I told this to underline that my requirement is to have dynamically positioned cursor, which points only to current line, and you also should note that cursor rect may be much longer that regular line width if line doesnt fit into TextView's frame's width (i.e. line 8 and 9).

Related

Efficiently draw a large scrollable area

I'm working on a simple game which uses a hexagonal grid layout. The grid is very large (a few thousand pixels in width and height). I need to be able to scroll and zoom it within a scrollView, and there are a lot of individual hexagons. I have written the drawing code in CoreGraphics. The hexagons are drawn in the drawRect: method of their view. This drawing code is called for each of the hexagons:
- (void)drawInContext:(CGContextRef)context colour:(UIColor *)colour size:(CGSize)size {
CGFloat width = size.width;
CGFloat height = size.height;
CGFloat x = self.offset.x;
CGFloat y = self.offset.y;
CGContextMoveToPoint(context, (width/2)+x, y);
CGContextAddLineToPoint(context, width+x, (height / 4)+y);
CGContextAddLineToPoint(context, width+x, (height * 3 / 4)+y);
CGContextAddLineToPoint(context, (width / 2)+x, height+y);
CGContextAddLineToPoint(context, x, (height * 3 / 4)+y);
CGContextAddLineToPoint(context, x, (height / 4)+y);
CGContextClosePath(context);
CGContextSetFillColorWithColor(context, colour.CGColor);
CGContextSetStrokeColorWithColor(context, [[UIColor whiteColor] CGColor]);
CGContextDrawPath(context, kCGPathFillStroke);
NSString *text = [NSString stringWithFormat:#"I:%ld\nR:%ld\nC:%ld", self.creationIndex, self.row, self.column];
[text drawAtPoint:CGPointMake(self.offset.x+20, self.offset.y+20) withAttributes:#{NSForegroundColorAttributeName: [UIColor blackColor], NSFontAttributeName: [UIFont systemFontOfSize:[UIFont systemFontSize]]}];
}
I call setNeedsDisplay on the view when a change is needed (like a hexagon changing colour). The problem is that this seems very inefficient. It takes approximately half a second for the map to redraw, which makes everything feel sluggish.
I have tried the following:
Calculate the visible rect of the scrollView and only draw that part of it. This causes problems when zooming to a different rect, as only the destination rect is drawn, causing black space to be displayed in the part being scrolled across.
Set a flag on the hexagons to indicate that they require an update, and only drawing the hexagons which have changed. This resulted in only the changed hexagons being visible, since drawRect: seems to fill the view in black before carrying out the drawing operation, rather than leaving the previous image there and drawing the changed hexagons over the top.
Using UIKit to build the grid of hexagons. This was simply too slow, as there were hundreds of individual views.
To summarise, my question is if there is a way of optimising CoreGraphics drawing, or if there is an alternative way of drawing which is more efficient.
There should not be any need to calc the visible rect, this is done by UIScrollView.
See Scrollview Programming Guide
Furthermore from the class documentation : The object that manages the drawing of content displayed in a scroll view should tile the content’s subviews so that no view exceeds the size of the screen. As users scroll in the scroll view, this object should add and remove subviews as necessary.

How to achieve horizontal scaling view in iOS?

I have used TSRDialScrollView library to do as per the design requirement in my code.
But couldn't solve few issues.
Referred from below link :
https://www.cocoacontrols.com/controls/trsdialscrollview
I have made few changes in my code
-> To clear background color
CGContextRef context = UIGraphicsGetCurrentContext();
[[UIColor clearColor] set];
CGContextFillRect(context, rect);
But it is displaying black color.
-> Setting label after view is inverted
- (void)drawLabelWithContext:(CGContextRef)context
atPoint:(CGPoint)point
text:(NSString *)text
fillColor:(UIColor *)fillColor
strokeColor:(UIColor *)strokeColor {
// We want the label to be centered on the specified x value
NSInteger label_y = ((point.y-100) - (boundingBox.width / 2));
}
-> Inverted the whole view by changing minor and major tick length to negative in code.
- (void)drawTicksWithContext:(CGContextRef)context atX:(long)x
{
CGPoint point = CGPointMake(x, self.frame.size.height);
CGContextSetShadowWithColor(
context,
self.shadowOffset,
self.shadowBlur,
self.shadowColor.CGColor);
if ([self isMajorTick:x]) {
[self drawMajorTickWithContext:context
atPoint:point
withColor:self.majorTickColor
width:self.majorTickWidth
length:-self.majorTickLength];
// Draw the text
//
// 1) Take the existing position and subtract off the lead spacing
// 2) Divide by the minor ticks to get the major number
// 3) Add the minimum to get the current value
//
int value = (point.x - self.leading) / self.minorTickDistance + _minimum;
NSString *text = [NSString stringWithFormat:#"%i", value];
[self drawLabelWithContext:context
atPoint:point
text:text
fillColor:self.labelFillColor
strokeColor:self.labelStrokeColor];
} else {
CGContextSaveGState(context);
[self drawMinorTickWithContext:context
atPoint:point
withColor:self.minorTickColor
width:self.minorTickWidth
length:-self.minorTickLength];
// Restore the context
CGContextRestoreGState(context);
}
}
I want scaling to be done as shown below.
1) By adjusting the offset, the view of scrollview should be in middle.(i,e when it is scrolled to minimum value the position of 0 is not at the middle of the view).
2) I want the background color to be clear color.
3) As per the design how to set red color to the middle line which is to be fixed?
4) How to display light grey color in the middle as shown in image ?
I appreciate your help, and please suggest how to achieve this,really i need to complete this task it is very much needed.
Thanks in advance.
TRSDialView.m
file :-
In - (instancetype)initWithFrame:(CGRect)frame add self.opaque = NO;
Inside - (void)drawRect:(CGRect)rect method add following code below CGContextRef context = UIGraphicsGetCurrentContext(); code of line
// Fill the background
CGContextSetFillColorWithColor(context, self.backgroundColor.CGColor);
// CGContextFillRect(context, rect);
CGContextClearRect(context, rect);
The above code will clear background color.
To adjust offset add width constraint to TRSDialView in Storyboard
The red line and grey view can be added as subview in storyboard

How do I rotate text in Quartz running on iOS so the origin does not move?

I am trying to create a PDF that will run on iOS devices (iPad only). I have created my PDF and I am able to write text and graphics into it. But I am stumped on writing rotated text such that the text will rotate but the origin will remain the same. Here is an image of the output I am trying to achieve:
I create my PDF like this:
-(void)drawPDF:(NSString*)fileName
{
self.pageSize = CGSizeMake(792, 612);
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, pageSize.width, pageSize.height), nil);
// Get the graphics context.
context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, 654);
CGContextScaleCTM(context, 1.0, -1.0);
UIFont *font = [UIFont fontWithName:#"Helvetica-Light" size:8];
[self drawText:#"here is a really long line of text so we can see" withFrame:CGRectMake(400, 100, 50, 50) withFont:font rotation:-50];
// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();
}
Then I have this function for drawing the text, that I've tried many variations of:
-(void)drawText:(NSString *)text withFrame:(CGRect)rect withFont:(UIFont *)font rotation:(float)degrees
{
float radians = [PDFMaker DegreesToRadians:degrees];
NSDictionary *attribs = #{NSFontAttributeName:[UIFont fontWithName:#"Helvetica-Light" size:10.0]};
CGContextSaveGState(context);
CGPoint pos = CGPointMake(rect.origin.x, -50 - rect.origin.y);
// Translate so we're drawing in the right coordinate space
CGContextScaleCTM(context, 1.0, -1.0);
CGSize stringSize = [text sizeWithAttributes:attribs];
CGContextTranslateCTM(context, 0-stringSize.width/2, 0-stringSize.height/2);
CGContextRotateCTM(context, radians);
CGContextTranslateCTM(context, stringSize.width/2, stringSize.height/2);
[text drawAtPoint:CGPointMake(pos.x, pos.y) withAttributes:attribs];
CGContextRestoreGState(context);
}
But no matter how much I play with the translates, the text jumps around as if being rotated around an axis at the origin. I've read in other answers to rotate the text on its center at the origin and have tried that but that doesn't seem to work (or, probably more accurately, I can't get it to work). I suspect the problem may be something related to using a PDF context but really don't know. Any help would be greatly appreciated.
After a lot of research and experimentation I figured it out. I'm not sure if it's OK to answer my own question so, if not, will an op please let me know and I'll delete it. I do think the answer and the reason behind the answer might be valuable for other community members.
Here is the working code:
-(void)drawText:(NSString *)text withFrame:(CGRect)rect withFont:(UIFont *)font
rotation:(float)degrees alignment:(int)alignment center:(BOOL)center
{
// Pivot rect
rect = CGRectMake(rect.origin.x, rect.origin.y*-1, rect.size.width, rect.size.height);
float radians = [PDFMaker DegreesToRadians:degrees];
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
paragraphStyle.alignment = alignment;
NSDictionary *attributes = #{ NSFontAttributeName: font,
NSParagraphStyleAttributeName: paragraphStyle,
NSForegroundColorAttributeName: [Util ColorFromHexString:#"FFFFFF"] };
CGContextSaveGState(context);
// Translate so we're drawing in the right coordinate space
CGContextScaleCTM(context, 1.0, -1.0);
// Save this is we want/need to offset by size
CGRect textSize = [text boundingRectWithSize:rect.size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil];
// Translate to origin, rotate, translate back
CGContextTranslateCTM(context, rect.origin.x, rect.origin.y);
CGContextRotateCTM(context, radians);
CGContextTranslateCTM(context, -rect.origin.x, -rect.origin.y);
// Center if necessary
if(center==YES)
{
CGContextTranslateCTM(context, 0, (rect.size.height/2)-(textSize.size.height/2));
}
[text drawInRect:rect withAttributes:attributes];
CGContextRestoreGState
}
What's as interesting or more interesting is the why behind it. All this has been written in other questions and answers, though usually in pieces and many of the code the code samples in other Q&A's have since been deprecated.
So the why: iOS starts with a matrix with the origin at the top-left, like Windows. But the text rendering functions of iOS are carried over from OSX (which is carried over from NEXT), and the origin is at the bottom-left. PDF is normally written from the bottom left but it's standard practice to write a transform so it runs from the top-left. But since these are text rendering functions that use Core Text we flip again. So you need to take into account and get the transforms correct or your text will print upside down and backwards or off-screen.
After that we need to rotate at the origin which, at least the way I've set things up, is now at the bottom left. If we don't rotate at the origin our text will seem to fly around: the further away from the origin the more it will fly. So we translate to the origin, rotate, then translate back. This is not unique to PDF and applies to any rotation in Quartz.
Finally one editorial comment on making PDF on an iOS device that should not be overlooked: it can be slow and consume a lot of memory. I think it's fine to do for a small number of pages when you want high-quality vector output for printing or embedding into things like Office docs; charts, graphs, maybe game instructions, printable tickets .. small things. But if you're printing something like a large report or a book it's probably best to produce PDF on a server.

Line isn't drawing at the top corner

I'm attempting to draw two lines -- I understand that 0 is suppose to start at the corner, but for me it seems off. I know it isn't the code that's wrong and I've tried doing negative numbers for the x-coordinate, but the line disappears.
Those who wanted the size of my frames:
2014-03-29 21:12:28.294 touchScreenClockTest[5178:70b] height = 568.000000
2014-03-29 21:12:28.295 touchScreenClockTest[5178:70b] width = 320.000000
What I want it to look like:
What it looks like:
// Draw the top line
CGContextRef drawTopLine = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(drawTopLine, [UIColor whiteColor].CGColor);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(drawTopLine, 1.5);
CGContextMoveToPoint(drawTopLine, 0,5); //start at this point
CGContextAddLineToPoint(drawTopLine, 300, 5); //draw to this point
// and now draw the Path!
CGContextStrokePath(drawTopLine);
// Draw the bottom line
CGContextRef drawBotLine = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(drawBotLine, [UIColor whiteColor].CGColor);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(drawBotLine, 1.5);
CGContextMoveToPoint(drawBotLine, 0, 30); //start at this point
CGContextAddLineToPoint(drawBotLine, 300, 30); //draw to this point
// and now draw the Path!
CGContextStrokePath(drawBotLine);
Also one more question. I wanted to know how to put the navigation button on my own. I have the image of the navigation, but I didn't know what I would need to call to place the image UIView? (since I don't have a storyboard)

How to create an UILabel with a red corner?

My goal is to create an UILabel with the top left corner red (a visible triangle with a side of 10-15 points). How can I do it?
I tried subclassing an UILabel and to override drawRect but I had no success. Not only I didn't get any red corner I also lost the label.text that is showed if I do not override drawRect.
-(void) drawRect: (CGRect) rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx,0,0);
CGContextMoveToPoint(ctx,10,0);
CGContextMoveToPoint(ctx,0,10);
CGContextClosePath(ctx);
CGContextSetRGBFillColor(ctx,50,0,0,1);
CGContextFillPath(ctx);
}
Thanks!
Nicola
P.s. I wanted to add an image of the label I want to create but I need a reputation of at least 10 point to add it :(
As always, it would have been sufficient to read the documentation of the function you're using. You made assumptions instead, which turned out to be wrong (no surprise).
The CGContextMoveToPoint() function begins a new subpath without drawing. If you want to draw a line, then... well... use the function that draws a line:
CGPoint points[] = {
CGPointMake(0, 0),
CGPointMake(10, 0),
CGPointMake(0, 10),
CGPointMake(0, 0)
};
CGContextAddLines(ctx, points, sizeof(points) / sizeof(points[0]));

Resources