Only one day left on the bounty!
Can you have a custom animatable property work with the UIView animateWithDuration block syntax?
I've been working on creating my own implicit animation for a custom animatable property on a custom CALayer.
I have set the backing layer of a custom UIView and can use a CATransaction to animate the custom property (in the custom example I am changing the color of a circle with a custom property circleColor).
I know that UIView turns off animatable properties by returning NSNull from the actionForLayer:forKey: selector method on UIView. Then when the property is wrapped in the UIView animateWithDuration block the UIView will return the animation.
So this will work with backgroundColor, opacity, etc. However, my custom property circleColor still returns NSNull.
I hope I provided enough context to the question. Thanks!
So this question has been open for a long-time and I'm concluding that it isn't possible. The code below shows how I would turn on the custom property to be detected within the block. This method is overridden in UIView.
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
NSLog(#"%s - event: %#", __PRETTY_FUNCTION__, event);
id<CAAction> action = [super actionForLayer:layer forKey:event];
if ([event isEqualToString:#"circleColor"] && (nil == action || (id)[NSNull null] == action)) {
return [CABasicAnimation animationWithKeyPath:event];
}
return action;
}
The code allows you to animate within the UIView block however there is no way for it to detect that it is within a UIView block. So if you use this code then it will even animate outside of the UIView block which defeats the purpose. I'll monitor for other people's responses, but the only options it seems is to make your own custom UIView animation block system which will allow you to add custom animatable properties. I would imagine that Facebook's Pop framework would be a great alternative solution.
Related
I am subclassing CALayer and doing custom drawing in drawInContext: method.
The problem is I need to manually call setNeedsDisplay: after I add an instance of my subclass to a superlayer, something like:
MyCustomLayer *instance = [MyCustomLayer layer];
//do some configurations
[self.view.layer addSublayer:instance];
[layer setNeedsDisplay]; //required
or the drawing will not happen.
But when I use buit-in CALayer subclasses (CATextLayer, CAShapeLayer etc.), the call to setNeedsDisplay: is not needed after adding them to a super layer. Can I make my subclass behaves like those classes?
You can call displayIfNeeded on yourself within the init method, however better is to subclass needsDisplayForKey: and return YES for the #"transform" key, which would redraw when the bounds changes (i.e. when first added to the view).
In my program I have started doing all initialization of objects in the init method without setting a frame and then in layoutSubviews I set the frames for these objects to make sure that they are properly set.
Firstly is this proper practice to initialize all objects in the init function without a set frame and then in layoutSubviews set each of their frames. The reason for my concern is that it is called quite often.
So I have a UIView subclass where I call these methods in the layoutSubviews
- (void)layoutSubviews
{
[super layoutSubviews];
[self.filterSwitcherView setFrame:self.viewFrame];
[self.drawingView setFrame:self.viewFrame];
[self.textView setFrame:self.textViewFrame];
[self.colorPicker setFrame:self.colorPickerFrame];
}
This currently works fine and all the objects are set correctly, but the problem is in my colorPicker class when the user touches the screen I adjust the frame of the colorPicker and by doing so this method gets called from the subview colorPicker and it readjusts a frame that it shouldn't since it has been modified in the subview. The subview causes the superviews layoutSubview to be called and this is not what I need.
My question is, is there a way to stop this behavior from happening or should I not use layoutSubviews to set frames because I was told this is a better way of making views programmatically?
Off the top of my head, there's two ways to fix this. You can either move this code to where the view is initialized, either in init, initWithFrame:, or initWithCoder:, depending on which you're using. It's good practice to make a separate method to initialize everything for your view, and call it from all the init methods to make sure it's always initialized correctly no matter how you instantiate the view.
Alternatively, if you want to keep your code in layoutSubviews, in your #interface add a boolean to flag that the frames were already set
#interface MyView : UIView
#property (nonatomic, assign) BOOL framesAreSet;
#end
Then when you set your frames, check if you already did
- (void)layoutSubviews
{
[super layoutSubviews];
if (!_framesAreSet)
{
[self.filterSwitcherView setFrame:self.viewFrame];
[self.drawingView setFrame:self.viewFrame];
[self.textView setFrame:self.textViewFrame];
[self.colorPicker setFrame:self.colorPickerFrame];
_framesAreSet = YES;
}
}
Your issue is likely that your colorPicker class is handling the touch methods to adjust its own frame. Instead, you should handle the touch methods in colorPicker's superview class, and have that superview class adjust colorPicker's frame in response to the touches.
Also, I would recommend doing all UI initialization in initWithFrame:, not init. The reason is because calling init on UIView ends up calling initWithFrame:.
I'm not sure how to proceed. I want to animate a slight graphics changes. I need this to happen in a loop every 2 seconds.
I have some drawing code in a class within a drawrect method. I pass in a style parameter with a custom method initWithFrame.
Within my animation animateWithDuration block, I thought I could just removeFromSuperview then addSubview different styled instances.
I know this sounds like the wrong approach.
However, I wasn't sure how else to do this.
Can anyone give me some options / point me at an example ?
To your original question about whether you should remove and re-add the view, typically you wouldn't do that (as that's a little inefficient). I usually have a public property in my custom view that (a) the view controller can set; and that (b) is used by the drawRect method. I then implement a custom setter that not only saves the ivar backing the property, but calls setNeedsDisplay, too (which will trigger drawRect to be called for you).
You haven't described what is changing in the view, but let me give you a random example. Let's say you have a drawRect that draws a circle of a given radius. Then my UIView subclass might have a property for this CGFloat, e.g.:
#property (nonatomic) CGFloat radius;
And then I might implement a custom setter which will not only update the ivar, like usual, but also triggers the re-calling of drawRect:
- (void) setRadius:(CGFloat)newRadius
{
_radius = newRadius; //note, I set the ivar backing the property; do not try self.radius = ..., because that will call this same method with infinite recursion
[self setNeedsDisplay];
}
Then, when my view controller wants to update the view, it can simply do the following:
customView.radius = 27.5;
Or, if I want to animate the changing of this every two seconds, fading to the new position, every two seconds, I'd wrap the changing of this property in a transitionWithView block:
[UIView transitionWithView:customView
duration:0.3
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
customView.radius = 27.5;
}
completion:nil];
Now, clearly this is a random example where this property was a CGFloat, but you can do this with any custom property that your drawRect needs to reflect the new rendering of the screen.
I found the code listed below from https://github.com/DuncanMC/iOS-CAAnimation-group-demo. This particular method allows the user to stop a UIView while it is "in-flight" in a core animation sequence using a gesture recognizer. When the view is tapped, the animation stops. As shown this code will only work on animated view. I have many animated views and I need interaction with any of the views. I think I must set up an array of views (or layers) and cycle through them. Is this correct? How could I do this? Thanks!
/*
This method gets called from a tap gesture recognizer installed on the view myContainerView.
We get the coordinates of the tap from the gesture recognizer and use it to hit-test
myContainerView.layer.presentationLayer to see if the user tapped on the moving image view's
(presentation) layer. The presentation layer's properties are updated as the animation runs, so hit-testing
the presentation layer lets you do tap and/or collision tests on the "in flight" animation.
*/
- (IBAction)testViewTapped:(id)sender
{
CALayer *tappedLayer;
id layerDelegate;
UITapGestureRecognizer *theTapper = (UITapGestureRecognizer *)sender;
CGPoint touchPoint = [theTapper locationInView: myContainerView];
if (animationInFlight)
{
tappedLayer = [myContainerView.layer.presentationLayer hitTest: touchPoint];
layerDelegate = [tappedLayer delegate];
if (((layerDelegate == imageOne && !doingMaskAnimation)) ||
(layerDelegate == waretoLogoLarge && doingMaskAnimation))
{
if (myContainerView.layer.speed == 0)
[self resumeLayer: myContainerView.layer];
else
{
[self pauseLayer: myContainerView.layer];
//Also kill all the pending label changes that we set up using performSelector:withObject:afterDelay
[NSObject cancelPreviousPerformRequestsWithTarget: animationStepLabel];
}
}
}
}
LOL. That demo project is mine. The code is written to find the layer that was tapped, and then use the fact that for a layer that backs a UIView, the layer's delegate is the view itself.
At the point in the code where it finds the layerDelegate, you should make sure it isKindOfClass UIView, then use whatever method is appropriate to match your view with the views you've animated. You could use an IBOutletCollection to keep track of the views that you are animating, or manually create a mutable array and add view objects to it, use view tags, or whatever makes sense for your application.
I saw this solution while researching for CALayers. I was looking for a way to implement custom drawings inside a UIView with multiple sublayers. I named my sublayers like this:
layer1.name = #"Back";
layer2.name = #"Middle";
layer3.name = #"Front";
And I created custom methods to be implemented by this layers
-(void)drawBackLayer:(CALayer *)layer inContext:(CGContextRef)ctx
-(void)drawMiddleLayer:(CALayer *)layer inContext:(CGContextRef)ctx
-(void)drawFrontLayer:(CALayer *)layer inContext:(CGContextRef)ctx
The problem is these methods are not implemented, instead the drawLayer:inContext:, which is being used by the view's root layer, is implemented four times. This means that the custom layers implement this method instead of the custom methods. Can anyone explain to me why?
NOTE: the solution I am referring in the link is the code provided by Dave Lee.
The layer used by the UIView must be a custom CALayer that implements those messages. The way you get a UIView subclass to use a particular layer is via the class method "+(Class)layerClass".
So:
- subclass a UIView (or subclass of it like UIImageView, etc)
- implement the layerClass method, and return [MyCALayer class];
Now that view will use YOUR CALayer subclass.