Let me implement a graphical representation that uses a bunch of tall skinny layers to reduce the amount of redrawing needed. We will move the layers to the left when we add samples, so at any time we probably have one layer hanging on the left edge of the view, and one hanging on the right edge of the view:

You can find the full working code example below my github account .
Constants
Let each layer have 32 width points:
#define kLayerWidth 32
And let's say that we will place the samples along the X axis one sample per point:
#define kPointsPerSample 1
Thus, we can output the number of samples per layer. Let us call one layer of sample values ββa tile:
#define kSamplesPerTile (kLayerWidth / kPointsPerSample)
When we draw a layer, we cannot just draw patterns strictly inside the layer. We need to draw a pattern or two in front of each edge, because the lines to these patterns intersect the edge of the layer. We will call these sample add-ons:
#define kPaddingSamples 2
The maximum iPhone screen size is 320 pixels, so we can calculate the maximum number of samples that we need to save:
#define kMaxVisibleSamples ((320 / kPointsPerSample) + 2 * kPaddingSamples)
(You must change 320 if you want to launch the iPad.)
We will need to calculate which tile contains the given pattern. And, as you will see, we will want to do this even if the sample number is negative, because it will facilitate the subsequent calculations:
static inline NSInteger tileForSampleIndex(NSInteger sampleIndex) {
Instance variables
Now, to implement GraphView we need some instance variables. We will need to save the layers that we use to draw the graph. And we want to be able to search each layer according to the graphic image:
@implementation GraphView {
In a real project, you want to save the samples in the model object and give the view a link to the model. But for this example, we just save the samples in the view:
// Samples are stored in _samples as instances of NSNumber. NSMutableArray *_samples;
Since we do not want to store an arbitrarily large number of samples, we discard old samples when _samples becomes large. But this will simplify the implementation if we can basically pretend that we never discard samples. To do this, we track the total number of samples received.
We should avoid blocking the main thread, so we will do our drawing on a separate GCD queue. We need to keep track of which tiles to draw in this queue. To avoid drawing the waiting tile more than once, we use a set (which eliminates duplicates) instead of an array:
// Each member of _tilesToRedraw is an NSNumber whose value // is a tile number to be redrawn. NSMutableSet *_tilesToRedraw;
And here is the GCD lineup on which we will draw.
// Methods prefixed with rq_ run on redrawQueue. // All other methods run on the main queue. dispatch_queue_t _redrawQueue; }
Initialization / Destruction
In order for this view to work regardless of whether you create it in code or at the tip, we need two initialization methods:
- (id)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { [self commonInit]; } return self; } - (void)awakeFromNib { [self commonInit]; }
Both methods call commonInit to perform real initialization:
- (void)commonInit { _tileLayers = [[NSMutableDictionary alloc] init]; _samples = [[NSMutableArray alloc] init]; _tilesToRedraw = [[NSMutableSet alloc] init]; _redrawQueue = dispatch_queue_create("MyView tile redraw", 0); }
ARC will not clear the GCD queue for us:
- (void)dealloc { if (_redrawQueue != NULL) { dispatch_release(_redrawQueue); } }
Adding a Sample
To add a new pattern, we select a random number and add it to _samples . We are also increasing _totalSampleCount . We discard the oldest samples if _samples become large.
- (void)addRandomSample { [_samples addObject:[NSNumber numberWithFloat:120.f * ((double)arc4random() / UINT32_MAX)]]; ++_totalSampleCount; [self discardSamplesIfNeeded];
Then we check if we started a new tile. If so, we find the layer that painted the oldest tile and reuse it to paint the newly created tile.
if (_totalSampleCount % kSamplesPerTile == 1) { [self reuseOldestTileLayerForNewestTile]; }
Now we recompile the layout of all layers, which will be on the left, so that the new sample is visible on the chart.
[self layoutTileLayers]
Finally, we add tiles to the redraw queue.
[self queueTilesForRedrawIfAffectedByLastSample]
We do not want to drop samples one at a time. That would be inefficient. Instead, we let the garbage accumulate for a while, and then throw it right away:
- (void)discardSamplesIfNeeded { if (_samples.count >= 2 * kMaxVisibleSamples) { [_samples removeObjectsInRange:NSMakeRange(0, _samples.count - kMaxVisibleSamples)]; } }
To reuse a layer for a new tile, we need to find the layer of the oldest tile:
- (void)reuseOldestTileLayerForNewestTile {
Now we can remove it from the _tileLayers dictionary under the old key and save it under the new key:
[_tileLayers removeObjectForKey:reusableTileObject]; [_tileLayers setObject:layer forKey:[NSNumber numberWithInteger:newestTile]];
By default, when we move a reused layer to a new position, Core Animation will animate it. We donβt want this, because it will be a large empty orange rectangle sliding over our schedule. We want to transfer it instantly:
// The reused layer needs to move instantly to its new position, // lest it be seen animating on top of the other layers. [CATransaction begin]; { [CATransaction setDisableActions:YES]; layer.frame = [self frameForTile:newestTile]; } [CATransaction commit]; }
When we add a sample, we always want to redraw the fragment containing the sample. We also need to redraw the previous tile if the new sample is in the fill range of the previous tile.
- (void)queueTilesForRedrawIfAffectedByLastSample { [self queueTileForRedraw:tileForSampleIndex(_totalSampleCount - 1)];
The tile queue for redrawing is just a matter of adding it to the redrawing set and sending the block to redraw it on _redrawQueue .
- (void)queueTileForRedraw:(NSInteger)tile { [_tilesToRedraw addObject:[NSNumber numberWithInteger:tile]]; dispatch_async(_redrawQueue, ^{ [self rq_redrawOneTile]; }); }
Layout
The system will send layoutSubviews to GraphView when it first appears, and at any time its size will change (for example, if the size of the device changes its size). And we get the layoutSubviews message when we are really going to appear on the screen, with our final borders. Thus, layoutSubviews are a good place to customize tile layers.
First, we need to create or delete layers as needed so that we have the right layers for our size. Then we need to lay out the layers, setting their borders accordingly. Finally, for each layer we need to put a queue on its tile for redrawing.
- (void)layoutSubviews { [self adjustTileDictionary]; [CATransaction begin]; {
Setting up a tile dictionary means setting up a layer for each visible tile and deleting layers for invisible tiles. We will just reset the dictionary from scratch every time, but we will try to reuse the already created layer. The tiles that need layers are the latest tiles and previous tiles, so we have enough layers to cover the view.
- (void)adjustTileDictionary { NSInteger newestTile = tileForSampleIndex(_totalSampleCount - 1); // Add 1 to account for layers hanging off the left and right edges. NSInteger tileLayersNeeded = 1 + ceilf(self.bounds.size.width / kLayerWidth); NSInteger oldestTile = newestTile - tileLayersNeeded + 1; NSMutableArray *spareLayers = [[_tileLayers allValues] mutableCopy]; [_tileLayers removeAllObjects]; for (NSInteger tile = oldestTile; tile <= newestTile; ++tile) { CALayer *layer = [spareLayers lastObject]; if (layer) { [spareLayers removeLastObject]; } else { layer = [self newTileLayer]; } [_tileLayers setObject:layer forKey:[NSNumber numberWithInteger:tile]]; } for (CALayer *layer in spareLayers) { [layer removeFromSuperlayer]; } }
For the first time, and at any time when the view becomes wider enough, we need to create new layers. While we are creating the presentation, we will talk about it so as not to animate its contents or position. Otherwise, they will animate them by default.
- (CALayer *)newTileLayer { CALayer *layer = [CALayer layer]; layer.backgroundColor = [UIColor greenColor].CGColor; layer.actions = [NSDictionary dictionaryWithObjectsAndKeys: [NSNull null], @"contents", [NSNull null], @"position", nil]; [self.layer addSublayer:layer]; return layer; }
Actually laying out tile layers is a matter of setting each layer:
- (void)layoutTileLayers { [_tileLayers enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { CALayer *layer = obj; layer.frame = [self frameForTile:[key integerValue]]; }]; }
Of course, the trick calculates a frame for each layer. And the parts y, width and height are quite simple:
- (CGRect)frameForTile:(NSInteger)tile { CGRect myBounds = self.bounds; CGFloat x = [self xForTile:tile myBounds:myBounds]; return CGRectMake(x, myBounds.origin.y, kLayerWidth, myBounds.size.height); }
To calculate the x coordinate of the tile frame, we calculate the x coordinate of the first tile pattern:
- (CGFloat)xForTile:(NSInteger)tile myBounds:(CGRect)myBounds { return [self xForSampleAtIndex:tile * kSamplesPerTile myBounds:myBounds]; }
Calculating the x coordinate for a sample requires a little thought. We want the new sample to be on the right edge of the view, and the second is the newest, which should be kPointsPerSample , point to the left of it, etc.:
- (CGFloat)xForSampleAtIndex:(NSInteger)index myBounds:(CGRect)myBounds { return myBounds.origin.x + myBounds.size.width - kPointsPerSample * (_totalSampleCount - index); }
redrawing
Now we can talk about how to draw tiles. We are going to make a drawing on a separate line of GCD. We cannot safely access most Cocoa Touch objects from two threads at the same time, so we need to be careful. We will use the rq_ prefix for all methods running on _redrawQueue to remind ourselves that we are not in the main thread.
To redraw one tile, we need to get the tile number, the graphic borders of the tile and the drawing points. All this comes from data structures that we could change in the main stream, so we need to access them only through the main stream. Therefore, we send back to the main queue:
- (void)rq_redrawOneTile { __block NSInteger tile; __block CGRect bounds; CGPoint pointStorage[kSamplesPerTile + kPaddingSamples * 2]; CGPoint *points = pointStorage;
It so happened that we may not have any tiles. If you look at queueTilesForRedrawIfAffectedByLastSample , you will see that it usually queues on the same plate twice. Since _tilesToRedraw is a collection (not an array), the duplicate was discarded, but rq_redrawOneTile was sent twice anyway. Therefore, we need to check that in fact we have a fragment for redrawing:
if (tile == NSNotFound) return;
Now we need to draw the tile samples:
UIImage *image = [self rq_imageWithBounds:bounds points:points pointCount:pointCount];
Finally, we need to update the tile layer to show a new image. We can only touch the layer on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{ [self setImage:image forTile:tile]; }); }
This is how we actually draw the image for the layer. Suppose you know Core Graphics well enough to follow this:
- (UIImage *)rq_imageWithBounds:(CGRect)bounds points:(CGPoint *)points pointCount:(NSUInteger)pointCount { UIGraphicsBeginImageContextWithOptions(bounds.size, YES, 0); { CGContextRef gc = UIGraphicsGetCurrentContext(); CGContextTranslateCTM(gc, -bounds.origin.x, -bounds.origin.y); [[UIColor orangeColor] setFill]; CGContextFillRect(gc, bounds); [[UIColor whiteColor] setStroke]; CGContextSetLineWidth(gc, 1.0); CGContextSetLineJoin(gc, kCGLineCapRound); CGContextBeginPath(gc); CGContextAddLines(gc, points, pointCount); CGContextStrokePath(gc); } UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image; }
But we still need to get tiles, graphic frames and points for drawing. We sent it back to the main thread:
// I return NSNotFound if I couldn't dequeue a tile. // The `pointsOut` array must have room for at least // kSamplesPerTile + 2*kPaddingSamples elements. - (NSInteger)dequeueTileToRedrawReturningBounds:(CGRect *)boundsOut points:(CGPoint *)pointsOut pointCount:(NSUInteger *)pointCountOut { NSInteger tile = [self dequeueTileToRedraw]; if (tile == NSNotFound) return NSNotFound;
Graphic borders are just the borders of the tile, just as we calculated earlier to set the layer frame:
*boundsOut = [self frameForTile:tile];
I need to start the graphic from fill patterns to the first tile sample. But, before you have enough samples to fill out the view, my tile number may be negative! Therefore, I must be sure that I am not trying to access a sample with a negative index:
NSInteger sampleIndex = MAX(0, tile * kSamplesPerTile - kPaddingSamples);
We also need to make sure that we are not trying to run past the end of the samples when we calculate the sample on which we stop the graphic:
NSInteger endSampleIndex = MIN(_totalSampleCount, tile * kSamplesPerTile + kSamplesPerTile + kPaddingSamples);
And when I really get access to the samples, I need to consider the dropped samples:
NSInteger discardedSampleCount = _totalSampleCount - _samples.count;
Now we can calculate the actual points on the graph:
CGFloat x = [self xForSampleAtIndex:sampleIndex myBounds:self.bounds]; NSUInteger count = 0; for ( ; sampleIndex < endSampleIndex; ++sampleIndex, ++count, x += kPointsPerSample) { pointsOut[count] = CGPointMake(x, [[_samples objectAtIndex:sampleIndex - discardedSampleCount] floatValue]); }
And I can return the number of points and tile:
*pointCountOut = count; return tile; }
This is how we actually get the tile out of the redraw queue. Remember that the queue may be empty:
- (NSInteger)dequeueTileToRedraw { NSNumber *number = [_tilesToRedraw anyObject]; if (number) { [_tilesToRedraw removeObject:number]; return number.integerValue; } else { return NSNotFound; } }
And finally, this is how we actually set the contents of the tile layer to a new image. Remember that we sent back to the main queue to do this:
- (void)setImage:(UIImage *)image forTile:(NSInteger)tile { CALayer *layer = [_tileLayers objectForKey:[NSNumber numberWithInteger:tile]]; if (layer) { layer.contents = (__bridge id)image.CGImage; } }
Make sex more sexy
If you do all this, everything will be fine. But you can make it a little nicer by animating the re-positioning of the layers when a new sample comes in. It is very simple. We simply modify newTileLayer to add animation for the position property:
- (CALayer *)newTileLayer { CALayer *layer = [CALayer layer]; layer.backgroundColor = [UIColor greenColor].CGColor; layer.actions = [NSDictionary dictionaryWithObjectsAndKeys: [NSNull null], @"contents", [self newTileLayerPositionAnimation], @"position", nil]; [self.layer addSublayer:layer]; return layer; }
and we create the animation as follows:
- (CAAnimation *)newTileLayerPositionAnimation { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"]; animation.duration = 0.1; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; return animation; }
You want to set the duration so that it matches the speed with which new patterns came.