I'm creating a custom animatable property on a CAGradientLayer derived class that has to change other built in animatable properties on the CAGradientLayer base class and was wondering what the best approach is. Currently I'm updating the dependant properties in the display method:
#implementation CustomGradientLayer
#dynamic myCustomProperty;
+ (BOOL) needsDisplayForKey: (NSString*)aKey
{
BOOL needsDisplay = [aKey isEqualToString: #"myCustomProperty"];
if (!needsDisplay)
{
needsDisplay = [super needsDisplayForKey: aKey];
}
return needsDisplay;
}
- (void) display
{
CGFloat myCustomProperty = [self.presentationLayer myCustomProperty];
[CATransaction begin];
[CATransaction setDisableActions: YES];
// Update dependant properties on self
[CATransaction commit];
[super display];
}
Is it possible to safely update the dependant properties in a custom property setter instead without affecting the underlying CALayer magic?
I found the solution but for anyone interested you can use the following method without interfering with the CALayar stuff:
- (void) didChangeValueForKey: (NSString*)aKey (void) didChangeValueForKey: (NSString*)aKey
{
// Update dependent properties
}
Related
Is there a way to know if my custom implementation of setFrame: (or an other setter of an animatable property) is being called from an animation block i.e. it will be animated or just set directly?
Example:
- (void)setFrame:(CGRect)newFrame {
[super setFrame:newFrame];
BOOL willBeAnimated = ?????
if (willBeAnimated) {
// do something
} else {
// do something else
}
}
In the above setter willBeAnimated should be YES it is called like this:
- (void)someMethod {
[UIView animateWithDuration:0.2
animations:^{view.frame = someRect;}
completion:nil];
}
and NO in this case:
- (void)someMethod {
view.frame = someRect;
}
someMethod here is a private method inside UIKit that I can't access or change, so I have to somehow determine this from the "outside".
You should be able to check the animationKeys of the layer of your UIView subclass right after changing the frame to see if it is being animated.
- (void)setFrame:(CGRect)newFrame {
[super setFrame:newFrame];
BOOL willBeAnimated = [super.layer animationForKey:#"position"] ? YES : NO;
if (willBeAnimated) {
// do something
} else {
// do something else
}
}
You can also to check if there are any animations by using animationsKeys which in this case would just return position.
In addition, if you want to force a change to not be animated you can use performWithoutAnimation:
[UIView performWithoutAnimation:^{
[super setFrame:newFrame];
}];
EDIT
Another tidbit I found by testing is that you can actually stop the animation if it is already in progress and instead making the change instantly by removing the animation from the layer and then using the above method instead.
- (void)setFrame:(CGRect)newFrame {
[super setFrame:newFrame];
BOOL willBeAnimated = [super.layer animationForKey:#"position"] ? YES : NO;
BOOL shouldBeAnimated = // decide if you want to cancel the animation
if (willBeAnimated && !shouldBeAnimated) {
[super removeAnimationForKey:#"position"];
[UIView performWithoutAnimation:^{
[super setFrame:newFrame];
}];
} else {
// do something else
}
}
I have a custom CALayer with an animatable property called progress. The basic code is:
#implementation AnimatedPieChartLayer
#dynamic progress;
+ (BOOL)needsDisplayForKey:(NSString *)key {
return [key isEqualToString: #"progress"] || [super needsDisplayForKey: key];
}
- (id <CAAction>)actionForKey:(NSString *)key {
if ([self presentationLayer] != nil) {
if ([key isEqualToString: #"progress"]) {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath: key];
[anim setFromValue: [[self presentationLayer] valueForKey: key]];
[anim setDuration: 0.75f];
return anim;
}
}
return [super actionForKey: key];
}
- (void)drawInContext:(CGContextRef)context {
// do stuff
}
#end
In the view controller I do this, to try and get my animation to start from the beginning every time:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear: animated];
[pieChart setProgress: 0.0];
}
This all works perfectly. I put the layer on a view, the view in a view controller, and it all works as expected.
The complication is that my view controller is inside a UIScrollView. This presents a situation where the user could swipe away from my view controller before the CALayer animation completes. If they swipe back quickly, the CALayer animation does something weird. It reverses. It's like the animation is trying to go back to the beginning. Once it gets there it starts over, and animates the way it is supposed to.
So the question is, how can I prevent this. I want the animation to start from the beginning every time the view is displayed – regardless of what happened last time.
Any suggestions?
UPDATE
Here's a visual example of the problem:
Working:
Failing:
In the setup you have there, you have an implicit animation for the key "progress" so that whenever the progress property of the layer changes, it animates. This works both when the value increases and when it decreases (as seen in your second image).
To restore the layer to the default 0 progress state without an animation, you can wrap the property change in a CATransaction where all actions are disabled. This will disable the implicit animation so that you can start over from 0 progress.
[CATransaction begin];
[CATransaction setDisableActions:YES]; // all animations are disabled ...
[pieChart setProgress: 0.0];
[CATransaction commit]; // ... until this line
Probably too simple a suggestion to merit an 'answer,' but I would suggest canceling the animation. Looks like you could rewrite this code pretty easily so that the progress updates were checking some complete flag and then when the view get hidden, kill it. This thread has some good ideas on it.
Is there anyway to detect a hidden state change (or other change) in a sub view in a UIView (not UIViewController). Would like to detect this async somehow.
There are reasons for my madness.
You can use KVO (key value observing) to detect a change to the value of the property hidden.
Add your observer (self in this example) in the following way:
UIView* viewToObserve = [self getViewToObserve]; // implement getViewToObserve
[viewToObserve addObserver:self forKeyPath:#"hidden" options:0 context:NULL];
Now add the following method to your observer class:
- (void) observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
UIView* viewToObserve = [self getViewToObserve];
if (object == viewToObserve)
{
if ([keyPath isEqualToString:#"hidden"])
{
// react to state change
}
}
}
The observer method will be invoked whenever the hiddenproperty changes its value. If I am not mistaken, the method will be invoked synchronously in the context of the thread that makes the change to the property. If you need asynchronous notification you can add that yourself, for instance by using one of the NSObject methods performSelector:withObject:afterDelay: or performSelector:onThread:withObject:waitUntilDone:.
BTW: You don't need the checks in the observer method, obviously, if you only observe a single object and/or property. I left the checks in for illustration purposes. I also recommend reading Apple's documentation on KVO and KVC (key value coding) to understand what's going on here.
The runtime happily continues notifying your observer even if the observer is deallocated - resulting in an application crash! So don't forget to remove the observer before it is de-allocated, at the latest this should happen in the observer's dealloc:
- (void) dealloc
{
UIView* viewToObserve = [self getViewToObserve];
[viewToObserve removeObserver:self forKeyPath:#"hidden"];
[super dealloc];
}
You can override the property in the UIView subclass and do anything in didSet
class MyView: UIView {
override var isHidden: Bool {
didSet {
//do something
}
}
}
I want to implement a UIScrollView subclass to present some custom formatted content. I just set a model object property of the scroll view and it handles all the required layout and rendering to display the content.
This works fine, but now I'd like to include zooming. According to the documentation, to support zooming you have to set a delegate and implement the viewForZoomingInScrollView: method. I guess I could set the delegate to the scroll view itself and implement that method in the subclass. But doing that I would lose the ability to have an external delegate (like an encapsulating UIViewController) that can be notified about scroll events.
Assuming the documentation is right and there is absolutely no (documented) way to implement zooming without a delegate, how could I still retain the possibility of having a regular, unrelated delegate?
Building upon H2CO3's suggestion of saving a hidden pointer to the real delegate and forwarding all incoming messages to it, I came up with the following solution.
Declare a private delegate variable to store a reference to the "real" delegate that is passed in to the setDelegate: method:
#interface BFWaveScrollView ()
#property (nonatomic, weak) id<UIScrollViewDelegate> ownDelegate;
#end
Set the delegate to self to be notified about scrolling events. Use super, so the original setDelegate: implementation is called, and not our modified one.
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[super setDelegate:self];
}
return self;
}
Override setDelegate: to save a reference to the "real" delegate.
- (void)setDelegate:(id<UIScrollViewDelegate>)delegate {
_ownDelegate = delegate;
}
When the UIScrollView tries to call a method of its delegate, it will first check to see if the delegate respondsToSelector:. We have to forward this to the real delegate if the selector is part of the UIScrollViewDelegate protocol (Don't forget to #import <objc/runtime.h>).
- (BOOL)selectorIsScrollViewDelegateMethod:(SEL)selector {
Protocol *protocol = objc_getProtocol("UIScrollViewDelegate");
struct objc_method_description description = protocol_getMethodDescription(
protocol, selector, NO, YES);
return (description.name != NULL);
}
- (BOOL)respondsToSelector:(SEL)selector {
if ([self selectorIsScrollViewDelegateMethod:selector]) {
return [_ownDelegate respondsToSelector:selector] ||
[super respondsToSelector:selector];
}
return [super respondsToSelector:selector];
}
Finally, forward all delegate methods to the real delegate that are not implemented in the subclass:
- (id)forwardingTargetForSelector:(SEL)selector {
if ([self selectorIsScrollViewDelegateMethod:selector]) {
return _ownDelegate;
}
return [super forwardingTargetForSelector:selector];
}
Don't forget to manually forward those delegate methods that are implemented by the subclass.
I'd abuse the fact that I'm being a subclass (on purpose :P). So you can hack it. Really bad, and I should feel bad for proposing this solution.
#interface MyHackishScrollView: UIScrollView {
id <UIScrollViewDelegate> ownDelegate;
}
#end
#implementation MyHackishScrollView
- (void)setDelegate:(id <UIScrollViewDelegate>)newDel
{
ownDelegate = newDel;
[super setDelegate:self];
}
- (UIView *)viewForScrollingInScrollView:(UIScrollView *)sv
{
return whateverYouWant;
}
// and then implement all the delegate methods
// something like this:
- (void)scrollViewDidScroll:(UIScrollView *)sv
{
[ownDelegate scrollViewDidScroll:self];
}
// etc.
#end
Maybe this is easier to read and understand a couple of weeks later :)
(sample code for intercepting locationManager:didUpdateLocations: in a subclass)
Other than that the same handling for setting self as delegate to the superclass and intercepting setDelegate in order to save the user's delegate to mDelegate.
EDIT:
-(BOOL)respondsToSelector:(SEL)selector {
if (sel_isEqual(selector, #selector(locationManager:didUpdateLocations:)))
return true;
return [mDelegate respondsToSelector:selector];
}
- (id)forwardingTargetForSelector:(SEL)selector {
if (sel_isEqual(selector, #selector(locationManager:didUpdateLocations:)))
return self;
return mDelegate;
}
I have some custom appearance properties in my view class (a descendant of UIView). I want to customize the view appearance according to these properties, but I can’t do that inside the initializer, since the values set using [[MyClass appearance] setFoo:…] aren’t in effect at that point:
#interface View : UIView
#property(strong) UIColor *someColor UI_APPEARANCE_SELECTOR;
#end
#implementation View
#synthesize someColor;
// Somewhere in other code before the initializer is called:
// [[View appearance] setSomeColor:[UIColor blackColor]];
- (id) initWithFrame: (CGRect) frame
{
self = [super initWithFrame:frame];
NSLog(#"%#", someColor); // nil
return self;
}
#end
They are already set in layoutSubviews, but that’s not a good point to perform the view customizations, since some customizations may trigger layoutSubviews again, leading to an endless loop.
So, what’s a good point to perform the customizations? Or is there a way to trigger the code that applies the appearance values?
One possible workaround is to grab the value directly from the proxy:
- (id) initWithFrame: (CGRect) frame
{
self = [super initWithFrame:frame];
NSLog(#"%#", [[View appearance] someColor); // not nil
return self;
}
Of course this kills the option to vary the appearance according to the view container and is generally ugly. Second option I found is to perform the customizations in the setter:
- (void) setSomeColor: (UIColor*) newColor
{
someColor = newColor;
// do whatever is needed
}
Still I’d rather have some hook that gets called after the appearance properties are set.
Why not wait until
- (void)willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
if (newSuperview) {
... code here ...
}
}
if it's giving you trouble?
I believe UIAppearance properties are applied to a view when it is being added into a view hierarchy. So presumably you could access the set properties in UIView didMoveToSuperview.
Caveat: I am using Swift 2, so not sure about earlier versions of Swift / Objective-C. But I have found that didMoveToSuperview() will not work. The properties are available in layoutSubviews(), but that's not a great place to do anything like this (since it can be called more than once). The best place to access these properties in the lifeCycle of the view I have found is didMoveToWindow().
I would have thought that viewDidLoad would be best if it's a one-time thing. Otherwise, viewWillAppear.
EDIT:
If you want to do it in the view, and not it's controller then I would create a custom init for the view along the lines of:
-(id) initWithFrame:(CGRect) frame andAppearanceColor:(UIColor)theColor;
thereby passing the colour into the view at creation time.