Improve performance of drawRect: - iphone

I am drawing cells from a grid with a NSTimer every 0.1 Seconds.
The size is about 96x64 => 6144 cells / images.
If i am drawing images instead of (e.g.) green rectangles it is 4 times slower !
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIGraphicsPushContext(context);
CGContextSetRGBFillColor(context, 0, 0, 0, 1);
CGContextFillRect(context, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height));
int cellSize = self.bounds.size.width / WIDTH;
double xOffset = 0;
for (int i = 0; i < WIDTH;i++)
{
for (int j = 0; j < HEIGHT;j++)
{
NSNumber *currentCell = [self.state.board objectAtIndex:(i*HEIGHT)+j];
if (currentCell.intValue == 1)
{
[image1 drawAtPoint:CGPointMake(xOffset + (cellSize * i),cellSize * j )];
}
else if (currentCell.intValue == 0){
[image2 drawAtPoint:CGPointMake(xOffset + (cellSize * i),cellSize * j )];
}
}
}
UIGraphicsPopContext();
}
Any idea how to makes this faster if i want to draw png or jpg in each rectangle?
The images are already scaled to an appropriate size.

a) Don't redraw the images/rects that are outside the view's bounds.
b) Don't redraw the images/rects that are outside the dirtyRect
c) Don't redraw the images/rects that haven't changes since the
previous update.
d) Use a layer to prerender the images, so you don't need to render
them at drawing time.

This scenario is exactly what Instruments is there for. Use it. Anyone here making a suggestion is guessing about what the bottleneck is.
That said, I'm going to guess at what the bottleneck is. You are drawing 6114 images using the CPU (confirm this by using the time profiler. Find your drawRect method, and check where the most time is spent. If it's drawInRect, then that's your problem)
If that's the case, how do we reduce its usage? An easy win would be to only redraw the images we need to draw. CALayers make this easy. Remove your drawRect method, add a sublayer to your view's layer for each image, and set the images as your layers' content properties. Instead of invalidating the view when an image needs to change, just switch the relevant layer's content property to the new image.
Another nice thing about CALayers is that they cache layer content on the GPU, meaning that the redraws that do happen will require less CPU time and won't block the rest of you app as much when they do happen.
If the overhead of that many layers is unacceptable (again, Instruments is your friend), check out CAReplicatorLayer. It's less flexible than having many CALayers, but allows a single image to be replicated many times with minimal overhead.

I tried to improve your code from performance perspective. However, check my comment about bottlenecks, too.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
//UIGraphicsPushContext(context); //not needed UIView does it anyway
//use [UIView backgroundColor] instead of this
//CGContextSetRGBFillColor(context, 0, 0, 0, 1);
//CGContextFillRect(context, CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height));
int cellSize = self.bounds.size.width / WIDTH;
double xOffset = 0;
CGRect cellFrame = CGRectMake(0, 0, cellSize, cellSize);
NSUinteger cellIndex = 0;
for (int i = 0; i < WIDTH; i++) {
cellFrame.origin.x = xOffset;
for (int j = 0; j < HEIGHT; j++, cellIndex++) {
cellFrame.origin.y = 0;
if (CGRectIntersectsRect(rect, cellFrame) {
NSNumber *currentCell = [self.state.board objectAtIndex:cellIndex];
if (currentCell.intValue == 1) {
[image1 drawInRect:cellFrame];
}
else if (currentCell.intValue == 0) {
[image2 drawInRect:cellFrame];
}
}
cellFrame.origin.y += cellSize;
}
cellFrame.origin.x += cellSize;
}
//UIGraphicsPopContext(context); //not needed UIView does it anyway
}

Use CGRectIntersects to check if the rect of your image is inside the dirtyRect to check if you need to draw it.

Related

Why is UIView drawRect causing first pass of UIScrollview to be choppy/jerky/slow?

EDIT: It looks like the problem code is in a procedural background that I am drawing in a UIView which I am then adding as a subview to UIScrollView. The procedural code is below. It draws box shapes, which look sort of like a skyline. Any Ideas why this is slowing down the first pass of my UIScrollView? It can be as much as a thousand pixels wide or more at times. See image...
- (void)drawRect:(CGRect)rect
{
UIBezierPath *vertLine = [[UIBezierPath alloc] init];
[vertLine moveToPoint:CGPointMake(0,self.frame.size.height)];
int detail = 10;
int ranNum = 0;
int count = self.bounds.size.width/detail;
CGFloat heightIncrement = 0.0;
CGFloat minHeight = self.frame.size.height;
CGFloat xPos = 0;
CGFloat yPos = self.frame.size.height-20;
for (int i =0; i<count; i++)
{
ranNum += (arc4random() % 9)-5;
yPos -= (arc4random() % 30);
[vertLine addLineToPoint:CGPointMake(xPos,yPos)];
xPos += (arc4random() % 20)+10;
[vertLine addLineToPoint:CGPointMake(xPos,yPos)];
yPos += (arc4random() % 30);
[vertLine addLineToPoint:CGPointMake(xPos,yPos)];
xPos += (arc4random() % 30);
[vertLine addLineToPoint:CGPointMake(xPos,yPos)];
if (yPos>self.frame.size.height-10) {
yPos = self.frame.size.height-10;
}
if (yPos<self.frame.size.height-50) {
yPos = self.frame.size.height-50;
}
}
[vertLine addLineToPoint:CGPointMake(count*20,(self.frame.size.height))];
[[UIColor colorWithRed:0.0/255.0 green:38.0/255.0 blue:51.0/255 alpha:1] setFill];
[vertLine fill];
}
I have a jerky scroll view, but ONLY on the first pass. After all the views have been viewed in the scroll view, it is very smooth.
First Pass: During the first pass it appears that when each UIImageView is coming into view (from right to left) There is a jerk right when it is entering the visible area. So if you reference the attached image, you'll see UIImageView 5 entering from right to left. When this happens there is a small pause as if the scroll view is telling the image view to load/prepare to be on stage. I have tried to profile this but I don't see any problems in my code and im not sure how I can profile the methods etc. that I have not overridden. So a sub question would be... What methods are called on a subview of UIScrollView when it is entering the visible area?
As I mention I tried to do the async and other concurrent approaches, but it seems that no matter how the images are loaded, the first pass is always jerky, then its as if the UIScrollView caches the subviews. Is it possible to do this caching/loading up front...
[scrollView cacheSubViews]; I would rather have a slower startup than it to be clunky on the first scroll.
Thanks for any ideas on this or information about how the UIScrollView works with its subviews. I have seen many questions and some solutions about jerky UIScrollViews with UIImageViews as subviews. I have tried many of them, but still have a slow scrollview.
austin
imageWithContentsOfFile is a synchronous process takes a lot of tile.Use some asynchronous way to load the images and the smoothness can be achieved

Identify a percentage of transparent pixels in an area of UIImageView

I'm trying to set up a collision type hit test for a defined of pixels within a UIImageView. I'm only wish to cycle through pixels in a defined area.
Here's what I have so far:
- (BOOL)cgHitTestForArea:(CGRect)area {
BOOL hit = FALSE;
CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
float areaFloat = ((area.size.width * 4) * area.size.height);
unsigned char *bitmapData = malloc(areaFloat);
CGContextRef context = CGBitmapContextCreate(bitmapData,
area.size.width,
area.size.height,
8,
4*area.size.width,
colorspace,
kCGImageAlphaPremultipliedLast);
CGContextTranslateCTM(context, -area.origin.x, -area.origin.y);
[self.layer renderInContext:context];
//Seek through all pixels.
float transparentPixels = 0;
for (int i = 0; i < (int)areaFloat ; i += 4) {
//Count each transparent pixel.
if (((bitmapData[i + 3] * 1.0) / 255.0) == 0) {
transparentPixels += 1;
}
}
free(bitmapData);
//Calculate the percentage of transparent pixels.
float hitTolerance = [[self.layer valueForKey:#"hitTolerance"]floatValue];
NSLog(#"Apixels: %f hitPercent: %f",transparentPixels,(transparentPixels/areaFloat));
if ((transparentPixels/(areaFloat/4)) < hitTolerance) {
hit = TRUE;
}
CGColorSpaceRelease(colorspace);
CGContextRelease(context);
return hit;
}
Is someone able to offer any reason why it isn't working?
I would suggest using ANImageBitmapRep. It allows for easy pixel-level manipulation of images without the hassle of context, linking against other libraries, or raw memory allocation. To create an ANImgaeBitmapRep with the contents of a view, you could do something like this:
BMPoint sizePt = BMPointMake((int)self.frame.size.width,
(int)self.frame.size.height);
ANImageBitmapRep * irep = [[ANImageBitmapRep alloc] initWithSize:sizePt];
CGContextRef ctx = [irep context];
[self.layer renderInContext:context];
[irep setNeedsUpdate:YES];
Then, you can crop out your desired rectangle. Note that coordinates are relative to the bottom left corner of the view:
// assuming aFrame is our frame
CGRect cFrame = CGRectMake(aFrame.origin.x,
self.frame.size.height - (aFrame.origin.y + aFrame.size.height),
aFrame.size.width, aFrame.size.height);
[irep cropFrame:];
Finally, you can find the percentage of alpha in the image using the following:
double totalAlpha;
double totalPixels;
for (int x = 0; x < [irep bitmapSize].x; x++) {
for (int y = 0; y < [irep bitmapSize].y; y++) {
totalAlpha += [irep getPixelAtPoint:BMPointMake(x, y)].alpha;
totalPixels += 1;
}
}
double alphaPct = totalAlpha / totalPixels;
You can then use the alphaPct variable as a percentage from 0 to 1. Note that, to prevent leaks, you must release the ANImageBitmapRep object using release: [irep release].
Hope that I helped. Image data is a fun and interesting field when it comes to iOS development.

Drawing ruled lines on a UITextView for IPhone

I would like to create a view like the notes app on iPhone and therefor need the view to have ruled lines as per the notes app, I have done this in windows where you need to get the font metrics and then draw the lines onto the device context, has anyone done this in the UITextView if so some help would be appriciated
Subclass UITextView. Override -drawRect:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
CGContextSetLineWidth(context, self.lineWidth);
CGFloat strokeOffset = (self.lineWidth / 2);
CGFloat rowHeight = self.font.lineHeight;
if (rowHeight > 0) {
CGRect rowRect = CGRectMake(self.contentOffset.x, - self.bounds.size.height, self.contentSize.width, rowHeight);
while (rowRect.origin.y < (self.bounds.size.height + self.contentSize.height)) {
CGContextMoveToPoint(context, rowRect.origin.x + strokeOffset, rowRect.origin.y + strokeOffset);
CGContextAddLineToPoint(context, rowRect.origin.x + rowRect.size.width + strokeOffset, rowRect.origin.y + strokeOffset);
CGContextDrawPath(context, kCGPathStroke);
rowRect.origin.y += rowHeight;
}
}
}
When you init the text view, be sure to set the contentMode to UIViewContentModeRedraw. Otherwise the lines won't scroll with the text.
self.contentMode = UIViewContentModeRedraw;
This isn't perfect. Ideally you should just draw into the rect that's passed. But I was lazy and this worked for my needs.
I think this works OK but I feel it has been hacked and I do not fully undestand the mechanism of the UITextView class;
first you must add the following to your delegate to force a redraw on scrolling
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// NSLog(#"scrollViewDidScroll The scroll offset is ---%f",scrollView.contentOffset.y);
[noteText setNeedsDisplay];
}
then implement drawRect in the subclass as so
- (void)drawRect:(CGRect)rect {
// Drawing code
// Get the graphics context
CGContextRef ctx = UIGraphicsGetCurrentContext();
[super drawRect:rect];
// Get the height of a single text line
NSString *alpha = #"ABCD";
CGSize textSize = [alpha sizeWithFont:self.font constrainedToSize:self.contentSize lineBreakMode:UILineBreakModeWordWrap];
NSUInteger height = textSize.height;
// Get the height of the view or contents of the view whichever is bigger
textSize = [self.text sizeWithFont:self.font constrainedToSize:self.contentSize lineBreakMode:UILineBreakModeWordWrap];
NSUInteger contentHeight = (rect.size.height > textSize.height) ? (NSUInteger)rect.size.height : textSize.height;
NSUInteger offset = 6 + height; // MAGIC Number 6 to offset from 0 to get first line OK ???
contentHeight += offset;
// Draw ruled lines
CGContextSetRGBStrokeColor(ctx, .8, .8, .8, 1);
for(int i=offset;i < contentHeight;i+=height) {
CGPoint lpoints[2] = { CGPointMake(0, i), CGPointMake(rect.size.width, i) };
CGContextStrokeLineSegments(ctx, lpoints, 2);
}
}
Still worry about this Magic Number 6
Bob
You can try setting the backgroundColor of you textView using an image with ruled lines
textView.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"RuledLinesPage.png"]];
Color with pattern image creates a tiled image if the area to be filled with the color is larger than the image. So you will have to make sure that the image size is correct size/tileable (I don't think 'tileable' is a real word but i hope you get what i mean). Also you will have to create the image with ruled lines to best match you textView's font.
Good Luck.
#lukya,
Your solution is bit messy as when we scroll the UITextView the text only scrolls leaving the lines (coming from the image) in its place.
A better solution would be to add subview to your text view where you have drawn the lines. You need to add an observer to the text view in order to track its change in content size as the text increase/decrease.

Is it possible to create a custom, animated MKAnnotationView?

I'm trying to simulate the user location animation in MapKit (where-by the user's position is represented by a pulsating blue dot). I've created a custom subclass of MKAnnotationView and in the drawRect method I'm attempting to cycle through a set of colors. Here's a simpler implementation of what I'm doing:
- (void)drawRect:(CGRect)rect {
float magSquared = event.magnitude * event.magnitude;
CGContextRef context = UIGraphicsGetCurrentContext();
if (idx == -1) {
r[0] = 1.0; r[1] = 0.5; r[2] = 0;
b[0] = 0; b[1] = 1.0; b[2] = 0.5;
g[0] = 0.5; g[1] = 0; g[2] = 1.0;
idx = 0;
}
// CGContextSetRGBFillColor(context, 1.0, 1.0 - magSquared * 0.015, 0.211, .6);
CGContextSetRGBFillColor(context, r[idx], g[idx], b[idx], 0.75);
CGContextFillEllipseInRect(context, rect);
idx++;
if (idx > 3) idx = 0;
}
Unfortunately this just causes the annotations to be one of the 3 different colors and doesn't cycle through them. Is there a way to force the MKAnnotations to continually redraw so that it appears to be animated?
You are free to call setNeedsDisplay on your annotation view whenever you want it to redraw. The easiest way to do this would be for the annotation view itself to set up a timer that fired every 1/60th of a second or so.
A more sophisticated approach would be to put your drawing code into a custom CALayer and apply a repeating Core Animation animation to it. See my answer to "Animating a custom property of CALayer subclass" for an approach.

iPhone: Draw rotated text?

I want to draw some text in a view, rotated 90°. I'm pretty new to iPhone development, and poking around the web reveals a number of different solutions. I've tried a few and usually end up with my text getting clipped.
What's going on here? I am drawing in a fairly small space (a table view cell), but there has to be a "right" way to do this… right?
Edit: Here are a couple of examples. I'm trying to display the text "12345" along the black bar at the left.
First attempt, from RJShearman on the Apple Discussions
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSelectFont (context, "Helvetica-Bold", 16.0, kCGEncodingMacRoman);
CGContextSetTextDrawingMode (context, kCGTextFill);
CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetTextMatrix (context, CGAffineTransformRotate(CGAffineTransformScale(CGAffineTransformIdentity, 1.f, -1.f ), M_PI/2));
CGContextShowTextAtPoint (context, 21.0, 55.0, [_cell.number cStringUsingEncoding:NSUTF8StringEncoding], [_cell.number length]);
CGContextRestoreGState(context);
(source: deeptechinc.com)
Second attempt, from zgombosi on iPhone Dev SDK. Identical results (the font was slightly smaller here, so there's less clipping).
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint point = CGPointMake(6.0, 50.0);
CGContextSaveGState(context);
CGContextTranslateCTM(context, point.x, point.y);
CGAffineTransform textTransform = CGAffineTransformMakeRotation(-1.57);
CGContextConcatCTM(context, textTransform);
CGContextTranslateCTM(context, -point.x, -point.y);
[[UIColor redColor] set];
[_cell.number drawAtPoint:point withFont:[UIFont fontWithName:#"Helvetica-Bold" size:14.0]];
CGContextRestoreGState(context);
Attempt two. There is almost identical clipping http://dev.deeptechinc.com/sidney/share/iphonerotation/attempt2.png
It turns out that the my table cell was always initialized 44px high regardless of the row height, so all of my drawing was getting clipped 44px from the top of the cell.
To draw larger cells it was necessary to set the content view's autoresizingMask with
cellContentView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
or
cellContentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
…and drawRect is called with the correct size. In a way, this makes sense, because UITableViewCell's initWithStyle:reuseIdentifier: makes no mention of the size of the cell, and only the table view actually knows how big each row is going to be, based on its own size and its delegate's response to tableView:heightForRowAtIndexPath:.
I read the Quartz 2D Programming Guide until the drawing model and functions started to make sense, and the code to draw my rotated text became simple and obvious:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextRotateCTM(context, -(M_PI/2));
[_cell.number drawAtPoint:CGPointMake(-57.0, 5.5) withFont:[UIFont fontWithName:#"Helvetica-Bold" size:16.0]];
CGContextRestoreGState(context);
Thanks for the tips, it looks like I'm all set.
Use :-
label.transform = CGAffineTransformMakeRotation(- 90.0f * M_PI / 180.0f);
where label is the object of UILabel.
Here's a tip. I presume you're doing this drawing in drawRect. Why don't you draw a frame around drawRect to see how big the rect is and if that is why you get clipping.
An alternative is to put your text in a UILabel, and then rotate that 90 degrees when you make your cells in cellForRowAtIndexPath.
You know about the UITableViewDelegate method heightForRowAtIndexPath right?
Here's a simple tutorial on various graphics level methods. Presuming you know how big your text is you should be able to size your table view row size appropriately.
Also, I'd check to make sure that the bounds after any transform actually meet your expectations. (Either use a debugger or log statement to verify this).
to what #Sidnicious said, and what i collected through out stack overflow, i want to give a usage example - appended my code to completely draw a ruler to the left screen side, with numbers rotated:
RulerView : UIView
// simple testing for iPhones (check for device descriptions to get all iPhones + iPads)
- (float)getPPI
{
switch ((int)[UIScreen mainScreen].bounds.size.height) {
case 568: // iPhone 5*
case 667: // iPhone 6
return 163.0;
break;
case 736: // iPhone 6+
return 154.0;
break;
default:
return -1.0;
break;
}
}
- (void)drawRect:(CGRect)rect
{
[[UIColor blackColor] setFill];
float ppi = [self getPPI];
if (ppi == -1.0) // unable to draw, maybe an ipad.
return;
float linesDist = ppi/25.4; // ppi/mm per inch (regular size iPad would be 132.0, iPhone6+ 154.0)
float linesWidthShort = 15.0;
float linesWidthMid = 20.0;
float linesWidthLong = 25.0;
for (float i = 0, c = 0; i <= self.bounds.size.height; i = i + linesDist, c = c +1.0)
{
bool isMid = (int)c % 5 == 0;
bool isLong = (int)c % 10 == 0;
float linesWidth = isLong ? linesWidthLong : isMid ? linesWidthMid : linesWidthShort;
UIRectFillUsingBlendMode( (CGRect){0, i, linesWidth, .5} , kCGBlendModeNormal);
/* FONT: Numbers without rotation (yes, is short)
if (isLong && i > 0 && (int)c % 10 == 0)
[[NSString stringWithFormat:#"%d", (int)(c/10)] drawAtPoint:(CGPoint){linesWidthLong +2, i -5} withAttributes:#{
NSFontAttributeName: [UIFont systemFontOfSize:9],
NSBaselineOffsetAttributeName: [NSNumber numberWithFloat:1.0]
}];
*/
// FONT: Numbers with rotation (yes, requires more effort)
if (isLong && i > 0 && (int)c % 10 == 0)
{
NSString *str = [NSString stringWithFormat:#"%d", (int)(c/10)];
NSDictionary *attrs = #{
NSFontAttributeName: [UIFont systemFontOfSize:9],
NSBaselineOffsetAttributeName: [NSNumber numberWithFloat:0.0]
};
CGSize textSize = [str sizeWithAttributes:attrs];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextRotateCTM(context, +(M_PI/2));
[str drawAtPoint:(CGPoint){i - (textSize.width/2), -(linesWidthLong + textSize.height +2)} withAttributes:attrs];
CGContextRestoreGState(context);
}
}
}
After I discovered that I needed to add the following to the top of my file I liked Matt's approach. Very simple.
#define degreesToRadian(x) (M_PI * (x) / 180.0)
mahboudz's suggestion will probably be your path of least resistance. You can rotate the UILabel 90deg with this: [label setTransform:CGAffineTransformMakeRotation(DegreesToRadians(-90.0f))]; You'll just have to calculate your cell height based upon the label width. -Matt – Matt Long Nov 10 at 0:09