Efficiently draw a large scrollable area - ios

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.

Related

iOS - Quartz drawing issue with parent/child views

Scenario
I have two views. One is the "parent" view which contains a "child" view that does the drawing. I refer to the child as QuartzView in the code to follow. QuartzView knows how to draw a square to it's own context.
Issue
When I tell the QuartzView on it's self to draw a square it does so as expected. When I use the parent view to tell QuartsView to draw a square on it's self it draws the square in the lower left corner of the screen at about 1/5 the expected size.
Question
I assume there's some parent/child or context issues here but I'm not sure what they are. How can I get both squares to draw in the exact same place at the exact same size?
Parent ViewController
- (void)drawASquare {
// this code draws the "goofy" square that is smaller and off in the bottom left corner
x = qv.frame.size.width / 2;
y = qv.frame.size.height / 2;
CGPoint center = CGPointMake(x, y);
[qv drawRectWithCenter:center andWidth:50 andHeight:50 andFillColor:[UIColor blueColor]];
}
Child QuartzView
- (void)drawRect:(CGRect)rect
{
self.context = UIGraphicsGetCurrentContext();
UIColor *color = [UIColor colorWithRed:0 green:1 blue:0 alpha:0.5];
// this code draws a square as expected
float w = self.frame.size.width / 2;
float h = self.frame.size.height / 2;
color = [UIColor blueColor];
CGPoint center = CGPointMake(w, h);
[self drawRectWithCenter:center andWidth:20 andHeight:20 andFillColor:color];
}
- (void)drawRectWithCenter:(CGPoint)center andWidth:(float)w andHeight:(float)h andFillColor:(UIColor *)color
{
CGContextSetFillColorWithColor(self.context, color.CGColor);
CGContextSetRGBStrokeColor(self.context, 0.0, 1.0, 0.0, 1);
CGRect rectangle = CGRectMake(center.x - w / 2, center.x - w / 2, w, h);
CGContextFillRect(self.context, rectangle);
CGContextStrokeRect(self.context, rectangle);
}
Note
The opacities are the same for both squares
I turned off "Autoresize subviews" with no noticeable difference
view.contentScaleFactor = [[UIScreen mainScreen] scale]; has not helped
Edit
I'm noticing that the x/y values of the square when drawn the parent starting from the bottom left as 0,0 whereas normally 0,0 would be the top left.
The return value from UIGraphicsGetCurrentContext() is only valid inside the drawRect method. You can not and must not use that context in any other method. So the self.context property should just be a local variable.
In the drawRectWithCenter method, you should store all of the parameters in properties, and then request a view update with [self setNeedsDisplay]. That way, the framework will call drawRect with the new information. The drawRectWithCenter method should look something like this
- (void)drawRectWithCenter:(CGPoint)center andWidth:(float)w andHeight:(float)h andFillColor:(UIColor *)color
{
self.showCenter = center;
self.showWidth = w;
self.showHeight = h;
self.showFillColor = color;
[self setNeedsDisplay];
}
And of course, the drawRect function needs to take that information, and do the appropriate drawing.
I assume there's some parent/child or context issues here but I'm not sure what they are. How can I get both squares to draw in the exact same place at the exact same size?
You normally don't need to worry about the graphics context in your -drawRect: method because Cocoa Touch will set up the context for you before calling -drawRect:. But your -drawASquare method in the view controller calls -drawRectWithCenter:... to draw outside the normal drawing process, so the context isn't set up for your view. You should really have the view do its drawing in -drawRect:. If your view controller wants to make the view redraw, it should call -setNeedsDisplay, like:
[qv setNeedsDisplay];
That will add the view to the drawing list, and the graphics system will set up the graphics context and call the view's -drawRect: for you.
I'm noticing that the x/y values of the square when drawn the parent starting from the bottom left as 0,0 whereas normally 0,0 would be the top left.
UIKit and Core Animation use an upper left origin, but Core Graphics (a.k.a. Quartz) normally uses a lower left origin. The docs say:
The default coordinate system used by Core Graphics framework is LLO-based.

Efficiently draw line numbers + cursor with UITextView

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).

Resizing a UIView but drawing done by drawRect also changing size

I am testing a UIView using a UISlider as in the example images below:
I have a custom UIView with a yellow background that draws the gray square, the drawRect method is like so:
-(void)drawRect:(CGRect)rect{
NSLog(#"Draw rect called");
UIBezierPath* squarePath = [UIBezierPath bezierPathWithRect: CGRectMake(10, 10, 100, 100)];
[UIColor.grayColor setFill];
[squarePath fill];
}
And the method for my slide changing value:
- (IBAction)changeValue:(id)sender {
CGAffineTransform transform = CGAffineTransformScale(CGAffineTransformIdentity, self.slider.value, self.slider.value);
self.tableView.transform = transform;
[self.tableView setNeedsDisplay];
}
I dont understand why the square is getting larger. I've noticed that drawRect is called every time the slider is moved. If this happens then why is the square size changing? Shouldn't it remain the same size and just the frame grow with the square in the top left corner?
My second question is, how would I change the code so just the frame grows and the drawing size stays the same? I ask this because actually I want the drawing size to change dynamically using my own code in drawRect.
Any pointers would be really appreciated! thanks!
The reason why the size of the square changes is because you've transformed it. Transformations don't just affect the frame of a view; they will affect the content. The square is getting drawn into its context at its constant size (100x100) and then the transform is stretching it before it gets rendered.
The reason why it's not expanding to the right and down is because by default the anchor point of a transform is the center of the bounds. Thus it'll scale from the center outwards. From the documentation:
The origin of the transform is the value of the center property ...
Transformations aren't intended to be used to simply scale the width and height of your frame. The frame property is for that. Simply store the view's frame in a variable, change its width and height, then set it back. In your drawRect: code you can check the dimensions of the rectangle that's given to you and make your square's width/height a percentage of that.

How to create a static, actual width MKOverlayPathRenderer subclass?

I've been working on a simple path overlay to an existing MKMapView. It takes a CGMutablePath and draws it onto the map. The goals is that the path is drawn representing an actual width. e.g. The subclass takes a width in meters and converts that width into a representative line width. Here is the one version of the code that calculates the line width:
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
float mapPoints = meterWidth * MKMapPointsPerMeterAtLatitude(self.averageLatitude);
float screenPoints = mapPoints * zoomScale; // dividing would keep the apparent width constant
self.lineWidth = ceilf(screenPoints * 2);
CGContextAddPath(context, _mutablePath);
[self applyStrokePropertiesToContext:context atZoomScale:zoomScale];
CGContextStrokePath(context);
Here we first find the number of map points that correspond to our line width and then convert that to screen points. We do the conversion based on the header comments in MKGeometry.h:
// MKZoomScale provides a conversion factor between MKMapPoints and screen points.
// When MKZoomScale = 1, 1 screen point = 1 MKMapPoint. When MKZoomScale is
// 0.5, 1 screen point = 2 MKMapPoints.
Finally, we add the path to the context, apply the stroking properties to the path and stroke it.
However this gives exceedingly flakey results. The renderer often draws random fragments of line in various places outside the expected live area or doesn't draw some tiles at all. Sometimes the CPU is pegged redrawing multiple version of the same tile (as best I can tell) over and over. The docs aren't much help in this case.
I do have a working version, but it doesn't seem like the correct solution as it completely ignores zoomScale and doesn't use -applyStrokePropertiesToContext:atZoomScale:
float mapPoints = meterWidth * MKMapPointsPerMeterAtLatitude(self.averageLatitude);
self.lineWidth = ceilf(mapPoints * 2);
CGContextAddPath(context, _mutablePath);
CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor);
CGContextSetLineWidth(context, self.lineWidth);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextStrokePath(context);
Anyone have pointers on what is wrong with this implementation?

Is there a way to add text using Paths Drawing

I have a map custom view that inherit from MKOverlayPathView. I need this custom view to display circle, line and text.
I already managed to draw circle and line using path drawing CGPathAddArc and CGPathAddLineToPoint functions.
However, I still need to add text.
I tried to add text using
[text drawAtPoint:centerPoint withFont:font];
but I got invalid context error.
any idea?
With MKOverlayPathView, I think the easiest way to add text is to override drawMapRect:zoomScale:inContext: and put the path and text drawing there (and do nothing in or don't implement createPath).
But if you're going to use drawMapRect anyway, you might want to just switch to subclassing a plain MKOverlayView instead of MKOverlayPathView.
With an MKOverlayView, override the drawMapRect:zoomScale:inContext: method and draw the circle using CGContextAddArc (or CGContextAddEllipseInRect or CGPathAddArc).
You can draw the text using drawAtPoint in this method which will have the required context.
For example:
-(void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
//calculate CG values from circle coordinate and radius...
CLLocationCoordinate2D center = circle_overlay_center_coordinate_here;
CGPoint centerPoint =
[self pointForMapPoint:MKMapPointForCoordinate(center)];
CGFloat radius = MKMapPointsPerMeterAtLatitude(center.latitude) *
circle_overlay_radius_here;
CGFloat roadWidth = MKRoadWidthAtZoomScale(zoomScale);
//draw the circle...
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
CGContextSetFillColorWithColor(context, [[UIColor blueColor] colorWithAlphaComponent:0.2].CGColor);
CGContextSetLineWidth(context, roadWidth);
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, 0, 2 * M_PI, true);
CGContextDrawPath(context, kCGPathFillStroke);
//draw the text...
NSString *text = #"Hello";
UIGraphicsPushContext(context);
[[UIColor redColor] set];
[text drawAtPoint:centerPoint
withFont:[UIFont systemFontOfSize:(5.0 * roadWidth)]];
UIGraphicsPopContext();
}
In relation to a comment in another answer...
When the center coordinate or radius (or whatever) of the associated MKOverlay changes, you can make the MKOverlayView "move" by calling setNeedsDisplayInMapRect: on it (instead of removing and adding the overlay again). (When using a MKOverlayPathView, you can call invalidatePath instead.)
When calling setNeedsDisplayInMapRect:, you can pass the boundingMapRect of the overlay for the map rect parameter.
In the LocationReminders sample app from WWDC 2010, the overlay view uses KVO to observe changes to the associated MKOverlay and makes itself move whenever it detects a change to the circle's properties but you could monitor the changes in other ways and call setNeedsDisplayInMapRect: explicitly from outside the overlay view.
(In a comment on another answer I did mention using MKOverlayPathView and that is how the LocationReminders app implements a moving circle overlay view. But I should have mentioned how you can also use MKOverlayView to draw a circle. Sorry about that.)
Pushing the context with UIGraphicsPushContext generated a problem for me. Remind that the method drawMapRect:zoomScale:inContext: is called from different threads in the same time so I had to synchronize the piece of code starting where the UIGraphicsPushContext is called down to UIGraphicsPopContext call.
Also when calculating the font size like in [UIFont systemFontOfSize:(5.0 * roadWidth)] one should take into consideration the [UIScreen mainScreen].scale, which for iPad, iPad2, iPhone3 is 1 and for iPhone4 - 5 and iPad3 is 2. Otherwise the text size will be different from iPad2 to iPad3.
So for me it ended like this: [UIFont boldSystemFontOfSize:(6.0f * [UIScreen mainScreen].scale * roadWidth)]

Resources