Using Quartz to draw every second via NSTimer (iPhone) - iphone

I'm relatively new to Objective-C + Quartz and am running into what is probably a very simple issue. I have a custom UIView sub class which I am using to draw simple rectangles via Quartz. However I am trying to hook up an NSTimer so that it draws a new object every second, the below code will draw the first rectangle but will never draw again. The function is being called (the NSLog is run) but nothing is draw.
Code:
- (void)drawRect:(CGRect)rect {
context = UIGraphicsGetCurrentContext();
[self step:self];
[NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval)(2) target:self selector:#selector(step:) userInfo:nil repeats:TRUE];
}
- (void) step:(id) sender {
static double trans = 0.5f;
CGContextSetRGBFillColor(context, 1, 0, 0, trans);
CGContextAddRect(context, CGRectMake(10, 10, 10, 10));
CGContextFillPath(context);
NSLog(#"Trans: %f", trans);
trans += 0.01;
}
context is in my interface file as:
CGContextRef context;
Any help will be greatly appreciated!

Your example won't work. The reason is that drawRect: is called for you when the view requires drawing, and it can't draw on its own.
Instead, try using your timer from outside of drawRect: (viewDidLoad comes to mind), and each time add an object to draw to a list, and call [view setNeedsDisplay] to request it to be redrawn. There are other techniques if you need stricter control, but it's a good idea to master the basics of application flow first.

Related

core graphics, how to draw lines at runtime?

The task is, to draw paths at runtime on custom maps which im using in a Scrollview, and then i will have to draw paths at runtime whenever the location coordinates (lat, long) updates. The problem what im trying to solve here is that i have made a class 'graphics' which is a subclass of UIView, in which i code the drawing in the 'drawrect:' method. So when im adding the graphics as subview of the scrollview over image, the line draws, but i need to keep drawing the line as though it were paths. I need to draw the lines at runtime, need to keep updating the points(x,y) of 'CGContextStrokeLineSegments' method. The code:
ViewController:
- (void)loadView {
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
CGRect fullScreenRect=[[UIScreen mainScreen] applicationFrame];
scrollView=[[UIScrollView alloc] initWithFrame:fullScreenRect];
graph = [[graphics alloc] initWithFrame:fullScreenRect];
scrollView.contentSize=CGSizeMake(320,480);
UIImageView *tempImageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"fortuneCenter.png"]];
self.view=scrollView;
[scrollView addSubview:tempImageView2];
scrollView.userInteractionEnabled = YES;
scrollView.bounces = NO;
[scrollView addSubview:graph];
}
Graphics.m:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint point [2] = { CGPointMake(160, 100), CGPointMake(160,300)};
CGContextSetRGBStrokeColor(context, 255, 0, 255, 1);
CGContextStrokeLineSegments(context, point, 2);
}
So how can i draw the lines at runtime. Im just simulating right now, so im not using the realtime data (coordinates). Just want to simulate by using dummy data (coordinates of x,y). Lets say have a button, whenever i press it it updates the coordinates so path extends.
The easiest way would be to add an instance variable representing the points to the UIView subclass.
Then, every time the path changes, update the ivar appropriately and call -setNeedsDisplay or setNeedsDisplayInRect on the custom UIView (or even on its superview). The runtime will then redraw the new path.
You just need to make CGPoint point[] dynamically resizable, from the looks of it.
You can use malloc, a std::vector, or even NSMutableData to store the points you add. Then you pass that array to CGContextStrokeLineSegments.
If 2 points is all you will need, move CGPoint point[2] to an ivar so you may store the positions, then (as Rich noted) invalidate rects appropriately when these values (or the array) are changed.
This subject comes up every now and then, so I created a longer blog post on the general concepts involved with one potential solution, creating and using your own graphics context, here: http://www.musingpaw.com/2012/04/drawing-in-ios-apps.html

Least CPU intensive way to Frequently & Repeately Draw Many Views

This Is a problem that I've been leaving and coming back to for a while now. I've never really nailed the problem.
What I've been trying to do use CADisplayLink to dynamically draw pie chart style progress. My code works fine when I have 1 - 4 uiviews updating simultaneously. When I add any more than that the drawing of the pies becomes very jerky.
I want to explain what I have been trying in the hope that somebody could point out the inefficiencies and suggest a better drawing method.
I create 16 uiviews and add a CAShapeLayer subview to each one. This is where I want to draw my pie slices.
I precalcuate 360 CGPaths representing 0 to 360 degrees of a circle and store them in an array to try and improve performance.
In a master View I start a displaylink,loop through all my other views, calculate how much of a full pie it should show, then find the right path and assign it to my shapelayer.
-(void)makepieslices
{
pies=[[NSMutableArray alloc]initWithCapacity:360];
float progress=0;
for(int i=0;i<=360;i++)
{
progress= (i* M_PI)/180;
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath, NULL, 0.f, 0.f);
CGPathAddLineToPoint(thePath, NULL, 28, 0.f);
CGPathAddArc(thePath, NULL, 0.f,0.f, 28, 0.f, progress, NO);
CGPathCloseSubpath(thePath);
_pies[i]=thePath;
}
}
- (void)updatePath:(CADisplayLink *)dLink {
for (int idx=0; idx<[spinnydelegates count]; idx++) {
id<SyncSpinUpdateDelegate> delegate = [spinnydelegates objectAtIndex:idx];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[delegate updatePath:dLink];
});
}
}
- (void)updatePath:(CADisplayLink *)dLink {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
currentarc=[engineref getsyncpercentForPad:cid pad:pid];
int progress;
progress = roundf(currentarc*360);
dispatch_async(dispatch_get_main_queue(), ^{
shapeLayer_.path = _pies[progress];
});
});
}
This technique just straight out isnt working for me when trying to simultaneously update more than 4 or 5 pies at the same time. 16 screen updates at the same time sounds like it should really not be that big of a deal for the ipad to me. So this leads me to think I doing something very very fundamentally wrong.
I'd really appreciate if somebody could tell me why this technique results in jittery screen updates and also if they could suggest a different technique that I could go an investigate that will allow me to perform 16 simultaneous shapelayer updates smoothly.
EDIT Just to give you an idea of how bad performance is, when I have all 16 pies drawing the cpu goes up to 20%
*EDIT *
This is based on studevs advice but I don't see anything been drawn. segmentLayer is a CGLayerRef as a property of my pieview.
-(void)makepies
{
self.layerobjects=[NSMutableArray arrayWithCapacity:360];
CGFloat progress=0;
CGContextRef context=UIGraphicsGetCurrentContext();
for(int i =0;i<360;i++)
{
progress= (i*M_PI)/180.0f;
CGLayerRef segmentlayer=CGLayerCreateWithContext(context, CGSizeMake(30, 30), NULL);
CGContextRef layerContext=CGLayerGetContext(segmentlayer);
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath, NULL, 0.f, 0.f);
CGPathAddLineToPoint(thePath, NULL, 28, 0.f);
CGPathAddArc(thePath, NULL, 0.f,0.f, 28, 0.f, progress, NO);
CGPathCloseSubpath(thePath);
[layerobjects addObject:(id)segmentlayer];
CGLayerRelease(segmentlayer);
}
}
-(void)updatePath
{
int progress;
currentarc=[engineref getsyncpercent];
progress = roundf(currentarc*360);
//shapeLayer_.path = _pies[progress];
self.pieView.segmentLayer=(CGLayerRef)[layerobjects objectAtIndex:progress];
[self.pieView setNeedsDisplay];
}
-(void)drawRect:(CGRect)rect
{
CGContextRef context=UIGraphicsGetCurrentContext();
CGContextDrawLayerInRect(context, self.bounds, segmentLayer);
}
I think one of the first things you should look to do is buffer your segments (currently represented by CGPath objects) offscreen using CGLayer objects. From the docs:
Layers are suited for the following:
High-quality offscreen rendering of drawing that you plan to reuse.
For example, you might be building a scene and plan to reuse the same
background. Draw the background scene to a layer and then draw the
layer whenever you need it. One added benefit is that you don’t need
to know color space or device-dependent information to draw to a
layer.
Repeated drawing. For example, you might want to create a
pattern that consists of the same item drawn over and over. Draw the
item to a layer and then repeatedly draw the layer, as shown in Figure
12-1. Any Quartz object that you draw repeatedly—including CGPath,
CGShading, and CGPDFPage objects—benefits from improved performance if
you draw it to a CGLayer. Note that a layer is not just for onscreen
drawing; you can use it for graphics contexts that aren’t
screen-oriented, such as a PDF graphics context.
Create a UIView subclass that draws the pie. Give it an instance variable for that pie's current progress, and override drawRect: to draw the layer representing that progress. The view needs to first get a reference the required CGLayer object, so implement a delegate with the method:
- (CGLayerRef)pieView:(PieView *)pieView segmentLayerForProgress:(NSInteger)progress context:(CGContextRef)context;
It will then become the delegate's job to return an existing CGLayerRef, or if it doesn't exist yet, create it. Since the CGLayer can only be created from within drawRect:, this delegate method should be called from PieView's drawRect: method. PieView should look something like this:
PieView.h
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#class PieView;
#protocol PieViewDelegate <NSObject>
#required
- (CGLayerRef)pieView:(PieView *)pieView segmentLayerForProgress:(NSInteger)progress context:(CGContextRef)context;
#end
#interface PieView : UIView
#property(nonatomic, weak) id <PieViewDelegate> delegate;
#property(nonatomic) NSInteger progress;
#end
PieView.m
#import "PieView.h"
#implementation PieView
#synthesize delegate, progress;
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGLayerRef segmentLayer = [delegate pieView:self segmentLayerForProgress:self.progress context:context];
CGContextDrawLayerInRect(context, self.bounds, segmentLayer);
}
#end
Your PieView's delegate (most likely your view controller) then implements:
NSString *const SegmentCacheKey = #"SegmentForProgress:";
- (CGLayerRef)pieView:(PieView *)pieView segmentLayerForProgress:(NSInteger)progress context:(CGContextRef)context
{
// First, try to retrieve the layer from the cache
NSString *cacheKey = [SegmentCacheKey stringByAppendingFormat:#"%d", progress];
CGLayerRef segmentLayer = (__bridge_retained CGLayerRef)[segmentsCache objectForKey:cacheKey];
if (!segmentLayer) { // If the layer hasn't been created yet
CGFloat progressAngle = (progress * M_PI) / 180.0f;
// Create the layer
segmentLayer = CGLayerCreateWithContext(context, layerSize, NULL);
CGContextRef layerContext = CGLayerGetContext(segmentLayer);
// Draw the segment
CGContextSetFillColorWithColor(layerContext, [[UIColor blueColor] CGColor]);
CGContextMoveToPoint(layerContext, layerSize.width / 2.0f, layerSize.height / 2.0f);
CGContextAddArc(layerContext, layerSize.width / 2.0f, layerSize.height / 2.0f, layerSize.width / 2.0f, 0.0f, progressAngle, NO);
CGContextClosePath(layerContext);
CGContextFillPath(layerContext);
// Cache the layer
[segmentsCache setObject:(__bridge_transfer id)segmentLayer forKey:cacheKey];
}
return segmentLayer;
}
So for each pie, create a new PieView and set it's delegate. When you need to update a pie, update the PieView's progress property and call setNeedsDisplay.
I'm using an NSCache here since there are a lot of graphics being stored, and it could take up a lot of memory. You could also limit the number of segments being drawn - 100 is probably plenty. Also, I agree with other comments/answers that you might try updating the views less often, as this will consume less CPU and battery power (60fps is probably not necessary).
I did some crude testing of this method on an iPad (1st gen) and managed to get well over 50 pies updating at 30fps.
dubbeat: ...CADisplayLink...
Justin: do you need to draw at the display's refresh rate?
dubbeat: The progress of the pie drawing is supposed to represent the progress of an mp3s playback progress so I guess at the displays refresh rate at a minimum.
That's much faster than is necessary, unless you're trying to display some really, really, really exotic visualizer, which is very unlikely if your spinner's radius is 28pt. Also, there's no reason to draw faster than the display's frequency.
One side effect is that your spinner's superviews may also updating at this high frequency. If you can make the spinner view opaque, then you can reduce overdrawing of superviews (and subviews if you have them).
60fps is a good number for a really fast desktop game. For an ornament/progress bar, it's far more than necessary.
Try this:
not using CADisplayLink, but the standard view system
use an NSTimer on the main run loop, begin with a frequency of 8 Hz*
adjust timer to taste
then let us know if that is adequately fast.
*the timer callback calls [spinner setNeedsDisplay]
Well, you could achieve some performance improvement by pre-assembling the background view, capturing the image of it, and then just using the image in an image view for the background. You could go further by capturing a view of the "relatively static" parts of your chart, updating that static view only when necessary.
Store your 360 circle segments as textures and use OpenGL to animate the sequences.

Draw a Path - iPhone

I am animating an object along a path on the iPhone. The code works great using CAKeyframeAnimation. For debugging purposes, I would like to draw the line on the screen. I can't seem to do that using the current CGContextRef. Everywhere I look it says you need to subclass the UIView then override the drawRect method to draw on the screen. Is there anyway around this? If not, how do I pass data to the drawRect method so it knows what do draw?
EDIT:
Ok. I ended up subclassing UIView and implementing the drawing in the drawRect method. I can get it to draw to the screen by creating another method in the subclassed view (called drawPath) that sets an instance variable then calls setNeedsDisplay. That in turn fires the drawRect method which uses the instance variable to draw to the screen. Is this the best practice? What happens if I want to draw 20+ paths. I shouldn't have to create properties for all of these.
In your drawRect method of UIView put some code like this
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 0,0 , 0.0);
CGContextSetLineWidth(context, 3.f);
CGContextBeginPath(context);
CGContextMoveToPoint(context,x1,y1);
CGContextAddLineToPoint(context, x2 , y2);
CGContextStrokePath(context);
If you want to use Core Graphics drawing use CALayer. If you do not want to subclass it, delegate drawing to the view.
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
CALayer* myLayer = [CALayer new];
myLayer.delegate = self;
self.layer = myLayer;
}
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
// draw your path here
}
Do not forget to call [self.layer setNeedsDisplay]; when you need to redraw it.

Even and odd buffers in UIView drawRect?

Hello again (I thank you all for your previous invaluable help).
The new issue: I am working up physics simulation but have encountered a problem with a toy version. All it does is draw a random rectangle on the iPhone/Touch screen each time NSStimer timer calls the tick method. I've indexed these calls by int frameCount. What one sees is two sets of images gradually building up: one set for the even frames, another for the odd. I validated this by drawing frameCount to the screen with the even and odd counts in slightly different positions. Thus one sees the counts blinking back and forth synchronously with the image in the frame. The abbreviated code is below. I appreciate any suggestions you might have. It seems to me that there must be two offscreen buffers. But I'm in the dark here:-) And even if this is true, I don't know how to merge them, or copy one to the other.
#define MAXFRAMES 1000
// Relevant parts of implemenation of FooBar, a subclass of UIView:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
// Start a timer that will call the tick method of this class
// 30 times per second -- slowed way down for diagnostics
timer = [NSTimer scheduledTimerWithTimeInterval:(3.0/1.0)
target:self selector:#selector(tick) userInfo:nil repeats:YES];
frameCount = 0;
self.clearsContextBeforeDrawing = NO;
NSLog(#"frame initialized");
}
return self;
}
- (void)tick
{
// Tell the view that it needs to re-draw itself
if (frameCount < MAXFRAMES) {
[self setNeedsDisplay];
frameCount++;
}
}
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
// Draw a random rectangle with CGContextFillRect
// after calling CGContextSetRGBFillColor
// Both called with suitable random parameters
// Draw the frameCount for diagnostic purposes
}
The number of buffers that a UIView might use in its drawing layer is unspecified. If you want to copy drawings, you should draw from and into your own CG drawing context(s), and then use or draw your result into an image that you can use as a view's CALayer contents.

Simple animation via delayed drawing

I'm trying to implement some very simple line drawing animation for my iPhone app. I would like to draw a rectangle on my view after a delay. I'm using performSelector to run my drawing method.
-(void) drawRowAndColumn: (id) rowAndColumn
{
int rc = [rowAndColumn intValue];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, currentColor.CGColor);
CGRect rect = CGRectMake(rc * 100, 100, 100, 100);
CGContextAddRect(context, rect);
CGContextDrawPath(context, kCGPathFillStroke);
}
Which is then invoked via:
int col = 10;
[self performSelector:#selector(drawRowAndColumn:)
withObject:[NSNumber numberWithInt:col]
afterDelay:0.2];
But it seems that when the drawRowAndColumn: message is actually sent, it no longer has access to a valid CGContextRef, as I get errors such as:
<Error>: CGContextAddRect: invalid context
If I replace the performSelector with a direct call to drawRowAndColumn, it works fine. So my first idea was to also pass in the CGContextRef via the performSelector, but I can't seem to figure out how to pass multiple arguments at the same time (that's another good question.)
What's wrong with above code?
You can't just draw at any time like that. You need to implement the drawRect: method of UIView and put your drawing code in there.
To get drawRect: to fire you need to let Cocoa know that the view needs to be drawn. For that you can call setNeedsDisplay, or setNeedsDisplayInRect:.
Directly translating your attempt this way you'd call setNeedsDisplay using performSelector:withObject:afterDelay - but that's probably not a good way to do animation.
It depends what you're really trying to do - but you could consider, for example, putting your drawing code in drawRect as I suggested, but start the view hidden. You could then call setHidden:NO using performSelector to make it appear after a delay - or you could smoothly animate it in by starting not hidden, but with an alpha of 0, then change alpha to 1 within a UIView animation block (there's lots about this in the docs).