Cocos2d DrawPoints - ios

I'm new in OpenGLES, and tring to draw a points, but no results, here is my draw method
-(void) draw
{
[super draw];
if(!isBackgroundInited)
{
glDisable(GL_TEXTURE_2D);
glEnable(GL_POINTS);
ccDrawColor4B(100,100,100,255);
ccPointSize(2);
[self drawDots];
isBackgroundInited=YES;
glDisable(GL_POINTS);
glEnable(GL_TEXTURE_2D);
}
}
-(void) drawDots
{
CGSize activeArea=CGSizeMake(winSize.width-2*margin, winSize.height-2*margin);
float xMargin=activeArea.width/dotsCount;
float yMargin=activeArea.height/dotsCount;
float yPosition=margin;
for(int i =0;i<10;i++)
{
float xPosition=margin;
for (int j=0;j<10;j++)
{
ccDrawPoint(CGPointMake(xPosition, yPosition));
xPosition+=xMargin;
}
yPosition+=yMargin;
}
}

You're drawing the dots only on the very first frame. Remove the isBackgroundInited ivar and things should appear on your screen.
OpenGL clears the framebuffer every frame, as is customary for games, so the entire contents of the screen need to be redrawn every frame.

Related

Gaps in cocos2d infinite looping game

I am using this code to implement infinite looping, but I'v got gaps for 1-2 seconds every time the offscreen image coordinates are changed. Why do they appear? How to fix it? I am also using SpriteBuilder.
#import "MainScene.h"
static const CGFloat scrollSpeed =100.f;
#implementation MainScene{
CCPhysicsNode *_world;
CCNode *_oneb;
CCNode *_twob;
NSArray *_bb;
}
- (void)didLoadFromCCB {
_bb = #[_oneb, _twob];
}
-(void)update:(CCTime)delta{
_world.position=ccp(_world.position.x - (scrollSpeed * delta), _world.position.y ); // moving world
for (CCNode *ground in _bb) {
// get the world position of the ground
CGPoint groundWorldPosition = [_world convertToWorldSpace:ground.position];
// get the screen position of the ground
CGPoint groundScreenPosition = [self convertToNodeSpace:groundWorldPosition];
// if the left corner is one complete width off the screen, move it to the right
if (groundScreenPosition.x <= (-1 * ground.contentSize.width)) {
ground.position = ccp(ground.position.x + 2 * ground.contentSize.width, ground.position.y);
}
}
}
#end
EDIT: I changed -1 to -0.5. Works fine!
Seems like you are using small image for iPhone 3.5-inch on iPhone 4-inch simulator. What resolution of your background image?
EDIT: In my game I have an infinite loop, too. Maybe my code may help you? First background sprite should be 1137x640, second 1136x640. And you will never have gaps again! Hope it helps.
init method:
backgroundSprite = [CCSprite spriteWithFile:#"background.png"];
backgroundSprite.anchorPoint = ccp(0,0);
backgroundSprite.position = ccp(0,0);
[self addChild:backgroundSprite z:0];
backgroundSprite2 = [CCSprite spriteWithFile:#"background2.png"];
backgroundSprite2.anchorPoint = ccp(0,0);
backgroundSprite2.position = ccp([backgroundSprite boundingBox].size.width,0);
[self addChild:backgroundSprite2 z:0];
tick method:
backgroundSprite.position = ccp(backgroundSprite.position.x-1,backgroundSprite.position.y);
backgroundSprite2.position = ccp(backgroundSprite2.position.x-1,backgroundSprite2.position.y);
if (backgroundSprite.position.x<-[backgroundSprite boundingBox].size.width) {
backgroundSprite.position = ccp(backgroundSprite2.position.x+[backgroundSprite2 boundingBox].size.width,backgroundSprite.position.y);
}
if (backgroundSprite2.position.x<-[backgroundSprite2 boundingBox].size.width) {
backgroundSprite2.position = ccp(backgroundSprite.position.x+[backgroundSprite boundingBox].size.width,backgroundSprite2.position.y);
}

Cocos2d, iOS 7.1: CCLabelTTF hides ccDrawLine

I'm trying to use a CCLabelTTF in the upper part of the screen, while drawing a line in the lower part using ccDrawLine. When the app starts, the label is empty, and after a while it's updated with some text. The line is drawn constantly in the draw method like that:
- (void)draw {
ccDrawColor4B(0, 255, 0, 0);
glLineWidth(40.0f);
ccDrawLine(ccp(0, 0), ccp(200, 200));
}
Thing is, the second the label is updated with text and actually displays something, the line disappears and is not drawn again, even if the label goes empty again. I'm not using any background at the moment, so it's not hiding it. I tried playing around with zOrders (even though the label and the line are in different areas of the screen) and still the line disappears. I even tried creating a CCSprite subclass with only an init and a draw method, and using it to draw the line. Here's what I have in this class:
#implementation DrawingSprite
- (id)init {
if (self = [super init]) {
}
return self;
}
- (void)draw {
ccDrawColor4B(0, 255, 0, 0); //Color of the line RGBA
glLineWidth(40.0f); //Stroke width of the line
ccDrawLine(ccp(0, 0), ccp(200, 200));
}
#end
And here's what I add to my main layer:
_topLabel = [CCLabelTTF labelWithString:#"" fontName:#"Helvetica" fontSize:24];
_topLabel.position = ccp(winSize.width/2, winSize.height - 100);
_topLabel.color = ccc3(255,255,255);
_topLabel.zOrder = -1;
[self addChild:_topLabel];
_drawingSprite = [DrawingSprite node];
_drawingSprite.zOrder = 10;
[self addChild:_drawingSprite];
What am I missing?
I think you should add
[super draw];
att the begiining of your draw method when you override the draw method of CCSprite. Only then a subclassed CCSprite does “sprite rendering” for its overridden method.

Draw a GMSPolyline with a background color

I see that the GMSPolyline protocol already defines a color property for its stroke color, but is there a way to shade the inside of its polygon (ideally with transparency)? I’m looking for a Google Maps equivalent to MKPolygon and friends.
A Polyline is different to a Polygon's. Polylines' have no concept of a fill color. File a feature request for Polygons to be added to the SDK.
There is a way, you can get something like this:
The approach is rather simple:
Add transparent noninteracting UIView with overriden drawing code and pass it CGPoints for drawing polygons
Get your CLLocationCoordinate2D coordinates for polygons and convert them to CGPoints for drawing
Update those CGPoints every time map moves so you can redraw them in the right position and make that UIView redraw itself.
So, what you want to do is add an UIView on top of your mapview, which is transparent and non-userinteractive, which has overridden drawRect method. It is provided with a double array of CGPoints, like CGpoint **points, acccessed with points[i][j] where i is each of closed polygons and j are individual points of each polygon. The class would be, let's call it OverView:
#import "OverView.h"
#interface OverView ()
{
CGPoint **points;
int *pointsForPolygon;
int count;
}
#end
#implementation OverView
- (id)initWithFrame:(CGRect)frame andNumberOfPoints:(int)numpoints andPoints:(CGPoint **)passedPoints andPointsForPolygon:(int *)passedPointsForPolygon;{
self = [super initWithFrame:frame];
if (self) {
// You want this to be transparent and non-user-interactive
self.userInteractionEnabled = NO;
self.backgroundColor = [UIColor clearColor];
// Passed data
points = passedPoints; // all CGPoints
pointsForPolygon = passedPointsForPolygon; // number of cgpoints for each polygon
count = numpoints; // Number of polygons
}
return self;
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
for(int i=0; i<count; i++) // For each of polygons, like blue ones in picture above
{
if (pointsForPolygon[i] < 2) // Require at least 3 points
continue;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetRGBFillColor(context, 0.0, 0.0, 1.0, 1.0);
CGContextSetLineWidth(context, 2.0);
for(int j = 0; j < pointsForPolygon[i]; j++)
{
CGPoint point = points[i][j];
if(j == 0)
{
// Move to the first point
CGContextMoveToPoint(context, point.x, point.y);
}
else
{
// Line to others
CGContextAddLineToPoint(context, point.x, point.y);
}
}
CGContextClosePath(context); // And close the path
CGContextFillPath(context);
CGContextStrokePath(context);
}
}
#end
Now, in original UIViewController with mapview, you need to have access to all coordinates that make all the polygons (same array as points, but consisting of CLLocationCoordinate2D, and several others:
#interface ViewController () <GMSMapViewDelegate>
{
CGPoint **points;
int howmanypoints;
int *pointsForPolygon;
CLLocationCoordinate2D **acoordinates;
}
acoordinates is populated wherever you get your coordinates for polygons, I parse the response string from Fusion Tables, part of my parser method
- (void)parseResponse2
{
NSMutableArray *fullArray = [[self.fusionStringBeaches componentsSeparatedByString:#"\n"] mutableCopy];
howmanypoints = fullArray.count; // This is number of polygons
pointsForPolygon = (int *)calloc(howmanypoints, sizeof(int)); // Number of points for each of the polygons
points = (CGPoint **)calloc(howmanypoints, sizeof(CGPoint *));
acoordinates = (CLLocationCoordinate2D **)calloc(howmanypoints, sizeof(CLLocationCoordinate2D *));
for(int i=0; i<fullArray.count; i++)
{
// Some parsing skipped here
points[i] = (CGPoint *)calloc(koji, sizeof(CGPoint));
acoordinates[i] = (CLLocationCoordinate2D *)calloc(koji, sizeof(CLLocationCoordinate2D));
pointsForPolygon[i] = koji;
if (koji > 2)
{
// Parsing skipped
for (int j=0; j<koji; j++)
{
CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(coordinates[j].latitude, coordinates[j].longitude);
// Here, you convert coordinate and add it to points array to be passed to overview
points[i][j] = [self.mapView.projection pointForCoordinate:coordinate];
// and added that coordinate to array for future access
acoordinates[i][j] = coordinate;
}
}
}
// Finally, allocate OverView passing points array and polygon and coordinate counts
self.overView = [[OverView alloc] initWithFrame:self.view.bounds
andNumberOfPoints:howmanypoints
andPoints:points
andPointsForPolygon:pointsForPolygon];
// And add it to view
[self.view addSubview:self.overView];
}
Now, you have Polygons where you want them, but must observe - (void)mapView:(GMSMapView *)mapView didChangeCameraPosition:(GMSCameraPosition *)position delegate method as drawn polygons won't move with map. The trick is that you have your 2D array of coordinates acoordinates and you can user helper function (CGPoint *)[self.mapview.projection pointForCoordinate:(CLLocationCoordinate2D)coordinate] to recalculate the positions, like:
- (void)mapView:(GMSMapView *)mapView didChangeCameraPosition:(GMSCameraPosition *)position
{
if (points != nil)
{
// Determine new points to pass
for (int i=0; i<howmanypoints; i++)
{
for(int j=0; j<pointsForPolygon[i]; j++)
{
// Call method to determine new CGPoint for each coordinate
points[i][j] = [self.mapView.projection pointForCoordinate:acoordinates[i][j]];
}
}
// No need to pass points again as they were passed as pointers, just refresh te view
[self.overView setNeedsDisplay];
}
}
And that's it. Hope you got the gist of it. Please, comment if I need to clarify something. I can also make a small complete project and upload it to github so you can research it better.

How can I render line faster than CGContextStrokePath?

I'm plotting ~768 points for a graph using CGContextStrokePath. The problem is that every second I get a new data point, and thus redraw the graph. This is currently taking 50% CPU in what's already a busy App.
Graph drawing is done in drawRect in a UIView. The graph is time based, so new data points always arrive on the right hand side.
I'm thinking a few alternative approaches:
Draw with GLKit (at cost of not supporting older devices) and seems like a lot of work.
Do some kind of screen grab (renderInContext?), shift left by 1 px, blit, and only draw a line for the last two data points.
Have a very wide CALayer and pan along it?
Smooth the data set, but this feels like cheating :)
It's also possible I'm missing something obvious here that I'm seeing such poor performance?
CGContextBeginPath(context);
CGContextSetLineWidth(context, 2.0);
UIColor *color = [UIColor whiteColor];
CGContextSetStrokeColorWithColor(context, [color CGColor]);
…
CGContextAddLines(context, points, index);
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
CGContextClosePath(context);
CGContextStrokePath(context);
Let's implement a graphing view that uses a bunch of tall, skinny layers to reduce the amount of redrawing needed. We'll slide the layers to the left as we add samples, so at any time, we probably have one layer hanging off the left edge of the view and one hanging off the right edge of the view:
You can find a complete working example of the code below on my github account.
Constants
Let's make each layer 32 points wide:
#define kLayerWidth 32
And let's say we're going to space the samples along the X axis at one sample per point:
#define kPointsPerSample 1
So we can deduce the number of samples per layer. Let's call one layer's worth of samples a tile:
#define kSamplesPerTile (kLayerWidth / kPointsPerSample)
When we're drawing a layer, we can't just draw the samples strictly inside the layer. We have to draw a sample or two past each edge, because the lines to those samples cross the edge of the layer. We'll call these the padding samples:
#define kPaddingSamples 2
The maximum dimension of an iPhone screen is 320 points, so we can compute the maximum number of samples we need to retain:
#define kMaxVisibleSamples ((320 / kPointsPerSample) + 2 * kPaddingSamples)
(You should change the 320 if you want to run on an iPad.)
We'll need to be able to compute which tile contains a given sample. And as you'll see, we'll want to do this even if the sample number is negative, because it will make later computations easier:
static inline NSInteger tileForSampleIndex(NSInteger sampleIndex) {
// I need this to round toward -∞ even if sampleIndex is negative.
return (NSInteger)floorf((float)sampleIndex / kSamplesPerTile);
}
Instance Variables
Now, to implement GraphView, we'll need some instance variables. We'll need to store the layers that we're using to draw the graph. And we want to be able to look up each layer according to which tile it's graphing:
#implementation GraphView {
// Each key in _tileLayers is an NSNumber whose value is a tile number.
// The corresponding value is the CALayer that displays the tile's samples.
// There will be tiles that don't have a corresponding layer.
NSMutableDictionary *_tileLayers;
In a real project, you'd want to store the samples in a model object and give the view a reference to the model. But for this example, we'll just store the samples in the view:
// Samples are stored in _samples as instances of NSNumber.
NSMutableArray *_samples;
Since we don't want to store an arbitrarily large number of samples, we'll discard old samples when _samples gets big. But it will simplify the implementation if we can mostly pretend that we never discard samples. To do that, we keep track of the total number of samples ever received.
// I discard old samples from _samples when I have more than
// kMaxTiles' worth of samples. This is the total number of samples
// ever collected, including discarded samples.
NSInteger _totalSampleCount;
We should avoid blocking the main thread, so we'll do our drawing on a separate GCD queue. We need to keep track of which tiles need to be drawn on that queue. To avoid drawing a pending 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's the GCD queue on which we'll do the drawing.
// Methods prefixed with rq_ run on redrawQueue.
// All other methods run on the main queue.
dispatch_queue_t _redrawQueue;
}
Initialization / Destruction
To make this view work whether you create it in code or in a nib, 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 do the 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 won't clean up the GCD queue for us:
- (void)dealloc {
if (_redrawQueue != NULL) {
dispatch_release(_redrawQueue);
}
}
Adding a sample
To add a new sample, we pick a random number and append it to _samples. We also increment _totalSampleCount. We discard the oldest samples if _samples has gotten big.
- (void)addRandomSample {
[_samples addObject:[NSNumber numberWithFloat:120.f * ((double)arc4random() / UINT32_MAX)]];
++_totalSampleCount;
[self discardSamplesIfNeeded];
Then, we check if we've started a new tile. If so, we find the layer that was drawing the oldest tile, and reuse it to draw the newly-created tile.
if (_totalSampleCount % kSamplesPerTile == 1) {
[self reuseOldestTileLayerForNewestTile];
}
Now we recompute the layout of all the layers, which will to the left a bit so the new sample will be visible in the graph.
[self layoutTileLayers];
Finally, we add tiles to the redraw queue.
[self queueTilesForRedrawIfAffectedByLastSample];
}
We don't want to discard samples one at a time. That would be inefficient. Instead, we let the garbage build up for a while, then throw it away all at once:
- (void)discardSamplesIfNeeded {
if (_samples.count >= 2 * kMaxVisibleSamples) {
[_samples removeObjectsInRange:NSMakeRange(0, _samples.count - kMaxVisibleSamples)];
}
}
To reuse a layer for the new tile, we need to find the layer of the oldest tile:
- (void)reuseOldestTileLayerForNewestTile {
// The oldest tile's layer should no longer be visible, so I can reuse it as the new tile's layer.
NSInteger newestTile = tileForSampleIndex(_totalSampleCount - 1);
NSInteger reusableTile = newestTile - _tileLayers.count;
NSNumber *reusableTileObject = [NSNumber numberWithInteger:reusableTile];
CALayer *layer = [_tileLayers objectForKey:reusableTileObject];
Now we can remove it from the _tileLayers dictionary under the old key and store it under the new key:
[_tileLayers removeObjectForKey:reusableTileObject];
[_tileLayers setObject:layer forKey:[NSNumber numberWithInteger:newestTile]];
By default, when we move the reused layer to its new position, Core Animation will animate it sliding over. We don't want that, because it will be a big empty orange rectangle sliding across our graph. We want to move 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'll always want to redraw the tile containing the sample. We also need to redraw the prior tile, if the new sample is within the padding range of the prior tile.
- (void)queueTilesForRedrawIfAffectedByLastSample {
[self queueTileForRedraw:tileForSampleIndex(_totalSampleCount - 1)];
// This redraws the second-newest tile if the new sample is in its padding range.
[self queueTileForRedraw:tileForSampleIndex(_totalSampleCount - 1 - kPaddingSamples)];
}
Queuing a tile for redraw is just a matter of adding it to the redraw set and dispatching a 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 the GraphView when it first appears, and any time its size changes (such as if a device rotation resizes it). And we only get the layoutSubviews message when we're really about to appear on the screen, with our final bounds set. So layoutSubviews is a good place to set up the tile layers.
First, we need to create or remove layers as necessary so we have the right layers for our size. Then we need to lay out the layers by setting their frames appropriately. Finally, for each layer, we need to queue its tile for redraw.
- (void)layoutSubviews {
[self adjustTileDictionary];
[CATransaction begin]; {
// layoutSubviews only gets called on a resize, when I will be
// shuffling layers all over the place. I don't want to animate
// the layers to their new positions.
[CATransaction setDisableActions:YES];
[self layoutTileLayers];
} [CATransaction commit];
for (NSNumber *key in _tileLayers) {
[self queueTileForRedraw:key.integerValue];
}
}
Adjusting the tile dictionary means setting up a layer for each visible tile and removing layers for non-visible tiles. We'll just reset the dictionary from scratch each time, but we'll try to reuse the layer's we've already created. The tiles that need layers are the newest tile, and preceding 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];
}
}
The first time through, and any time the view gets sufficiently wider, we need to create new layers. While we're creating the view, we'll tell it to avoid animating its contents or position. Otherwise it 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 the tile layers is just a matter of setting each layer's frame:
- (void)layoutTileLayers {
[_tileLayers enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
CALayer *layer = obj;
layer.frame = [self frameForTile:[key integerValue]];
}];
}
Of course the trick is computing the frame for each layer. And the y, width, and height parts are easy enough:
- (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 compute the x coordinate of the tile's frame, we compute the x coordinate of the first sample in the tile:
- (CGFloat)xForTile:(NSInteger)tile myBounds:(CGRect)myBounds {
return [self xForSampleAtIndex:tile * kSamplesPerTile myBounds:myBounds];
}
Computing the x coordinate for a sample requires a little thought. We want the newest sample to be at the right edge of the view, and the second-newest to be kPointsPerSample points to the left of that, and so on:
- (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 actually draw tiles. We're going to do the drawing on a separate GCD queue. We can't safely access most Cocoa Touch objects from two threads simultaneously, so we need to be careful here. We'll use a prefix of rq_ on all the methods that run on _redrawQueue to remind ourselves that we're not on the main thread.
To redraw one tile, we need to get the tile number, the graphical bounds of the tile, and the points to draw. All of those things come from data structures that we might be modifying on the main thread, so we need to access them only on the main thread. So we dispatch back to the main queue:
- (void)rq_redrawOneTile {
__block NSInteger tile;
__block CGRect bounds;
CGPoint pointStorage[kSamplesPerTile + kPaddingSamples * 2];
CGPoint *points = pointStorage; // A block cannot reference a local variable of array type, so I need a pointer.
__block NSUInteger pointCount;
dispatch_sync(dispatch_get_main_queue(), ^{
tile = [self dequeueTileToRedrawReturningBounds:&bounds points:points pointCount:&pointCount];
});
It so happens that we might not have any tiles to redraw. If you look back at queueTilesForRedrawIfAffectedByLastSample, you'll see that it usually tries to queue the same tile twice. Since _tilesToRedraw is a set (not an array), the duplicate was discarded, but rq_redrawOneTile was dispatched twice anyway. So we need to check that we actually have a tile to redraw:
if (tile == NSNotFound)
return;
Now we need to actually draw the tile's samples:
UIImage *image = [self rq_imageWithBounds:bounds points:points pointCount:pointCount];
Finally we need to update the tile's layer to show the new image. We can only touch a layer on the main thread:
dispatch_async(dispatch_get_main_queue(), ^{
[self setImage:image forTile:tile];
});
}
Here's how we actually draw the image for the layer. I will assume you know enough Core Graphics 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 have to get the tile, the graphics bounds, and the points to draw. We dispatched back to the main thread to do it:
// 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;
The graphics bounds are just the bounds of the tile, just like we computed earlier to set the frame of the layer:
*boundsOut = [self frameForTile:tile];
I need to start graphing from the padding samples before the first sample of the tile. But, prior to having enough samples to fill the view, my tile number may actually be negative! So I need to be sure not to try to access a sample at a negative index:
NSInteger sampleIndex = MAX(0, tile * kSamplesPerTile - kPaddingSamples);
We also need to make sure we don't try to run past the end of the samples when we compute the sample at which we stop graphing:
NSInteger endSampleIndex = MIN(_totalSampleCount, tile * kSamplesPerTile + kSamplesPerTile + kPaddingSamples);
And when I actually access the sample values, I need to account for the samples I've discarded:
NSInteger discardedSampleCount = _totalSampleCount - _samples.count;
Now we can compute the actual points to 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 the tile:
*pointCountOut = count;
return tile;
}
Here's how we actually pull a tile off the redraw queue. Remember that the queue might be empty:
- (NSInteger)dequeueTileToRedraw {
NSNumber *number = [_tilesToRedraw anyObject];
if (number) {
[_tilesToRedraw removeObject:number];
return number.integerValue;
} else {
return NSNotFound;
}
}
And finally, here's how we actually set the tile layer's contents to the new image. Remember that we dispatched 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;
}
}
Making it sexier
If you do all of that, it will work fine. But you can actually make it slightly nicer-looking by animating the repositioning of the layers when a new sample comes in. This is very easy. We just modify newTileLayer so that it adds an 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 like this:
- (CAAnimation *)newTileLayerPositionAnimation {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.duration = 0.1;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
return animation;
}
You will want to set the duration to match the speed at which new samples arrive.
You don't have to rasterize whole path every time you draw it - you can cache it as raster bitmap. BTW, your idea with "scrolling" is standard solution for such task...
Create a bitmap context the same height as your view but twice the width. Start drawing your points into the context, then in drawRect create a CGImageRef. The idea is to as you initially fill the screen your image will start at the beginning. The image you will draw will have the proper width and height, but the bytesPerRow will be 2x (more on that). You continue to draw new points as they come until you get to the last point - now x is exhausted.
Continue writing points in your context, but now, when you create the image, offset the initial pointer by one pixel. Continue doing this until you have done 2x lines - you are now at the very very end of your context.
At that one time, you will need to move the "right" side of the image to the left, and reset your offset count. That is, you will need to memcpy(starOfBitMap, startOfBitMap+bytesPerRow/2, sizeOfBitMap - bytesPerRow/2). In essence, you are left shifting one visible frame.
Now as you add new lines, its at the end of the first frame, and you start offseting by one pixel as you draw.

Rendering a UIView into a PDF as vectors on an iPad - Sometimes renders as bitmap, sometimes as vectors

I have an iPad app and I'm trying to generate a PDF from a UIView and it's almost working perfectly.
The code is really simple as follows:
UIGraphicsBeginPDFContextToFile( filename, bounds, nil );
UIGraphicsBeginPDFPage();
CGContextRef pdfContext = UIGraphicsGetCurrentContext();
[view.layer renderInContext:pdfContext];
UIGraphicsEndPDFContext();
This works really well with one weird exception. If the view has been on screen before being rendered to PDF then the UILabels on the view are rendered to the PDF as wonderful vectors. If the view has not yet been on the screen (IE the controller was initWithNib etc but hasn't been pushed into a navigation controller or anything) then the text is rendered as a bitmap at 'ipad' resolution.
It's like the act of getting rendered to the screen sets up the view to be rendered as vectors when I subsequently render it to a pdf context.
Is there some method I can call or property I can set on the view or the layer or elsewhere to mimic this behaviour without having to show the view on screen?
Is it something to do with UIViewPrintFormatter?
The only way I found to make it so labels are rendered vectorized is to use a subclass of UILabel with the following method:
/** Overriding this CALayer delegate method is the magic that allows us to draw a vector version of the label into the layer instead of the default unscalable ugly bitmap */
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
BOOL isPDF = !CGRectIsEmpty(UIGraphicsGetPDFContextBounds());
if (!layer.shouldRasterize && isPDF)
[self drawRect:self.bounds]; // draw unrasterized
else
[super drawLayer:layer inContext:ctx];
}
Swift 5.x:
override func draw(_ layer: CALayer, in ctx: CGContext) {
let isPDF = !UIGraphicsGetPDFContextBounds().isEmpty
if !self.layer.shouldRasterize && isPDF {
self.draw(self.bounds)
} else {
super.draw(layer, in: ctx)
}
}
That does the trick for me: labels are unrasterized and selectable in the resulting PDF view, and behave normally when rendered to the screen.
What about if you add the view on screen but at offscreen coordinates. This seems more like a hack but it might work.
I want to suggest an alternative to mprudhom's great solution:
Using the UIString extensions you can also make the text in the UILabel be rendered as font (with select'n'copy support etc.)
This way the glyphs of the font are embedded in the PDF correctly.
To support right and center text alignments as well as the default vertical centered alignment, I had to calculate a bounding box for the drawInRect method.
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
BOOL isPDF = !CGRectIsEmpty(UIGraphicsGetPDFContextBounds());
if (!layer.shouldRasterize && isPDF) {
// [self drawRect:self.bounds];
CGSize fitSize = [self sizeThatFits:self.bounds.size];
float x = self.bounds.origin.x;
float y = self.bounds.origin.y;
if (self.textAlignment == NSTextAlignmentCenter) {
x += (self.bounds.size.width - fitSize.width) / 2.0f;
y += (self.bounds.size.height - fitSize.height) / 2.0f;
} else if (self.textAlignment == NSTextAlignmentRight) {
x += self.bounds.size.width - fitSize.width;
y += self.bounds.size.height - fitSize.height;
}
[self.textColor set];
[self.text drawInRect:CGRectMake(x, y, fitSize.width, fitSize.height) withFont:self.font lineBreakMode:NSLineBreakByWordWrapping alignment:self.textAlignment];
} else {
[super drawLayer:layer inContext:ctx];
}
}
Use drawInContext rather than renderInContext.
Trying using the view's viewPrintFormatter.
Instead of [view.layer renderInContext:pdfContext];
try this
CALayer* formattedLayer = [view viewPrintFormatter].view.layer;
[formattedLayer renderInContext:pdfContext];

Resources