UIScrollView with custom layoutSubviews does not animate anymore - iphone

I have a subclass of UIScrollView that I'm using for images slideshow, with infinite scrolling and circular slideshow.
I used to animate the transition in this way: (Because I wanted the transition to be slower)
[UIView animateWithDuration:1.0
delay:0 options:(UIViewAnimationCurveEaseOut)
animations:^{
self.scrollView.contentOffset = newOffset;}
completion:NULL];
And it worked just fine.
Then I watched the lecture "Advanced Scrolling Techniques" from WWDC 2011, and they recommend to implement infinite scrolling by overriding layoutSubviews.
So I changed my implementation and override layoutSubviews
Once I did that the transition animation stopped working.
If I comment out my custom layoutSubviews - It's working again!
Why??
What can I do to make my own scrolling animation while overriding layoutSubviews?
Thanks!

OK, I think I found the problem, and a possible fix.
The key to understand the problem is understanding that when you call
[UIView animateWithDuration...]
All the properties that you change inside the animation block, are changed immediately. They do not change as the animation actually executing (visually).
So in my case, I'm animating the contentOffset to my target value.
This operation invokes layoutSubviews immediately.
In my implementation of layoutSubviews I'm checking to see if enough movement has been accumulated. If so - I'm rearranging my ImageViews, and set the contentOffset to its initial value. Once I do that there is nothing to animate anymore, because nothing has changed!
Animate contentOffset to X:
calls setContentOffset to X:
invokes layoutSubview
Sets the contentOffset back to Y! (after rearranging the views)
Nothing to animate at this point...
Possible Fix
I'm not sure if it is the right way to do it, but its working working.
So what we need to do is to ensure that no changes will be made in layoutSubviews while we are in the middle of animation block.
So I created a BOOL instance variable named isAnimating, and set it appropriately in the animation block:
self.isAnimating = YES;
[UIView animateWithDuration:self.transitionDuration
delay:0
options:(UIViewAnimationOptionCurveEaseOut)
animations:^{
self.contentOffset = newOffset;
}
completion:^(BOOL finished) {
self.isAnimating = NO;
[self setNeedsLayout];
}];
Now in layoutSubviews we need to take isAnimating into account:
-(void) layoutSubviews
{
[super layoutSubviews];
if (self.isAnimating == NO)
{
[self rearrangeIfNecessary];
}
}
And it's working again!

Related

Unusual animateWithDuration behaviour in viewDidLoad

I'm experiencing some unusual behaviour when performing an animated transform on a UIImageView. The code in the method below makes the image appear like it's rocking from side-to-side:
-(void) shakeAnimation
{
float degrees = 30; //the value in degrees
imgShake.autoresizingMask = UIViewAutoresizingNone;
[UIView animateWithDuration:0.20f delay:0 options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat) animations:^{
imgShake.transform = CGAffineTransformMakeRotation(degrees*M_PI/180);
} completion:^(BOOL finished) {
imgShake.transform = CGAffineTransformMakeRotation(1*M_PI/180);
NSLog(#"Shake finished");
}];
}
The problem comes with where i call the method. If i call the method in viewDidAppear the animation seems to work perfectly...but for other reasons i need to call it in viewDidLoad. When i call the method from viewDidLoad the animation functions but not at the speed specified by animateWithDuration. It's much slower, probably 0.70f. Is there something i could be missing here?
You never need to call it in viewDidLoad. That's simply wrong and it won't work. Move the code into viewDidAppear. If you have reasons to put it into viewDidLoad, fix the reasons!
EDIT: You never know when viewDidLoad is called - it can even be called multiple times for one controller. Usually the problem with slow animations is caused by a collision between two animations. For example, if your controller is animated to the screen by a UINavigationController, your animation will collide with the "push" animation and they will be slow. That's why you are supposed to use viewDidAppear because when this method is called you know that the controller is displayed and the "appear" animation has ended.
Call the method using some delay.
[self performSelector:#selector(shakeAnimation) withObject: afterDelay:2.0]

Detect Touch on an animated View

I'm animating a couple of views with animateWithDuration: and i'm simply unable to detect any touches on them.
I've tried simple touch handling (touchesEnded:) and a tapGestureRecognizer.
First i've animated them with CGAffineTransformTranslation but then i realized that this won't if i check the coordinate with touchesMoved: so i switched to animate the frame property. I quickly noticed, that the frame values are not really changing during the animation so i dropped the touchesEnded: idea. So i changed to a tapGestureRecognizer which doesn't work too.
I've enabled userInteraction on all views and i also added the option UIViewAnimationOptionAllowUserInteraction to the animation.
Here's the code of the animation and the stuff that happens before:
// init the main view
singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapOnItem:)]; // singleFingerTap is an ivar
// code somewhere:
// userItem is a subview of MainView
[userItem addGestureRecognizer:singleFingerTap];
[UIView animateWithDuration:animationTime
delay:0.0f
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction
animations:^{
userItem.frame = CGRectMake(-(self.bounds.size.width + userItem.frame.size.width), userItem.frame.origin.y, userItem.frame.size.width, userItem.frame.size.height);
}
completion:^(BOOL finished) {
if (finished) {
[unusedViews addObject:userItem];
[userItem removeFromSuperview];
[userItem removeGestureRecognizer: singleFingerTap];
}
}
];
And here's the gesture recognizer:
- (void) handleTapOnItem:(UITapGestureRecognizer *)recognizer{
NSLog(#"Touched");
}
So how can i actually get a touch from an animated View or what is the best solution?
Adding transparent uibutton to each view is not an option. :(
When an animation is applied to a view, the animated property changes to its end value right away. what you are actually seeing on screen is the presentation layer of your views layer.
I wrote a blog post about hit testing animating views/layers a while back that explain it all in more detail.
While anything is being moved using Core Animation touches on the object are halted until the object completes its movement. Interesting tid-bit, if you move the object and touch where its final location will be, the touch is picked up. This is because as soon as the animation is started the frame of the object is set to the ending point, it is not updated as it moves across the screen.
You might have to look into alternate technologies like Quartz or OpenGL ES to detect touches on your views as they are being moved.

completion block of uiview animate never called if view disappear

i noticed some strange behavior. When i start a animation and change the View (the view will not dismissed!), the completion handler never get called.
[UIView animateWithDuration:0.1f
delay:0.0f
options:UIViewAnimationCurveEaseOut
animations:^(void){
[myView setHidden:YES];
myLabel.alpha = 0.0f;
someOtherView.frame = CGRectMake(130, bubbleBigRect.origin.y, 61, 65);
[button setHidden:YES];
}
completion:^(BOOL finished){
NSLog(#"Complete %d",finished);
[imageVIew setImage:[UIImage imageNamed:#"myPng.png"]];
}];
}
is there any solution for this?
Don't know where to write it, but I get the same thing that you have, but in my case the completion block was sometimes called. It might be same thing.
I found out that if in the animation block has nothing was animate- for example, if you set alpha=0 to uiview that is alpha is already 0, or you set content offset to UIScrollView to the same content offset (like in my case), the completion block not called.
Put this in your animation block, and put everything you want to do on completion in YOUR_SELECTOR method. You can now do whatever you want with your view and still have a way of executing something on completion!
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(YOUR_SELECTOR)];
In my case the view which I was animating has its frame set to (0,-568,320,568) and after animation I forget to change the frame to its required position i.e. (0,0,320,568) so the completion block was not being called. Changing the frame of the animated view did the job for me. So I can say if for instance the view has nothing to show(as the frame was not set in visible region). The completion block may not called.
Move your call to [myView setHidden:YES] from the animations block to the completion block. I think that will probably help. You can still set the alpha of myView to 0 during the animation block (if you want it to fade out), or before the entire call to -[UIView animateWithDuration:delay:options:animations:completion:] if you want it to disappear right away.

UIView Animations stop working after dismiss Modal View

I just upgraded my iPhone 4 from iOS 4.2.1 to 4.3.2, and to XCode 4.0.2, and I am encountering some bizarre issues with uiview animations. When I first launch my app, code like this executes perfectly:
[UIView beginAnimations:#"fadeAlphaIn" context:nil];
[UIView setAnimationDuration:0.5f];
viewClue.alpha = 1.0f;
[UIView commitAnimations];
But then, after dismissing a presenting and then dismissing a modal view by the standard method:
[self presentModalViewController:more animated:YES];
and
[self dismissModalViewControllerAnimated:YES];
the first animation no longer works. Instead of fading in, for example, the viewClue view simply jumps from alpha = 0 to alpha = 1. Similarly, other animations altering other views' frame property just force the frame to jump from the initial to final value without animation. These animations worked fine before the modal view was presented and dismissed.
I understand that others have experienced animation issues with the upgrade to iOS 4.3.2, but the way the modal view disrupts animation seems very odd. Has anyone else experienced this problem? Any ideas as to a solution? I'm thinking of just adding the modal view as a subview and animation it as it hides and appears, but using the standard modal view method would be much preferred.
Thanks for your help,
James
EDIT: Some more code showing how the app's map is animated
-(void) viewMapfunc
{
AudioServicesPlaySystemSound(soundID);
if(mapvisible){
[UIView animateWithDuration:0.5
delay:0.1
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
map.frame = CGRectMake(0, 350, 320, 27);
mapscroll.frame = CGRectMake(0, 27, 320, 0);
}
completion:nil];
mapvisible = NO;
viewMapLabel.text = #"View Map";
}else {
[UIView animateWithDuration:0.5
delay:0.1
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
map.frame = CGRectMake(0, 50, 320, 300);
mapscroll.frame = CGRectMake(0, 27, 320, 300);
}
completion:nil];
mapvisible = YES;
viewMapLabel.text = #"Hide Map";
}
}
Try to check two things:
Do you commit all started animations? I got all kinds of strange effects after not committing one of them.
Do any animations take place in the same time? Especially with the same view.
Whether any animations take place right after changing properties. Something like:
-
view.alpha = 1;
[UIView beginAnimations:…];
view.alpha = 0;
[UIView commitAnimations:…];
In this example, view will not change it's alpha value from 1 to 0. It will change it instantly. To start an animation you have to extract animations block to another method and call it with performSelectorInMainThread:withObject:afterDelay:. Delay can be even 0.
I solved it by restarting my animation in my UIView subclass:
override func willMove(toWindow newWindow: UIWindow?) {
if newWindow != nil {
spinner.startSpinning() // Restart any animation here
}
}
In the end, I just removed all modal views and implemented them in other ways. For some reason, using modal views messed up animations. Makes no sense, but removing them fixed the problem. If anyone can enlighten me as to why this is going on, it might be nice for memory concerns...
I had the same issue. The root of my trouble was that my animation was being triggered by a notification, and I was adding an observer on each viewWillAppear, but forgot to remove in viewDidDisappear (remember that iOS 6 no longer calls viewDidUnload reliably).
Essentially, I was calling my animation function twice in quick succession, which was causing the visible irregularity. Hopefully this helps someone out down the line!
I've managed to solve this same issue in my own application.
I noticed while debugging that my UIImageViews which I was animating had different memory addresses before and after I pushed my modal view controller(s). At no other time did these UIImageViews switch their memory addresses.
I thought this might have been the root of the issue and it seems I was right.
My client's code had been allocating/initializing my View Controller's UIImageViews in
-viewDidAppear instead of in -viewDidLoad. Thus, every time I launched and dismissed a modal view controller my UIImageViews I was animating would get reinitialized.
Check for yourself if your map object's memory address is changing before and after you launch your modals, and if it is be sure to move your initialization logic to a more proper section of your code.
Hope this helps you!
Dexter
I was using UIView animateWithDuration: and I solved it by not using the completion block. This is code from a subclassed UIView. In the view controller's viewWillAppear: I set self.shouldAnimate to YES, and in the view controller's viewWillDisappear: I set self.shouldAnimate to NO.
-(void)continueRotate {
if (self.shouldAnimate) {
[self rotateRadarView:self.radarInner];
}
}
-(void)rotateRadarView:(UIView *)view {
[UIView animateWithDuration:0.6 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(continueRotate)];
[view setTransform:CGAffineTransformRotate(view.transform, M_PI_2)];
}completion:nil];
}

Possible to "UIScrollView.zoomToRect" and then do UIView animations?

I have a UIScrollView. I'd like to do some animation with it, but I want to add a neat little animation that basically zooms the UIScrollView back to where it started and then I can do some UIView animation. Is that possible?
I have tried to have a button trigger one method with the scrollView.zoomToRect, and this method calls another method in a background thread that does UIView animation. The issue is that whatever I do, the UIScrollView will just not zoom out to normal if I have an animation after it. I just want to make it zoom out followed by some animation, but I cannot.
It does not help to insert the scrollView.zoomToRect method in the animation block.
Anyone have an idea?
I am not sure if this qualifies as an answer to my own question, but in case anyone else was wondering, or in case anyone else have a better solution. I used the following code:
(Called when I hit the flip button)
- (void) flipCurrentViewZoomOut {
// If either view is zoomed in
if (view1.scrollView.zoomScale != 1 || view2.scrollView.zoomScale != 1 ) {
if (view1IsVisible == YES) {
[view1.scrollView zoomToRect:[UIScreen mainScreen].bounds animated:YES];
} else {
[bview2.scrollView zoomToRect:[UIScreen mainScreen].bounds animated:YES];
}
[UIView beginAnimations:nil context:nil];
// The duration is enough time for the zoom-out to happen before the second animation methods gets called (flipCurrentView).
[UIView setAnimationDuration:0.4];
[UIView setAnimationDelegate:self];
// When done, then do the actual flipping of the views (exchange subviews, etc.)
[UIView setAnimationDidStopSelector:#selector(flipCurrentView)];
// In order for the zoomToRect to run at all, I need to do some animation, so I basically just move the view 0.01 which is not noticable (and it's animating a flip right after anyway)
if (view1IsVisible == YES) {
view1.frame = CGRectMake(-0.01, 0, 320, 480);
} else {
view2.frame = CGRectMake(-0.01, 0, 320, 480);
}
[UIView commitAnimations];
} else {
// If either view hasn't been zoomed, the flip animation is called immediately
[self flipCurrentView];
}
}
An important thing to note is that in my flipCurrentView method (the second animation method that flips the views), I reset the frames for view1 and view2 to [UIScreen mainScreen].bounds (in this case that's the bounds I need). I have to do this, otherwise the animation code I pasted above won't run a second time, because the origin.x will then be -0.01 and it can't animate from -0.01 to -0.01, so it would have just skip that animation block.
Let me know if I am doing something completely wrong and there's a better way to do it. Always happy to learn :)