I have a custom UIView with drawRect:
-(void)drawRect:(CGRect)rect{
CGContextRef context = UIGraphicsGetCurrentContext();
[self drawControlPointsWithCoordsX:point.x andY:point.y forRect:CGRectMake(0, 0, 320, 600) andContext:context];
NSLog(#"Values: %f and %f", point.x, point.y);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self startTimer];
});
}
[self startTimer]; is the initiation method. In this method I start a timer which produces some pairs of ordinates. It is called only once. The coordinates come in this form:
Values: 276.711670 and 117.279999
and keep changing all the time. All the values are logged. In the method which is repeated by the timer every 0.1s I call:
[self drawRect:CGRectMake(0, 0, 320, 600)];
The log works, and the coordinates keep changing, and are correct, but only two or three dots are plotted (instead of more or less 400) and they are in the wrong place.
This is the code to draw the points:
- (void)drawControlPointsWithCoordsX:(int)x andY:(int)y forRect:(CGRect)rect andContext:(CGContextRef)contextRef
{
UIGraphicsPushContext(contextRef);
CGContextSetLineWidth(contextRef, 2.0);
CGContextSetRGBFillColor(contextRef, 0, 0, 1.0, 1.0);
CGContextSetRGBStrokeColor(contextRef, 0, 0, 1.0, 1.0);
CGRect circlePoint = (CGRectMake(x,y, 5.0, 5.0));
CGContextFillEllipseInRect(contextRef, circlePoint);
}
First, you should not be manually calling drawRect:, because it gets called automatically when the view is told to be redrawn. So if you choose to redraw it manually like this, the behavior is probably undefined because what the current graphics context is might be different or not valid. The following is taken from Apple's documentation on the drawRect: method:
This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay or setNeedsDisplayInRect: method instead.
Also you should probably not be starting a timer running from within drawRect:. You should probably be starting your timer from somewhere else, like somewhere in your code where you first display this view. Also each time you redraw the view, it is clearing out what was previously there, not overlaying it. So if you expect this to build on what was previously drawn, you must keep track of every point that you draw and redraw all of them up to the current point. You could accomplish this in other ways that wouldn't involve redrawing what you've already drawn, but it would be more complicated.
Related
I am working on multiple terminal screen app, for that I have a custom UIView subclass for the terminal views. Every time I need a new terminal screen, I prepare a new view.
This view class draws the text using a CGContextRef. The problem I am facing is that the context only draws the text of the last view that was created, e.g. if I have 3 terminals and drawing on first/second, it still draws on the third one.
My code so far:
-(void)drawRect:(CGRect)rect{
contxt = UIGraphicsGetCurrentContext();
}
-(void)setNeedsDisplayInRect:(CGRect)rect{
UIGraphicsPushContext(contxt);
//CGContextSaveGState(contxt);
CGContextSetTextMatrix(contxt,CGAffineTransformIdentity);
if (translated) {
CGContextScaleCTM(contxt, 1, -1);
translated = NO;
}
CGRect rectConvert = CGRectMake(rect.origin.x, rect.origin.y-screenWindowHeight, rect.size.width, rect.size.height);
CGContextSetFillColorWithColor(contxt, bgColor.CGColor);
CGContextFillRect(contxt, rectConvert);
if (!isDeleteChar) {
CGContextSetFillColorWithColor(contxt, fgColor.CGColor);
[displayString drawInRect:rectConvert withFont:font lineBreakMode:NSLineBreakByWordWrapping alignment:NSTextAlignmentLeft];
}
if (ul == EXTENDED_5250_UNDERLINE) {
CGContextSetFillColorWithColor(contxt, fgColor.CGColor);
[#"_" drawInRect:rectConvert withFont:font lineBreakMode:NSLineBreakByWordWrapping alignment:NSTextAlignmentLeft];
}
//CGContextRestoreGState(contxt);
UIGraphicsPopContext();
}
Finally I solved it by own using
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]]; just after setNeedsDisplay.
First and foremost, you should not be doing drawing in the -setNeedsDisplayRect: method, all of your drawing code should be in drawRect: instead. This way, the main runloop can better organize redrawing of the views.
Second, I suspect the variables that you are using for your CGRect conversions are faulty and are drawing outside of the view bounds. You can test this premise by clipping the view's drawing (set layer.masksToBounds to YES for the views)
If this is the case, you can adjust them to be relative to the view (all drawing within the view should be relative to its bounds, not its frame). When drawing the view, assume a canvas that stretches the bounds property of the view, i.e origin at (0,0) and size of (width,height).
Now, it is worth also discussing what the rect property passed to drawRect: really is, it is not guaranteed to be the entire bounds of the view, so you should not assume that. It is the portion of the view that needs to be redrawn, however, common practice (for simpler views) is to ignore that parameter and just redraw the entire view. Once this becomes too expensive (or you need the drawing to be more optimal), you can look into doing partial redraws of your view.
All in all, it is difficult to diagnose the full problem without seeing the entire UIView subclass code.
I have a ViewController on storyboard. I have used the interface builder to set a toolbar at the bottom of the screen. I have set the custom view to a view overrides drawRect. However, for the life of me, I cannot get anything ever to show up on screen called from that drawRect. drawRect itself is called just fine, but nothing shows up on screen.
Also, I have this ViewController with a method that uses AVCaptureSession to toggle its background to a live view from camera input. I had suspected that this might have been the cause for error, but after removing all references of AVCaptureSession, I still cannot get this to work.
Sorry for my writing and/or lack of logic, I don't have any sleep right now.
Edit: Here is a small example of code that won't work. Every method inside gets called, but nothing is to show.
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
// Draw them with a 2.0 stroke width so they are a bit more visible.
CGContextSetLineWidth(context, 2.0);
CGContextMoveToPoint(context, 0,0); //start at this point
CGContextAddLineToPoint(context, 100, 100); //draw to this point
// and now draw the Path!
CGContextStrokePath(context);
}
The documentation for UIView says to not call [super drawRect] if you're overriding UIView. Remove that call! It's been known to cause strange behaviour.
According to the docs:
If you subclass UIView directly, your implementation of this method
does not need to call super. However, if you are subclassing a
different view class, you should call super at some point in your
implementation.
I have a view which implements freehand drawing, but I have a small problem. I noticed on the iPad 3 that everything went to hell, so I tried to update my drawing code (probably as I should have done in the first place) to only update the portion that was stroked. However, the first stroke after open, and the first stroke after about 10 seconds of idle are extremely slow. After everything is "warmed up" it is smooth as butter and only takes about 0.15ms per drawRect. I don't know why, but the whole view rectangle is getting marked as dirty for the first drawRect, and the first drawRect after idle (then it takes about 150 ms to update). The stack trace shows that my rectangle is being overridden by CABackingStoreUpdate_
I tried not drawing the layer if the rectangle was huge, but then my entire context goes blank (will reappear as I draw over the old areas like a lotto ticket). Does anyone have any idea what goes on with UIGraphicsGetCurrentContext()? That's the only place I can imagine the trouble is. That is, my views context got yanked by the context genie so it needs to render itself fully again. Is there any setting I can use to persist the same context? Or is there something else going on here...there is no need for it to update the full rectangle after the initial display.
My drawRect is very simple:
- (void)drawRect:(CGRect)rect
{
CGContextRef c = mDrawingLayer ? CGLayerGetContext(mDrawingLayer) : NULL;
if(!mDrawingLayer)
{
c = UIGraphicsGetCurrentContext();
mDrawingLayer = CGLayerCreateWithContext(c, self.bounds.size, NULL);
c = CGLayerGetContext(mDrawingLayer);
CGContextSetAllowsAntialiasing(c, true);
CGContextSetShouldAntialias(c, true);
CGContextSetLineCap(c, kCGLineCapRound);
CGContextSetLineJoin(c, kCGLineJoinRound);
}
if(mClearFlag)
{
CGContextClearRect(c, self.bounds);
mClearFlag = NO;
}
CGContextStrokePath(c);
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
CGContextDrawLayerInRect(UIGraphicsGetCurrentContext(), self.bounds, mDrawingLayer);
NSLog(#"%.2fms : %f x %f", (CFAbsoluteTimeGetCurrent() - startTime)*1000.f, rect.size.width, rect.size.height);
}
I found a useful thread on on the Apple Dev Forums describing this exact problem. It only exists since iOS 5.0 and the theory is that it is because Apple introduced a double buffering system, so the first two drawRects will always be full. However, there is no explanation for why this will happen again after idle. The theory is that the underlying buffer is not guaranteed by the GPU, and this will be discarded at whim and need to be recreated. The solution (until Apple issues some kind of real solution) is to ping the buffer so that it won't be released:
mDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(pingRect)];
[mDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
- (void)pingRect
{
//Already drawing
if(mTouchCount > 0) return;
//Even touching just one pixel will keep the buffer alive
[self setNeedsDisplayInRect:CGRectMake(0, 0, 1, 1)];
}
The only weakness is if the user keeps their finger perfectly still for more than 5 seconds, but I think that is an acceptable risk.
EDIT Interesting update. It turns out simply calling setNeedsDisplay is enough to keep the buffer alive, even if it returns immediately. So I added this to my drawRect method:
- (void)drawRect:(CGRect)rect
{
if(rect.size.width == 1.f)
return;
//...
}
Hopefully, it will curb the power usage that this refresh method will surely increase.
I don't really understand how CALayer's display and drawInContext relate to drawRect in the view.
If I have an NSTimer that sets the [self.view setNeedsDisplay] every 1 second, then drawRect is called every 1 second, as shown by an NSLog statement inside of drawRect.
But if I subclass a CALayer and use that for the view, if I make the display method empty, then now drawRect is never called. Update: But display is called every 1 second, as shown by an NSLog statement.
If I remove that empty display method and add an empty drawInContext method, again, drawRect is never called. Update: But drawInContext is called every 1 second, as shown by an NSLog statement.
What is exactly happening? It seems that display can selectively call drawInContext and drawInContext can selectively somehow call drawRect (how?), but what is the real situation here?
Update: there is more clue to the answer:
I changed the CoolLayer.m code to the following:
-(void) display {
NSLog(#"In CoolLayer's display method");
[super display];
}
-(void) drawInContext:(CGContextRef)ctx {
NSLog(#"In CoolLayer's drawInContext method");
[super drawInContext:ctx];
}
So, let's say, if there is a moon (as a circle drawn by Core Graphics) at location (100,100) in the View, and now I change it to location (200,200), naturally, I will call [self.view setNeedsDisplay], and now, CALayer will have no cache at all for the new view image, as my drawRect dictates how the moon should now be displayed.
Even so, the entry point is CALayer's display, and then CALayer's drawInContext: If I set a break point at drawRect, the call stack shows:
So we can see that CoolLayer's display is entered first, and it goes to CALayer's display, and then CoolLayer's drawInContext, and then CALayer's drawInContext, even though in this situation, no such cache exist for the new image.
Then finally, CALayer's drawInContext calls the delegate's drawLayer:InContext. The delegate is the view (FooView or UIView)... and drawLayer:InContext is the default implementation in UIView (as I did not override it). It is finally that drawLayer:InContext calls drawRect.
So I am guessing two points: why does it enter CALayer even though there is no cache for the image? Because through this mechanism, the image is drawn in the context, and finally returns to display, and the CGImage is created from this context, and then it is now set as the new image cached. This is how CALayer caches images.
Another thing I am not quite sure is: if [self.view setNeedsDisplay] always trigger drawRect to be called, then when can a cached image in CALayer be used? Could it be... on Mac OS X, when another window covers up a window, and now the top window is moved away. Now we don't need to call drawRect to redraw everything, but can use the cached image in the CALayer. Or on iOS, if we stop the app, do something else, and come back to the app, then the cached image can be used, instead of calling drawRect. But how to distinguish these two types of "dirty"? One is a "unknown dirty" -- that the moon needs to be redrawn as dictated by the drawRect logic (it can use a random number there for the coordinate too). The other types of dirty is that it was covered up or made to disappear, and now needs to be re-shown.
When a layer needs to be displayed and has no valid backing store (perhaps because the layer received a setNeedsDisplay message), the system sends the display message to the layer.
The -[CALayer display] method looks roughly like this:
- (void)display {
if ([self.delegate respondsToSelector:#selector(displayLayer:)]) {
[[self.delegate retain] displayLayer:self];
[self.delegate release];
return;
}
CABackingStoreRef backing = _backingStore;
if (!backing) {
backing = _backingStore = ... code here to create and configure
the CABackingStore properly, given the layer size, isOpaque,
contentScale, etc.
}
CGContextRef gc = ... code here to create a CGContext that draws into backing,
with the proper clip region
... also code to set up a bitmap in memory shared with the WindowServer process
[self drawInContext:gc];
self.contents = backing;
}
So, if you override display, none of that happens unless you call [super display]. And if you implement displayLayer: in FooView, you have to create your own CGImage somehow and store it in the layer's contents property.
The -[CALayer drawInContext:] method looks roughly like this:
- (void)drawInContext:(CGContextRef)gc {
if ([self.delegate respondsToSelector:#selector(drawLayer:inContext:)]) {
[[self.delegate retain] drawLayer:self inContext:gc];
[self.delegate release];
return;
} else {
CAAction *action = [self actionForKey:#"onDraw"];
if (action) {
NSDictionary *args = [NSDictionary dictionaryWithObject:gc forKey:#"context"];
[action runActionForKey:#"onDraw" object:self arguments:args];
}
}
}
The onDraw action is not documented as far as I know.
The -[UIView drawLayer:inContext:] method looks roughly like this:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)gc {
set gc's stroke and fill color spaces to device RGB;
UIGraphicsPushContext(gc);
fill gc with the view's background color;
if ([self respondsToSelector:#selector(drawRect:)]) {
[self drawRect:CGContextGetClipBoundingBox(gc)];
}
UIGraphicsPopContext(gc);
}
The update procedure of UIView is based on the dirty state meaning the view is not likely to be redrawn if there's no change at it's appearance.
That is internal implementation mentioned at the developer reference.
Implementing a drawInContext or display or drawRect tells the OS which one you want called when the view is dirty (needsDisplay). Pick the one you want called for a dirty view and implement that, and don't put any code you depend on getting executed in the others.
I have am trying to draw a vertical line in a UIView (call it View A) based on the center of subview of "View A".
The subview animates well, however, the line drawing is not animated. it bumps to the final position.
For instance if subview is at CGPoint(0,100) and I want to animate to CGPoint(100,100).
THe subview moves as expected, however, the line drawing does not it only appears at CGPoint(100,100) throughout the animation.
Of course inside the animation block I am calling [self setNeedsDisplay]
The code I am using is as follows(the pointView is the subview mentioned above) :
CGPointView newCetner = CGPointMake(100,100);
[UIView animateWithDuration:.5 animations:^{
pointView.center = newCenter;
[self setNeedsDisplay];
}];
in the drawRect method I have the following code:
CGPathMoveToPoint(path, NULL, CGRectGetMidX(pointView.frame), pointView.center.y-100 );
CGPathAddLineToPoint(path, NULL, CGRectGetMidX(pointView.frame), pointView.center.y+100 );
CGContextAddPath(context, path);
CGContextStrokePath(context);
Any ideas how I might force the view to redraw in accordance with the animation block?
By default, animation works by storing the start and final versions of the view, and applying some inexpensive transform to move from one to the other. Intermediate versions of the view are not drawn with drawRect:.
You can request that drawRect: be called for the intermediate steps by passing the option UIViewAnimationOptionAllowAnimatedContent to animateWithDuration:delay:options:animations:completion:. Be sure that drawRect: is fast when using this option, since it will be called often.