Background:
I'm making an app that is grid-based using ARC. Basically there is a 4x4-8x8 grid in the center of the screen (that takes up most of the screen). This grid is constructed using a single UIView that is tinted some color and lines drawn with drawRect: (I'll be posting all of the relevant code below for reference).
Each of the cells is contained inside an NSMutableArray for each row that is contained inside another NSMutableArray of the rows:
Array (Rows)
Array (Cols)
Cell Contents
In each of these cells, I either have an actor object or a placeholder object. The placeholder object is essentially just a blank NSObject while the actor object has 8 primitive properties and 1 object property.
For instance, one of the actors is a source, which essentially recursively draws a plain UIView from the source across the grid until it hits another actor or a wall of the grid.
The blue and red lines show different UIViews as they are currently running. With a grid this small, memory doesn't seem to be an issue often; however, when the full game runs with an 8x8 grid, there can feasibly be 50+ drawn UIViews on the screen in addition to the UIImageViews that function as the sources, movables, etc. as well as the other UILabels and buttons that are not included in the grid. There can easily be over 100 UIViews on the screen at once, which, even on the latest devices with the best hardware, causes some pretty bad lag.
I have a feeling that this has to do with the fact that I am rendering 100+ views to the screen at once.
Question:
Can I incorporate all of these dynamically drawn lines into one view, or is there a better solution entirely?
drawRect:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef color = [[self backgroundColor] CGColor];
int numComponents = CGColorGetNumberOfComponents(color);
CGFloat red = 0, green = 0, blue = 0;
if (numComponents == 4)
{
const CGFloat *components = CGColorGetComponents(color);
red = components[0];
green = components[1];
blue = components[2];
}
CGContextSetRGBFillColor(context, red, green, blue, 0.5);
float top;
float cell = [self cellWidth];
float grid = [self gridWidth];
//Draw
for(int i = 0; i < [self size]+1; i++)
{
top = i*(cell+lineWidth);
CGContextFillRect(context, CGRectMake(0, top, grid, lineWidth));
CGContextFillRect(context, CGRectMake(top, 0, lineWidth, grid));
}
}
addSources:
- (void)addSources:(NSArray*)sources
{
for(int i = 0; i < [sources count]; i++)
{
NSArray* src = [sources objectAtIndex:i];
int row = [[src objectAtIndex:1] intValue];
int column = [[src objectAtIndex:0] intValue];
int direction = [[src objectAtIndex:2] intValue];
int color = [UIColor colorKeyForString:[src objectAtIndex:3]];
float width = [self cellWidth]*scaleActors;
float x = lineWidth + (([self cellWidth]+lineWidth) * (column-1)) + (([self cellWidth]-width)/2.0);
float y = lineWidth + (([self cellWidth]+lineWidth) * (row-1)) + (([self cellWidth]-width)/2.0);
ActorView* actor = [[ActorView alloc] initWithFrame:CGRectMake(x, y, width, width)];
[actor setType:4];
[actor setDirection:direction];
[actor setColorKey:color];
[actor setIsGlowing:YES];
[actor setPicture];
if([self isCreatingLevel])
[actor setCanRotate:YES];
[self addSubview:actor];
[[[self rows] objectAtIndex:(row-1)] replaceObjectAtIndex:(column-1) withObject:actor];
}
}
Edit: Time Profiler Results
By this point, I have roughly 48 drawn views on the screen, (about 70 views total).
I'd suggest WWDC 2012 video iOS App Performance: Responsiveness as a good primer in using Instruments to track down these sorts of issues. Lots of good techniques and tips in that video.
But I agree that this number of views doesn't seem outlandish (though I might be tempted to render this all in CoreGraphics). I'm not using your same model, but here is a pure Core Graphics rendering of that graphic with a single UIView subclass:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
// configure the gridlines
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
CGContextSetLineWidth(context, 8.0);
CGContextSetLineCap(context, kCGLineCapSquare);
// add the horizontal gridlines
for (NSInteger row = 0; row <= self.rows; row++)
{
CGPoint from = [self coordinateForX:0 Y:row];
CGPoint to = [self coordinateForX:_cols Y:row];
CGContextMoveToPoint(context, from.x, from.y);
CGContextAddLineToPoint(context, to.x, to.y);
}
// add the vertical gridlines
for (NSInteger col = 0; col <= self.cols; col++)
{
CGPoint from = [self coordinateForX:col Y:0 ];
CGPoint to = [self coordinateForX:col Y:_rows];
CGContextMoveToPoint(context, from.x, from.y);
CGContextAddLineToPoint(context, to.x, to.y);
}
// stroke the gridlines
CGContextStrokePath(context);
// now configure the red/blue line segments
CGContextSetLineWidth(context, self.bounds.size.width / _cols / 2.0);
CGContextSetLineCap(context, kCGLineCapRound);
// iterate through our array of points
CGPoint lastPoint = [self.points[0] CGPointValue];
for (NSInteger i = 1; i < [self.points count]; i++)
{
// set the color
if (i % 2)
CGContextSetStrokeColorWithColor(context, [[UIColor redColor] CGColor]);
else
CGContextSetStrokeColorWithColor(context, [[UIColor blueColor] CGColor]);
CGPoint nextPoint = [self.points[i] CGPointValue];
// create path
CGPoint from = [self coordinateForCenterX:lastPoint.x Y:lastPoint.y];
CGPoint to = [self coordinateForCenterX:nextPoint.x Y:nextPoint.y];
CGContextMoveToPoint(context, from.x, from.y);
CGContextAddLineToPoint(context, to.x, to.y);
// stroke it
CGContextStrokePath(context);
// save the last point
lastPoint = nextPoint;
}
}
Now, maybe you're doing something else that requires more sophisticated treatment, in which case that WWDC video (or, perhaps iOS App Performance: Graphics and Animations) should point you in the right direction.
Related
Our app contains several MKPolyline boundaries that all create a closed in polygon. These are primarily to display as an MKOverlay on a MKMapView but I'm looking for a solution to display these polygons as small thumbnails to be visible not on the MKMapView but instead as a standard UIImage or UIImageView.
Just to be clear, I'm wanting these small thumbnails would just be displayed as small shapes that have a stroke color and a fill color but without any map background.
Could anyone help me with this?
Here you go.
+ (UIImage *)imageNamed:(NSString *)name withColor:(UIColor *)color{
// load the image
UIImage *img = [UIImage imageNamed:name];
// begin a new image context, to draw our colored image onto
UIGraphicsBeginImageContext(img.size);
// get a reference to that context we created
CGContextRef context = UIGraphicsGetCurrentContext();
// set the fill color
[color setFill];
// translate/flip the graphics context (for transforming from CG* coords to UI* coords
CGContextTranslateCTM(context, 0, img.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// set the blend mode to color burn, and the original image
CGContextSetBlendMode(context, kCGBlendModeColorBurn);
CGRect rect = CGRectMake(0, 0, img.size.width, img.size.height);
CGContextDrawImage(context, rect, img.CGImage);
// set a mask that matches the shape of the image, then draw (color burn) a colored rectangle
CGContextClipToMask(context, rect, img.CGImage);
CGContextAddRect(context, rect);
CGContextDrawPath(context,kCGPathFill);
// generate a new UIImage from the graphics context we drew onto
UIImage *coloredImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//return the color-burned image
return coloredImg;
}
Please check this original post for detail description.
I had to do exactly the same in my own app. Here is my solution : I generate a UIView which represents path's shape. In your case the path is a MKPolyline.
Here is my code :
+ (UIView *)createShapeForGPX:(GPX *)gpx
withFrameSize:(CGSize)frameSize
lineColor:(UIColor *)lineColor {
// Array of coordinates (Adapt this code with your coordinates)
// Note : in my case I have a double loops because points are in paths
// and I can have many paths for one route. So I concact all points
// into one array to simplify the code for your case. If you also have
// many paths, you have to change a little bit next code.
NSMutableArray<NSValue *> *dataPoints = [NSMutableArray new];
for (NSArray *path in gpx.paths) {
for (NSDictionary *point in path) {
double latitude = [point[#"latitude"] doubleValue];
double longitude = [point[#"longitude"] doubleValue];
[dataPoints addObject:[NSValue valueWithCGPoint:CGPointMake(longitude, latitude)]];
}
}
// Graph bounds (You need to calculate topRightCoordinate and bottomleftCoordinate. You can do it in previous for loop)
double lngBorder = gpx.topRightCoordinate.longitude - gpx.bottomLeftCoordinate.longitude;
double latBorder = gpx.topRightCoordinate.latitude - gpx.bottomLeftCoordinate.latitude;
double middleLng = gpx.bottomLeftCoordinate.longitude + (lngBorder / 2.f);
double middleLat = gpx.bottomLeftCoordinate.latitude + (latBorder / 2.f);
double boundLength = MAX(lngBorder, latBorder);
// *** Drawing ***
CGFloat margin = 4.f;
UIView *graph = [UIView new];
graph.frame = CGRectMake(0, 0, frameSize.width - margin, frameSize.height - margin);
CAShapeLayer *line = [CAShapeLayer layer];
UIBezierPath *linePath = [UIBezierPath bezierPath];
float xAxisMin = middleLng - (boundLength / 2.f);
float xAxisMax = middleLng + (boundLength / 2.f);
float yAxisMin = middleLat - (boundLength / 2.f);
float yAxisMax = middleLat + (boundLength / 2.f);
int i = 0;
while (i < dataPoints.count) {
CGPoint point = [dataPoints[i] CGPointValue];
float xRatio = 1.0-((xAxisMax-point.x)/(xAxisMax-xAxisMin));
float yRatio = 1.0-((yAxisMax-point.y)/(yAxisMax-yAxisMin));
float x = xRatio*(frameSize.width - margin / 2);
float y = (1.0-yRatio)*(frameSize.height - margin);
if (i == 0) {
[linePath moveToPoint:CGPointMake(x, y)];
} else {
[linePath addLineToPoint:CGPointMake(x, y)];
}
i++;
}
// Line
line.lineWidth = 0.8;
line.path = linePath.CGPath;
line.fillColor = [[UIColor clearColor] CGColor];
line.strokeColor = [lineColor CGColor];
[graph.layer addSublayer:line];
graph.backgroundColor = [UIColor clearColor];
// Final view (add margins)
UIView *finalView = [UIView new];
finalView.backgroundColor = [UIColor clearColor];
finalView.frame = CGRectMake(0, 0, frameSize.width, frameSize.height);
graph.center = CGPointMake(CGRectGetMidX(finalView.bounds), CGRectGetMidY(finalView.bounds));
[finalView addSubview:graph];
return finalView;
}
In my case GPX class contains few values :
- NSArray<NSArray<NSDictionary *> *> *paths; : contains all points of all paths. In your case I think it is your MKPolyline.
- topRightCoordinate and bottomLeftCoordinate : Two CLLocationCoordinate2D that represent top right and bottom left virtual coordinates of my path (you have to calcul them also).
You call this method like that :
UIView *shape = [YOURCLASS createShapeForGPX:gpx withFrameSize:CGSizeMake(32, 32) lineColor:[UIColor blackColor]];
This solution is based on this question how to draw a line graph in ios? Any control which will help me show graph data in ios which gives a solution to draw a graph from points.
Maybe all this code is not usefull for you (like margins) but it should help you to find your own solution.
Here is how it displays in my app (in a UITableView) :
I have a UISlider that I use to play a video, and I would like to put red "break indication" lines in my UISlider to visually show the user when the break is coming up, as visualized below. My slider has a set .duration property, and I have an array full of timestamps that contain the times that the video will need to pause for a break. I'm still getting the hang of iOS, so I don't know how to go about drawing the lines over UISlider's. I would like it to appear similar to this:
In my research I've read in the Apple Docs that UISlider luckily provides a method to the sliders coordinates based upon a float value. This is perfect because now I can determine where in the slider I can draw the yellow line based upon the timestamps in the array, right?
So, I created a for loop to call the method and am (attempting to) draw a line.
The drawing the line part is what I'm having issues with. I've been reading through Apple's docs and other questions on this site, but I cannot seem to figure out the drawing logic. It draws just fine, the issue is that it's coordinates are all wrong. It draws and overlaps the majority of its lines in one specific location, the top left of my view. This is what I'm trying to do:
Updated Code In correlation with #Bannings answer (Many thanks to you).
- (void)breakStarted:(NSNotification *)notification {
self.lines = [NSMutableArray new];
for (int i = 0; i < myArray.count; i++) {
long long nanoSeconds = [[myArray.adMarkerTimes objectAtIndex:i] floatValue];
float minutes = nanoSeconds / 60000000000;
[self sliderThumbCenter:self.scrubberSliderView forValue:minutes];
NSLog(#"Minute Readings: %f", minutes);
}
}
- (float)sliderThumbCenter:(UISlider *)slider forValue:(float)value {
CGRect trackRect = [slider trackRectForBounds:slider.bounds];
CGRect thumbRect = [slider thumbRectForBounds:slider.bounds trackRect:trackRect value:value];
CGFloat centerThumb = CGRectGetMidX(thumbRect);
NSLog(#"Center Thumb / Line Placements on slider are: %f", centerThumb);
[self.lines addObject:#(centerThumb)]; // Added the rect values to an array which we will loop through to draw the lines over the slider
[self setNeedsDisplay];
return centerThumb;
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 2);
for (NSNumber *x in self.lines) {
CGContextMoveToPoint(context, x.floatValue, 0);
CGContextAddLineToPoint(context, x.floatValue, rect.size.height);
CGContextStrokePath(context);
}
}
I forgot to add: I'm manually converting CMTimeValue to seconds in the loop. That's what myArray.adMarkerTimes. Maybe I did that wrong...
You can override the drawRect of UISlider.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 2);
CGContextMoveToPoint(context, 20, 0);
CGContextAddLineToPoint(context, 20, rect.size.height);
CGContextStrokePath(context);
}
or insert UIView to UISlider.
- (UIView *)lineViewForRect:(CGRect)rect {
UIView *lineView = [[UIView alloc] initWithFrame:rect];
lineView.backgroundColor = [UIColor redColor];
return lineView;
}
- (void)viewDidLoad {
[super viewDidLoad];
[slider addSubview:[self lineViewForRect:CGRectMake(20, 0, 2, slider.bounds.size.height)]];
}
You also can set lineView.layer.zPosition = 1:
before:
after:
EDIT:
You can store lines in an array, and draw each in context.
It seems like this:
// YourSlider.m
- (void)addLineToX:(CGFloat)x {
[self.lines addObject:#(x)];
// This will cause drawRect to be called
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 2);
for (NSNumber *x in self.lines) {
CGContextMoveToPoint(context, x.floatValue, 0);
CGContextAddLineToPoint(context, x.floatValue, rect.size.height);
}
CGContextStrokePath(context);
}
I have drawn text labels in front of a circular arc of dots using iOS Core Graphics. I am trying to understand why CGContextSetStrokeColorWithColor does not set text colour to black in the two examples below.
Text labels are black if the dots are outlined. But when images are filled the text changes to the dot colour.
EDIT: *~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The suggestion of Ben Zotto helped to resolved this issue. In the original code below the solution was to replace
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
with
CGContextSetFillColorWithColor(context, [[UIColor blackColor] CGColor]);
I also removed
CGContextSetShadowWithColor(context, CGSizeMake(-3 , 2), 4.0, [UIColor clearColor].CGColor);
resulting in a much cleaner label.
Thanks Ben.
*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here is the original code
- (void)rosette {
context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 0.5);
uberX = 160;
uberY = 240;
uberRadius = 52;
sectors = 16;
uberAngle = (2.0 * PI) / sectors;
dotAngle = PI * -0.5; // start drawing 0.5 PI radians before 3 o'clock
endAngle = PI * 1.5; // stop drawing 1.5 PI radians after 3 o'clock
NSLog(#"%f %f %f %f %f", uberX, uberY, uberRadius, uberAngle, dotAngle);
dotRadius = 20;
dotsFilled = FALSE;
alternateDots = TRUE; // alternately filled and outlined
textOffset = 4; // offset added to centre text
for (dotCount = 1; dotCount <= sectors; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self newPoint]; // find point coordinates for next dot
dotPosition = CGPointMake(x,y); // create dot centre
NSLog(#"Circle%i: %#", dotCount, NSStringFromCGPoint(dotPosition));
newCircle.circleCentre = dotPosition; // place each dot on the frame
[totalCircles addObject:newCircle];
[self setNeedsDisplay];
}
CGContextSetShadowWithColor(context, CGSizeMake(-3 , 2), 4.0, [UIColor clearColor].CGColor);
dotCount = 1;
for (iOSCircle *circle in totalCircles) {
CGContextEOFillPath (context);
CGContextAddArc(context, circle.circleCentre.x, circle.circleCentre.y, circle.circleRadius, 0.0, M_PI * 2.0, YES);
// draw the circles
int paintThisDot = dotsFilled * alternateDots * !(dotCount % 2); // paint if dotCount is even
NSLog(#"Dot %i Filled %i ", dotCount, dotsFilled);
switch (paintThisDot) {
case 1:
CGContextSetFillColorWithColor(context, [[UIColor cyanColor] CGColor]);
CGContextDrawPath(context, kCGPathFillStroke);
break;
default: // draw dot outline
CGContextStrokePath(context);
break;
}
CGContextClosePath(context);
[self newPoint]; // find point coordinates for next dot
dotCount++;
}
CGContextSetShadowWithColor(context, CGSizeMake(-3, 2), 4.0, [UIColor grayColor].CGColor);
CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
CGContextBeginPath(context);
// draw labels
for (dotCount = 1; dotCount <= sectors; dotCount++)
{
// Create a new iOSCircle Object
iOSCircle *newCircle = [[iOSCircle alloc] init];
newCircle.circleRadius = dotRadius;
[self newPoint]; // find point coordinates for next dot
dotPosition = CGPointMake(x,y); // use point coordinates for label
[self autoLabel]; // prints labels
}
}
And here is the method for newPoint
- (void)newPoint {
dotAngle = dotAngle + uberAngle;
x = uberX + (uberRadius * 2 * cos(dotAngle));
y = uberY + (uberRadius * 2 * sin(dotAngle));
NSLog(#"%i %f %f %f", dotCount, dotAngle, endAngle, uberAngle);
}
And the method for autoLabel
- (void)autoLabel {
boxBoundary = CGRectMake(x-dotRadius, y-dotRadius+textOffset, dotRadius*2, dotRadius*2); // set box boundaries relative to dot centre
[[NSString stringWithFormat:#"%i",dotCount] drawInRect:boxBoundary withFont:[UIFont systemFontOfSize:24] lineBreakMode:NSLineBreakByCharWrapping alignment:NSTextAlignmentCenter];
}
Use CGContextSetFillColorWithColor (Fill) instead of CGContextSetStrokeColorWithColor (Stroke) before you draw the text.
My question is how to animate the drawing process when drawing with CGContextRef. Is it possible? Assuming it is, how?
I have two code snippets that I would like to animate. First one draws a progress bar and the second one draws a simple line chart. The drawing is done inside a subclass of UIView.
Progress bar is nice and easy. But I want it to sort of draw itself out from the left. I am pretty sure that this will require using something other than UIRectFill but I dont know how to accomplish it.
- (void)drawProgressLine
{
[bgColor set];
UIRectFill(self.bounds);
[graphColor set];
UIRectFill(CGRectMake(0, 0, self.frame.size.width / 100 * [[items objectAtIndex:0] floatValue], self.frame.size.height));
}
The line chart is a bit more complex. I would really really like it to start drawing itself from the left line by line slowly completing itself towards the right but if that is too much how can I just slowly fade it in? The code:
- (void)drawLineChart
{
[bgColor set];
UIRectFill(self.bounds);
[graphColor set];
if (items.count < 2) return;
CGRect bounds = CGRectMake(0, 50, self.bounds.size.width, self.bounds.size.height - 100);
float max = -1;
for (GraphItem *item in items)
if (item.value > max)
max = item.value;
float xStep = (self.frame.size.width) / (items.count - 1);
for (int i = 0; i < items.count; i++)
{
if (i == items.count - 1) break;
float itemHeight = bounds.origin.y + bounds.size.height - ((GraphItem*)[items objectAtIndex:i]).value / max * bounds.size.height;
float nextItemHeight = bounds.origin.y + bounds.size.height - ((GraphItem*)[items objectAtIndex:i + 1]).value / max * bounds.size.height;
CGPoint start = CGPointMake(xStep * i, itemHeight);
CGPoint stop = CGPointMake(xStep * (i + 1), nextItemHeight);
[self drawLineFromPoint:start toPoint:stop lineWidth:1 color:graphColor shadow:YES];
}
}
Pretty simple I guess. If important the drawLineFromPoint..... is implemented like:
- (void)drawLineFromPoint:(CGPoint)startPoint toPoint:(CGPoint)endPoint lineWidth:(CGFloat)width color:(UIColor *)color shadow:(BOOL)shadow
{
if (shadow)
{
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat components[4] = {0.0, 0.0, 0.0, 1.0};
CGColorRef shadowColor = CGColorCreate(colorSpace, components);
CGContextSetShadowWithColor(UIGraphicsGetCurrentContext(), CGSizeMake(1,1), 2.0, shadowColor);
}
CGContextBeginPath(context);
CGContextSetLineWidth(context, width);
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
CGContextAddLineToPoint(context, endPoint.x, endPoint.y);
CGContextClosePath(context);
[color setStroke];
CGContextStrokePath(context);
CGContextSetShadowWithColor(context, CGSizeZero, 0, NULL);
}
I hope I made myself clear cause its 1 am in my country and this post is the last thing that stands between me and my bed. Cheers, Jan.
It sounds like you don't understand the UIKit view drawing cycle. Do you understand that each time you want to change the appearance of your custom-drawn view, you need to send it setNeedsDisplay? And then you need to redraw it entirely in your drawRect: method? Your drawing doesn't appear on screen until drawRect: returns, and after that you cannot draw more in that view until it receives another drawRect: message. If you want the contents of the view to be animated, you will need to send setNeedsDisplay to the view periodically (say, every 1/30th or 1/60th of a second, using either an NSTimer or a CADisplayLink).
It seems like you got the progress bar handled, so here is what I suggest for the graph drawing. Just create and debug your code once to draw the entire graph. Then, use a clip rect that you animate the width of, so that the clip rect starts out skinny and then extends in width until the whole graph becomes visible (from left to right). That will give the user the idea that whatever lines you have are "drawing" from the left to the right, but the actual code is very simple as the animation steps just modify the clip rect to make it wider for each "step". See this question for more info on the CoreGraphics calls: How to set up a clipping rectangle or area
I'm relatively new to graphics. This block of code simply draws graph nodes and edges in a UIView. This code works fine for a small number of nodes, but takes a 10+ seconds to load when the number of nodes and edges exceeds 200 in a UIView that is equaly to the frame of the iPad. Am I drawing inefficiently? My goal is to create an 'infinite' scrollable/zoomable graph view where nodes and edges are loaded only when they become visible. I believe that UIView dimensions are limited, so inserting an extremely large UIView into a UIScrollView will not work.
Currently, I am creating a UIView that is slightly larger than the dimensions of the iPad and placing that within a UIScrollView. All nodes and edges are being rendered, even if they are not visible. This is most likely inefficient.
How can I minimize time drawing? How can I create an infinite scrollable/zoomable view?
Thanks
A sample of the drawing:
UIView Code:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
//Set background
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextFillRect(context, self.bounds);
//Translate the coordinate system relative to the minimum and maximum points in the graph
CGContextTranslateCTM(context, -graph.minX + MAP_BORDER_WIDTH, -graph.minY + MAP_BORDER_HEIGHT);
if (graph) {
//Draw all edges in graph
for (FNETEdge *edge in graph.edges) {
[self drawEdge:edge
inRect:rect];
}
//Draw all nodes in graph
for (FNETNode *node in graph.nodes) {
[self drawNode:node
inRect:rect];
}
}
CGContextRestoreGState(context);
}
- (void)drawNode:(FNETNode*)node inRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
//Get the size of the text
NSMutableAttributedString *text = node.getAttributedText;
CGSize maxTextSize = [text boundingRectWithSize:CGSizeMake(0, 0) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
//Find the width and height of the node
CGFloat width = NODE_PADDING + node.icon.size.width + NODE_SPACING + maxTextSize.width + NODE_PADDING;
CGFloat height = node.icon.size.height > maxTextSize.height ? (NODE_PADDING + node.icon.size.height + NODE_PADDING) : (NODE_PADDING + maxTextSize.height + NODE_PADDING);
//Top left corner of the node
CGPoint topLeftPoint = CGPointMake(node.center.x - width/2, node.center.y - height/2);
//Inner and outer rectangles for node
CGRect outerNodeRect = CGRectMake(topLeftPoint.x, topLeftPoint.y, width, height);
CGRect innerNodeRect = CGRectMake(topLeftPoint.x + NODE_PADDING, topLeftPoint.y + NODE_PADDING, width - (2 * NODE_PADDING), height - (2 * NODE_PADDING));
//Find where we will draw the icon and the text
CGRect iconRect = CGRectMake(innerNodeRect.origin.x, topLeftPoint.y + (height - node.icon.size.height) / 2, node.icon.size.width, node.icon.size.height);
CGRect textRect = CGRectMake(innerNodeRect.origin.x + node.icon.size.width + NODE_SPACING, innerNodeRect.origin.y, maxTextSize.width, maxTextSize.height);
//Fill background
[[node getDeviceColor] setFill];
UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:outerNodeRect cornerRadius:8];
[roundedRect fillWithBlendMode: kCGBlendModeNormal alpha:1.0f];
//Draw icon
[node.icon drawInRect:iconRect];
//Draw text
CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
[text drawWithRect:textRect options:NSStringDrawingUsesLineFragmentOrigin context:nil];
CGContextRestoreGState(context);
}
- (void)drawEdge:(FNETEdge*)edge inRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextSetStrokeColorWithColor(context, [edge getSpeedColor].CGColor);
CGContextSetLineWidth(context, EDGE_LINE_WIDTH);
//Iterate through all points and connect the dots!
for (int i = 0; i < edge.points.count -1; i++) {
CGPoint startPoint = ((NSValue*)[edge.points objectAtIndex:i]).CGPointValue;
CGPoint endPoint = ((NSValue*)[edge.points objectAtIndex:i+1]).CGPointValue;
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
CGContextAddLineToPoint(context, endPoint.x, endPoint.y);
}
CGContextStrokePath(context);
CGContextRestoreGState(context);
}