I am using CATiledLayer as backing layer for my UIView, which I have put inside UIScrollView. In init method of my view I am creating CGPathRef object which draws simple line. When I am trying to draw this path inside drawLayer:inContext it occasionally crashes with EXEC_BAD_ACCESS (rarely) when I am scrolling / zooming.
The code is very simple, I am using only standard CG* functions:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
CATiledLayer *tiledLayer = (CATiledLayer *)[self layer];
tiledLayer.levelsOfDetail = 10;
tiledLayer.levelsOfDetailBias = 5;
tiledLayer.tileSize = CGSizeMake(512.0, 512.0);
CGMutablePathRef mutablePath = CGPathCreateMutable();
CGPathMoveToPoint(mutablePath, nil, 0, 0);
CGPathAddLineToPoint(mutablePath, nil, 700, 700);
path = CGPathCreateCopy(mutablePath);
CGPathRelease(mutablePath);
}
return self;
}
+ (Class) layerClass {
return [CATiledLayer class];
}
- (void) drawRect:(CGRect)rect {
}
- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
CGContextSetRGBFillColor(ctx, 1, 1, 1, 1);
CGContextFillRect(ctx, self.bounds);
CGContextSetLineWidth(ctx, 5);
CGContextAddPath(ctx, path);
CGContextDrawPath(ctx, kCGPathStroke);
}
- (void)dealloc {
[super dealloc];
}
UPDATE:
I have noiced that this problem exists only on iOS 5, it works fine on 4.3
I ran into a similar issue when attempting to draw cached CGPath objects on a custom MKOverlayView.
The crash may occur because a CGPath can't be simultaneously drawn on multiple threads – it's an opaque class which (as specified in the documentation) contains a pointer to the current point in its points array. Two or more threads iterating over this array simultaneously while they draw it could lead to undefined behavior and a crash.
I worked around this by copying the CGPath object into each drawing thread (contained within a mutex lock to prevent incomplete copying):
//lock the object's cached data
pthread_mutex_lock(&cachedPathMutex);
//get a handle on the previously-generated CGPath (myObject exists on the main thread)
CGPathRef myPath = CGPathCreateCopy(myObject.cachedPath);
//unlock the mutex once the copy finishes
pthread_mutex_unlock(&cachedPathMutex);
// all drawing code here
CGContextAddPath(context, myPath);
...
...
CGPathRelease(myPath);
If you're concerned about the memory overhead of doing a copy on each thread, you can also work directly on the cached CGPath objects, but the mutex will have to remain locked during the whole drawing process (which kind of defeats the purpose of threaded drawing):
//lock the object's cached data
pthread_mutex_lock(&cachedPathMutex);
//get a handle on the previously-generated CGPath (myObject exists on the main thread)
CGPathRef myPath = myObject.cachedPath;
// draw the path in the current context
CGContextAddPath(context, myPath);
...
...
//and unlock the mutex
pthread_mutex_unlock(&cachedPathMutex);
I'll qualify my answer by saying that I'm not an expert on multithreaded drawing with Quartz, only that this approach solved the crashes in my scenario. Good luck!
UPDATE:
I revisited this code now that iOS 5.1.0 is out and it looks like the root cause of the issue may have actually been a bug in Quartz in iOS 5.0.x. When testing on iOS 5.1.0 with the CGPathCreateCopy() and mutex calls removed, I'm seeing none of the crashes experienced on iOS 5.0.x.
//get a handle on the previously-generated CGPath (myObject exists on the main thread)
CGPathRef myPath = myObject.cachedPath;
// all drawing code here
CGContextAddPath(context, myPath);
...
...
//drawing finished
Since chances are we'll be supporting iOS 5.0.x for a while, it won't hurt to keep the mutex in your code (other than a slight performance hit), or simply run a version check before drawing.
Related
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
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.
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.
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.
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).