How to know if view is being animated? - ios

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
}
}

Related

Why are completion handlers of [UIView animate] called in a wrong order?

In my project there's a ViewController which contains a few subviews(e.g. buttons).
It shows/hides those buttons, always with animation.
It has an interface like this:
#interface MyViewController: UIViewController
- (void)setMyButtonsVisible:(BOOL)visible;
#end
And an implementation looks like this:
- (void)setMyButtonsVisible:(BOOL)visible
{
if( visible )
{
// "show"
_btn1.hidden = NO;
_btn2.hidden = NO;
[UIView animateWithDuration:0.2 animations:^{
_btn1.alpha = 1.0;
_btn2.alpha = 1.0;
}];
}
else
{
// "hide"
[UIView animateWithDuration:0.2 animations:^{
_btn1.alpha = 0.0;
_btn2.alpha = 0.0;
} completion:^(BOOL finished) {
_btn1.hidden = YES;
_btn2.hidden = YES;
}];
}
}
When [_myVC setMyButtonsVisible:NO] is called, and then after some time ( > 0.2s) [_myVC setMyButtonsVisible:YES] is called, everything is OK.
However, it setMyButtonsVisible:YES is called immediately after ( < 0.2s) setMyButtonsVisible:NO, the animations overlap and setMyButtonsVisible:NO callback is called the last.
I've tried to change "hide" duration to 0.1, it doesn't help - after "hide(0.1)"+"show(0.2)" calls, "hide" callback is called after the "show" callback and my buttons are not visible.
I've added a quick-fix by caching the visible param and checking in "hide" completion handler if the state should be !visible.
My questions are:
Why the first animation completion handler is called the last if animations overlap?
What are better approahes to discard a previous "overlapping" animation?
Check the finished flag on completion:
if (finished) {
_btn1.hidden = YES;
_btn2.hidden = YES;
}

How to automatically change/transit from one view to another, without pushing a button or any action

How to automatically change/transit from one view to another, without pushing a button or any action, just after some time, say 2,3 seconds in Xcode/Objective-C, how would the code look like, and where to write it?
The basic solution might look like this:
- (void)viewDidLoad
{
[super viewDidLoad];
// create and position the subviews, if necessary
self.subview1 = ...
self.subview2 = ...
self.view addSubview:self.subview1];
self.view addSubview:self.subview2];
}
- (void)viewWillAppear
{
[super viewWillAppear];
[self showFirstView];
}
- (void)showFirstView
{
self.subview1.hidden = NO;
self.subview2.hidden = YES;
[self performSelector:#selector(showSecondViewView) withObject:nil afterDelay:5];
}
- (void)showSecondViewView
{
self.subview1.hidden = YES;
self.subview2.hidden = NO;
}

Recursive method with block and stop arguments

I've written a category on UIView that allows me to walk the view hierarchy:
UIView+Capture.h
typedef void(^MSViewInspectionBlock)(UIView *view, BOOL *stop);
#interface UIView (Capture)
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block;
#end
UIView+Capture.m
#implementation UIView (Capture)
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block
{
BOOL stop = NO;
[self inspectViewHeirarchy:block stop:stop];
}
#pragma - Private
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block stop:(BOOL)stop
{
if (!block || stop) {
return;
}
block(self, &stop);
for (UIView *view in self.subviews) {
[view inspectViewHeirarchy:block stop:stop];
if (stop) {
break;
}
}
}
#end
Which you can use like so:
[[[UIApplication sharedApplication] keyWindow] inspectViewHeirarchy:^(UIView *view, BOOL *stop) {
if ([view isMemberOfClass:[UIScrollView class]]) {
NSLog(#"Found scroll view!");
*stop = YES;
}
}];
Everything works fine, except setting stop to YES. This appears to have absolutely no effect whatsoever. Ideally, I'd like this to halt the recursion, so when I've found the view I want to take some action on I don't have to continue to traverse the rest of the view hierarchy.
I'm pretty dense when it comes to using blocks, so it may be something completely obvious. Any help will be greatly appreciated.
The way you're using a block is exactly the same as using a C function. So there's nothing special you really need to know about blocks. Your code should work but note the difference between passing stop as a BOOL * to your block and to create a new local when you recurse.
It looks like you're expecting calls down to inspectViewHierarchy:stop: to affect the outer stop variable. That won't happen unless you pass it as a reference. So I think what you want is:
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block stop:(BOOL *)stop
...and appropriate other changes.
I assume you want to return all the way out from the top-level inspectViewHierarchy when the user sets stop to YES.
(Incidentally, you spelled “hierarchy” wrong and you should use a prefix on methods you add to standard classes.)
#implementation UIView (Capture)
- (void)micpringle_visitSubviewsRecursivelyWithBlock:(MSViewInspectionBlock)block
{
BOOL stop = NO;
[self inspectViewHierarchy:block stop:&stop];
}
#pragma - Private
- (void)micpringle_visitSubviewsRecursivelyWithBlock:(MSViewInspectionBlock)block stop:(BOOL *)stop
{
block(self, stop);
if (*stop)
return;
for (UIView *view in self.subviews) {
[view micpringle_visitSubviewsRecursivelyWithBlock:block stop:stop];
if (*stop)
break;
}
}
#end
- (BOOL) inspectViewHeirarchy:(MSViewInspectionBlock)block
{
BOOL stop = NO;
block(self, &stop);
if (stop)
return YES;
for (UIView *view in self.subviews) {
if ([view inspectViewHeirarchy:block])
return YES;
}
return NO;
}
Try this:
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block
{
__block BOOL stop = NO;
[self inspectViewHeirarchy:block stop:stop];
}
Blocks, by nature, copy the variables and context in which they are declared.
Even though you are passing the boolean as a reference, it's possible that it's using a copy of the context and not the true stop.
This is just a wild guess but, inside inspectViewHierarchy:stop: do something like:
- (void)inspectViewHeirarchy:(MSViewInspectionBlock)block stop:(BOOL)stop
{
if (!block || stop) {
return;
}
// Add these changes
__block BOOL blockStop = stop;
block(self, &blockStop);
for (UIView *view in self.subviews) {
[view inspectViewHeirarchy:block stop:stop];
if (stop) {
break;
}
}
}
This may be a long shot and I'm not 100% sure it will work without having your project, but it's worth a shot.
Also, refactor your method so "heirarchy" is actually spelled "hierarchy" :] It's good for reusability and for keeping a good code base ;)
wouldn't you want to check the status of 'stop' directly after you invoke the block? It doesn't help to invoke it after you call inspectViewHierarchy:stop: because you are passing a copy of 'stop' to that method instead of the reference.

iOS prevent rotations until after animation completes

I'm creating an iOS app and in one method I have a for loop that does a series of animations that last around 2 seconds.
My problem is that if the user rotates the device while the animations are still in progress, it messes up the formatting for the new orientation (everything works just the way it should if the rotation happens after the animation is complete).
So I was wondering if there is a way to delay rotations
You could have a BOOL instance variable that you update based on whether your animation is complete or not. Then override the shouldAutorotate method and return that BOOL. Might look something like this:
#implementation YourViewController {
BOOL _shouldAutoRotate;
}
-(BOOL)shouldAutorotate {
[super shouldAutorotate];
return _shouldAutoRotate;
}
-(void)yourAnimationMethod {
_shouldAutoRotate = NO;
[UIView animateWithDuration:2.0f animations:^{
//your animations
} completion:^(BOOL finished) {
if(finished) {
_shouldAutoRotate = YES;
}
}];
}

How to animate transition from one state to another for UIControl/UIButton? [duplicate]

This question already has an answer here:
Fading out an UIButton when touched
(1 answer)
Closed 9 years ago.
I have a UIButton that changes image on highlight. When transitioning from UIControlStateHighlight to UIControlStateNormal, I want the highlighted image to slowly fade back into the normal image. Is there an easy way to do this?
I ended up subclassing UIButton. Here's the implementation file code. I took some app-specific stuff out, so I haven't tested this exact code, but it should be fine:
#import "SlowFadeButton.h"
#interface SlowFadeButton ()
#property(strong, nonatomic)UIImageView *glowOverlayImgView; // Used to overlay glowing animal image and fade out
#end
#implementation SlowFadeButton
-(id)initWithFrame:(CGRect)theFrame mainImg:(UIImage*)theMainImg highlightImg:(UIImage*)theHighlightImg
{
if((self = [SlowFadeButton buttonWithType:UIButtonTypeCustom])) {
self.frame = theFrame;
if(!theMainImg) {
NSLog(#"Problem loading the main image\n");
}
else if(!theHighlightImg) {
NSLog(#"Problem loading the highlight image\n");
}
[self setImage:theMainImg forState:UIControlStateNormal];
self.glowOverlayImgView = [[UIImageView alloc] initWithImage:theHighlightImg];
self.glowOverlayImgView.frame = self.imageView.frame;
self.glowOverlayImgView.bounds = self.imageView.bounds;
self.adjustsImageWhenHighlighted = NO;
}
return self;
}
-(void)setHighlighted:(BOOL)highlighted
{
// Check if button is going from not highlighted to highlighted
if(![self isHighlighted] && highlighted) {
self.glowOverlayImgView.alpha = 1;
[self addSubview:self.glowOverlayImgView];
}
// Check if button is going from highlighted to not highlighted
else if([self isHighlighted] && !highlighted) {
[UIView animateWithDuration:1.0f
animations:^{
self.glowOverlayImgView.alpha = 0;
}
completion:NULL];
}
[super setHighlighted:highlighted];
}
-(void)setGlowOverlayImgView:(UIImageView *)glowOverlayImgView
{
if(glowOverlayImgView != _glowOverlayImgView) {
_glowOverlayImgView = glowOverlayImgView;
}
self.glowOverlayImgView.alpha = 0;
}
#end
You could also just pull the highlighted image from [self imageForState:UIControlStateHighlighted] and use that, it should work the same. The main things are to make sure adjustsImageWhenHighlighted = NO, and then overriding the setHighlighted: method.

Resources