how to avoid overriding drawRect when clipping - objective-c

At the moment i'm overriding drawRect in a view in order to be able to clip a colored rectangle with a mask (using CGContextClipToMaskinside drawRect).
Sometimes I change the color of this clipped rectangle. In this case drawRect gets called again, redrawing and clipping the rectangle with the new color.
Now I don't want to change the color at once, but animate this. The problem is, that animations are not performed when overriding drawRect (drawRect only gets called one time and immediately).
Is there a way to perform this animation, maybe by subclassing the view, so that I still override drawRectin the superclass, but the animation is somehow performed through the subclass so that drawRect from the superclass gets performed multiple times during the animation?
Or is also possible not to override drawRect at all, and still be able to clip this rectangle with the mask somehow?
drawRectlooks something like this:
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGImageRef maskImage = [[UIImage imageNamed:maskName] CGImage];
CGContextClipToMask(ctx, rect, maskImage);
CGContextSetFillColorWithColor(ctx, self.currentColor);
CGContextFillRect( ctx, rect );

I think it is better to use a CALayer to mask your view (don't forget to import QuartzCore):
maskLayer.contents = (id)[UIImage imageNamed:maskName].CGImage; // maskLayer is a CALayer
view.layer.mask = maskLayer;
Now assuming you have a method -(void)animate in your view controller, you have execute it multiple times using a timer, or using recursive calls to performSelector:withObject:afterDelay.

One solution would be to have a timer that fires to redraw repeatedly over time whenever the color is changed. When the user picks a new color, star an NSTimer that fires, say 10x a second. When the timer fires, update the current color to be partway from the start to the end color, and invalidate the view. After you reach the final color, kill the timer.

Related

Move rect in NSView

I've got a NSView subclass "graphics" where some rects are drawn.
Now I want a public method to move/resize the rects. I tried to set a new origins, but that doesn't work for me.
rect.origin.x = anything;
Now how can I move the rects?
regards
You are drawing the rectangles in a method called "drawRect", which is run every time the NSView thinks it should need to redisplay itself.
Your problem is that you are setting the initial location of the rectangles in this method, so every changed you make in other method wouldn't matter and would be reset every time. You should instead set the rectangles initial location in methods like init or awakeFromNib that only happen once.
After your button changed the values of the origin of the rectangle, you can tell the view to redraw itself, using :
[self setNeedsDisplay:YES];
So add this after the code that changes the rect origin and you should be fine.

Apply CALayer changes to UIView

I have a UIView. I applied the animation to its CALayer.
[view.layer addAnimation:groupAnimation forKey:name];
I want the final state of the layer to be the state of the UIView after the animation. Let's say I rotated by 45degrees and moved to a new position using the layer; is it possible for my view to be in that state after the animation? Because right now, after the animation, it goes back to the original state of the UIView. I hope to receive some help with this. Thanks.
The thing to understand is that layer animation is just animation; it's merely a kind of temporary "movie" covering the screen. When the animation ends, the "movie" is removed, thus revealing the true situation. It is up to you to match that situation with the final frame of the movie.
UIView animation does this for you, to a great extent. But layer animation leaves it entirely up to you.
Thus, let's say you animate a position change; doing something so that the layer or view actually is where you animated to is completely up to you.
The usual thing is to perform the changes yourself as a separate set of commands. But be careful not to do anything that might trigger implicit layer animation, as this will conflict with the animation you are trying to perform explicitly.
Here's example code (not related to yours, but it does show the general form):
CAAnimationGroup* group = // ...
// ... configure the animation ...
[view.layer addAnimation:group forKey:nil];
// now we are ready to set up the view to look like the final "frame" of the animation
[CATransaction setDisableActions:YES]; // do not trigger implicit animation by mistake
view.layer.position = finalPosition; // assume we have worked this out
When animating a CALayer or using a CAAnimationGroup, the following properties must be set, e.g.:
groupAnimation.removedOnCompletion = NO;
groupAnimation.fillMode = kCAFillModeForwards;
See also: After Animation, View position resets
Also note that it may be helpful to apply animations directly to the view itself, rather than by accessing the view's layer. This is accomplished using animation blocks, which I have found to be very useful.
Block style animations can be customized in many ways, but here's a basic example, which could be invoked within a function when your view needs to animate:
- (void) animateMyView
{
CGRect newViewFrame = CGRectMake(x,y,w,h);
[UIView animateWithDuration:0.5
delay:0
options: (UIViewAnimationOptionCurveLinear )
animations:^{
self.myView = CGAffineTransformMakeRotation (45);
[self.myView setBounds:newViewFrame];
}
completion:NULL];
}
For more information, see Apple's documentation on View Animation.

How to draw text in UIView after -drawRect has been called

I have an iPad app, XCode 4.5, iOS 6.1 and Storyboards. I have a UIView, with two (2) UIViews embedded in it. The first UIView (called Calendar) is on the top half, and the second UIView (called Schedule) is on the bottom half. I create a calendar on the top half, and draw a grid to display a schedule on the bottom half.
The sequence of events is the user is presented with the scene with the Calendar (completely filled in for current month) and Schedule (an empty grid is displayed). When a day is tapped on the Calendar, the schedule for that day will be displayed in the Schedule view.
The problem is -drawRect is called when the scene is initially displayed, before the user has the ability to choose a date to display for the schedule. Since I have to do the drawing from within -drawRect, I can't figure out how to fill in the schedule outside of -drawRect. I was wondering if using setNeedsDisplayInRect:, would accomplish what I need to do?
I have looked at SO and Google for several days now, and found nothing that would answer this particular question. This is NOT a duplicate of this question, but rather a follow-on since the project has been redefined. Any ideas would be gratefully appreciated.
UPDATE: here is my code using setNeedsDisplayInRect:
- (void) drawSchedule {
const float rectWidth = 120.0;
// draw detail for selected day for each staff member
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextSetRGBFillColor(context, 0, 0, 1, 0.3);
UIGraphicsBeginImageContext(self.bounds.size);
// draw customer name using bounds from CGRectMake
const float nameFontSize = 12;
UIFont *hourfont=[UIFont systemFontOfSize: nameFontSize];
[[UIColor blackColor] set];
[self setNeedsDisplayInRect: CGRectMake(160.0, 150.0, 120.0, 50.0)]; // mark as needs to be redrawn
// customer for this time slot
[#"John Doe" drawAtPoint:CGPointMake(114, 40) withFont:hourfont];
}
When a day is tapped on the Calendar, the schedule for that day will be displayed in the Schedule view.
When you user taps the a day on the calendar, tell the Schedule to change:
[self.calendarView displayDate: thatDay];
The Schedule, in turn, gets the message, figures out what it needs to do, and tells itself to redisplay when its time to draw.
- (void) displayDate: (YourDateObject*) thatDay
{
self.date=thatDay;
[self setNeedsDisplay: YES]
}
Now, the system will call the Schedule's drawRect: at the appropriate time, and you'll display the new schedule.
The general rule is simply this: set the state of the view when you know it, and let the view redisplay itself when asked. Don't try to say, "Draw this"; instead, say, "Here is what you should draw next time. As soon as you're ready, raise your hand; the teacher will call on you."
Perhaps you can use UIView's method: setNeedsDisplay. This will cause drawRect: to be called again. Or, if you only want to redraw a portion of the view you could use setNeedsDisplayInRect:, as you mentioned.
You should never call drawRect: yourself.
UIGraphicsBeginImageContext(self.bounds.size)
[#"John Doe" drawAtPoint:CGPointMake(114,40) withFont:hourFont];
UIimage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
Then you can use imageView to add it as a sub-view to your view.
Typically, you should do all of your drawing within the drawRect: method. When a user taps on a date in the calendar, you only need to send the setNeedsUpdate message to the schedule view. Then the schedule view will automatically redraw and call its own drawRect: method.
Calling setNeedsDisplayInRect: (or setNeedsDisplay) simply notifies the system that the receiver needs to be redrawn. Your drawRect: method will be invoked at some point later, when it is appropriate to perform the actual drawing.
This question is moot; you can't clear the drawing on a UIView, which makes this design not viable.
I am now considering using UITableViews or a UIView sub-view on top of the schedule grid, which brings up new issues.
Thank you everybody for your comments.

How to create a "stretchable" UIView

I have a UIView that contains another UIView. The outer UIView draws a border around the inner UIView via drawRect. (The border is too complicated to be drawn via CALayer properties.)
At present, when I animate the resizing of the outer UIView, its drawRect method is called once at the beginning of the animation and the result is stretched or shrunk. This does not look good.
I am looking for a way to either redraw the content at every step of the animation, or find a way to achieve the same visual effect. (The result should be similar to the resizing of a stretchable UIImage.)
You should change view's content type to:
your_view.contentMode = UIViewContentModeRedraw;
And it will redraw each time its frame changes.
I ended up adding subviews with autoresizing masks that kept them positioned correctly during the animation.
You need to send a [UIView setNeedsToDisplay] to the view for every time the frame size is changed, you could try overriding the setFrame: method like
- (void)setFrame:(CGRect)r
{
[super setFrame:r];
[self setNeedsToDisplay];
}

Tracking a Core Animation animation

I have two circles which move around the screen. The circles are both UIViews which contain other UIViews. The area outside each circle is transparent.
I have written a function to create a CGPath which connects the two circles with a quadrilateral shape. I fill this path in a transparent CALayer which spans the entire screen. Since the layer is behind the two circular UIViews, it appears to connect them.
Finally, the two UIViews are animated using Core Animation. The position and size of both circles change during this animation.
So far the only method that I have had any success with is to interrupt the animation at regular intervals using an NSTimer, then recompute and draw the beam based on the location of the circle's presentationLayer. However, the quadrilateral lags behind the circles when the animation speeds up.
Is there a better way to accomplish this using Core Animation? Or should I avoid Core Animation and implement my own animation using an NSTimer?
I faced a similar problem. I used layers instead of views for the animation. You could try something like this.
Draw each element as a CALayer and include them as sublayers for your container UIVIew's layer. UIViews are easier to animate, but you will have less control. Notice that for any view you can get it's layer with [view layer];
Create a custom sublayer for your quadrilateral. This layer should have a property or several of properties you want to animate for this layer. Let's call this property "customprop". Because it is a custom layer, you want to redraw on each frame of the animation. For the properties you plan to animate, your custom layer class should return YES needsDisplayForKey:. That way you ensure -(void)drawInContext:(CGContextRef)theContext gets called on every frame.
Put all animations (both circles and the quad) in the same transaction;
For the circles you can probably use CALayers and set the content, if it is an image, the standard way:
layer.contents = [UIImage imageNamed:#"circle_image.png"].CGImage;
Now, for the quad layer, subclass CALayer and implement this way:
- (void)drawInContext:(CGContextRef)theContext{
//Custom draw code here
}
+ (BOOL)needsDisplayForKey:(NSString *)key{
if ([key isEqualToString:#"customprop"])
return YES;
return [super needsDisplayForKey:key];
}
The transaction would look like:
[CATransaction begin];
CABasicAnimation *theAnimation=[CABasicAnimation animationWithKeyPath:#"customprop"];
theAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(1000, 1000)];
theAnimation.duration=1.0;
theAnimation.repeatCount=4;
theAnimation.autoreverses=YES;
theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
theAnimation.delegate = self;
[lay addAnimation:theAnimation forKey:#"selecting"];
[CATransaction setValue:[NSNumber numberWithFloat:10.0f]
forKey:kCATransactionAnimationDuration];
circ1.position=CGPointMake(1000, 1000);
circ2.position=CGPointMake(1000, 1000);
[CATransaction commit];
Now all the draw routines will happen at the same time. Make sure your drawInContext: implementation is fast. Otherwise the animation will lag.
After adding each sublayer to the UIViews's layer, rememeber to call [layer setNeedsDisplay]. It does not get called automatically.
I know this is a bit complicated. However, the resulting animations are better than using a NSTimer and redrawing on each call.
If you need to find the current visible state of the layers, you can call -presentationLayer on the CALayer in question, and this will give you a layer that approximates the one used for rendering. Note I said approximates - it's not guaranteed to be fully accurate. However it may be good enough for your purposes.