1

This seems like such a basic thing to want, I can't believe I'm not able to find out how to do it. To make the description easy to understand, suppose I simply want to draw a bunch of random rectangles on the screen. These random rectangles would keep adding on top of each other repeatedly until something stopped the process. How would one do that?

The closest explanation I've seen is drawing applications, where the basic scheme is to draw into an image view, first copying the previous image into the new image and then adding the new content. Copying the original image sure seems like a waste of effort, and it sure seems like it should be possible to simply write the new content in place over whatever is there. Am I missing something obvious?

Note that drawRect replaces the entire frame. It works well for drawing a small set of objects, but it quickly becomes awkward when there's an indefinite amount of history that also needs to be displayed.

Edit: I'm attaching some sample images that are screen prints from a Mix C program that does what I'm after. Essentially, there are cellular automata that move around the screen leaving trails. The color of the trail depends upon the logic in the automaton as well as the color of the pixel where the automaton just traveled to. The automata should be able to move at rates of hundreds of pixels per second. Because of the logic used by the automata, I need to be able to not only write quickly to the image but also be able to inquire what the color of a pixel is (or mirrored data).

enter image description here

enter image description here

enter image description here

Victor Engel
  • 2,037
  • 2
  • 25
  • 46

2 Answers2

1

Typically you do this by either creating separate paths or layers for all your rectangles (if you want to keep track of them), or by drawing repeatedly into a CGBitmapContextRef, and then converting that into an image and drawing it in drawRect:. This is basically the same approach you're describing ("where the basic scheme is to draw into an image view…") except there's no need to copy the image. You just keep using the same context and making new images out of it.

The other tool you could use here is a CGLayer. The Core Graphics team discourages its use because of performance concerns, but it does make this kind of drawing much more convenient. When you look at the docs, and they say "benefit from improved performance," remember that this was written in 2006, and when I asked the Core Graphics team about it, they said that the faster answer today is CGBitmapContext. But you can't beat CGLayer for convenience on this kind of problem.


This should be fine by maintaining a CGBitmapContext that you continually write into (and that allows you to also read from it). When it changes, call setNeedsDisplayInRect:. In drawRect:, create the image, and draw it using CGContextDrawImage, passing the rect you were passed. (You may be passed the entire rect.)

It may be a little more flexible to do this on the CALayer instead of the UIView, but I doubt you'll see a great difference in performance. The view passes drawing to its layer.

The number of times a second this updates isn't really that important. drawRect: will not be called more often than the frame rate (max of 60 fps), no matter how often you call setNeedsDisplayInRect:. So you won't be creating images hundreds or thousands of times a second; just at the time that you need to draw something.

Are you seeing particular performance problems, or are you just concerned that you may in the future encounter performance problems? Do you have any sample code that shows the issue? I'm not saying it can't be slow (it might be if you're trying to do this full screen with retina). But you want to start with the simple solution and then optimize. Apple does a lot of graphics optimizations behind the scenes. Don't try to second guess them too much. They generate and draw images really well.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Hmmm. It sounds like drawing repeatedly into a `CGBitmapContextRef` might be what I need to do, but it still seems like drawing the whole view is a waste of effort. I suspect there will be a performance issue, but perhaps not. I suppose another alternative is using OpenGL ES 2.0 and `CAEAGLLayer`. I'll look into your suggestion tomorrow. – Victor Engel Dec 20 '13 at 05:20
  • You don't have to redraw the whole view. You can can use `setNeedsDisplayInRect:` and then just redraw the rectangle passed to you in `drawRect:`. But that's usually more trouble than it's worth. – Rob Napier Dec 20 '13 at 13:31
  • Have you seen this reported issue? http://stackoverflow.com/questions/10484291/setneedsdisplayinrect-causes-the-whole-view-to-be-updated – Victor Engel Dec 21 '13 at 13:03
  • I'm aware of it. My latest tests suggests that this has changed in the iOS 6 timeframe (they are at least sending us dirty rects rather than the whole rect like they did in earlier versions), but it doesn't really matter. On screens the size of the iPhone, redrawing the entire view generally is not a problem. The performance impact of preventing it often outweighs the performance gains of not drawing it. – Rob Napier Dec 21 '13 at 15:29
  • The device I am using is an iPad 3rd generation. – Victor Engel Dec 21 '13 at 16:25
  • And you're having a drawing performance problem you're trying to work around. An iPad 3rd gen is my standard dev platform. Drawing performance is a concern on it (though I've gotten it to do extremely complex drawing very quickly). I'm not quite certain what problem you're currently trying to fix. Redrawing views is very common, and trying to do partial drawing is often more expensive, by the time you consider all the compositing problems. What problem are you currently having? – Rob Napier Dec 21 '13 at 16:31
  • I just added some illustrations and text describing how they are produced. – Victor Engel Dec 21 '13 at 20:27
  • I checked this as the answer even though my testing used a slightly different method. The key point, though, was to separate updating the image from drawing to the persistent context. Drawing is extremely fast. Updating the image property turns out to be the slow part. Setting up an NSTimer to update the image, I get the following timings: 5080 20x20 squares/sec. at 1 image update/sec. (fps), 2978 at 5 fps., 833 at 10 fps. Faster than 13 fps, it speeds up, presumably because some of the image updates are skipped. I am not using drawrect but my own method to draw to the context. – Victor Engel Dec 23 '13 at 04:24
0

I've accepted another answer, but I'm including my own answer to show the alternative I used for testing.

  -(void)viewWillLayoutSubviews {
      self.pixelCount = 0;
      self.seconds = 0;
      CGRect frame = self.testImageView.bounds;
      UIGraphicsBeginImageContextWithOptions(frame.size, YES, 0.0);
      CGContextRef context = UIGraphicsGetCurrentContext();
      CGContextSetLineWidth(context, 1.0);
      CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
      CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
      CGContextMoveToPoint(context, 0.0, 0.0);
      CGContextAddRect(context, frame);
      CGContextFillRect(context, frame);
      CGContextStrokePath(context);
      CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
      CGContextStrokePath(context);

      UIImage *blank = UIGraphicsGetImageFromCurrentImageContext();
      self.context = context;
      self.testImageView.image = blank;

      // This timer has no delay, so it will draw squares as fast as possible.
      [NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(drawRandomRectangle) userInfo:nil repeats:NO];

      // This timer is used to control the frequency at which the imageViews image is updated.
      [NSTimer scheduledTimerWithTimeInterval:1/20.f target:self selector:@selector(updateImage) userInfo:nil repeats:YES];

      // This timer just outputs the counter once per second so I can record statistics.
      [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(displayCounter) userInfo:nil repeats:YES];
  }
  -(void)updateImage
  {
      self.testImageView.image = UIGraphicsGetImageFromCurrentImageContext();
  }
  -(void)displayCounter
  {
      self.seconds++;
      NSLog(@"%d",self.pixelCount/self.seconds);
  }
  -(void)drawRandomRectangle
  {
      int x1 = arc4random_uniform(self.view.bounds.size.width);
      int y1 = arc4random_uniform(self.view.bounds.size.height);
      int xdif = 20;
      int ydif = 20;
      x1 -= xdif/2;
      y1 -= ydif/2;
      CGFloat red = (arc4random() % 256) / 255.0f;
      CGFloat green = (arc4random() % 256) / 255.0f;
      CGFloat blue = (arc4random() % 256) / 255.0f;
      UIColor *randomColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0f];
      CGRect frame = CGRectMake(x1*1.0f, y1*1.0f, xdif*1.0f, ydif*1.0f);
      CGContextSetStrokeColorWithColor(self.context, [UIColor blackColor].CGColor);
      CGContextSetFillColorWithColor(self.context, randomColor.CGColor);
      CGContextSetLineWidth(self.context, 1.0);
      CGContextAddRect(self.context, frame);
      CGContextStrokePath(self.context);
      CGContextFillRect(self.context, frame);
      CGContextStrokePath(self.context);
      if (self.pixelCount < 100000) {
         [NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(drawRandomRectangle) userInfo:nil repeats:NO];
      }
      self.pixelCount ++;
  }

The graph shows image updates per second on the x-axis and number of 20x20 squares drawn to the context per second in the y-axis.

enter image description here

Victor Engel
  • 2,037
  • 2
  • 25
  • 46