I'm working on code for an expandable tray view that uses UIDynamicAnimator to achieve a nice expand/contract animation.
To achieve a realistic acceleration I use UIGravityBehavior to make my tray fall, until the "tab" of the tray hits the bottom of the screen.
This works well, but even though all items in the scene have stopped moving, UIDynamicAnimatorDelegate dynamicAnimatorDidPause: is never called. This means that the animator continues using CPU cycles to animate the scene ( the delegate is set, and fires for UIDynamicAnimatorDelegate dynamicAnimatorDidPause: ).
I tried removing the UIGravityBehavior from the scene, which did indeed cause the animator to stop in the end. I can't time the removal of the gravity behavior right though, since I need to remove it from the scene once everything has stopped moving.
I understand that gravity is a constant force, but I still assumed it would stop the animator once everything has 0 velocity and 0 acceleration.
Is this last assumption false?
Anyone having similar problems?
You are correct that the animator should pause once everything comes to rest.
Check what items are attached to your gravity behavior, and make sure that there aren't other items still falling. For example, it is easy to accidentally create the following bug:
Add a view to gravity and collision
Remove view from superview and from collision
Fail to remove view from gravity
In this situation, the "ghost item" will fall forever.
Another possible problem (though less likely given your description) is if your items are attached to other behaviors that are causing infinite but small "bounce." I would check the full list of behaviors on your animator (remember to check child behaviors, too). In particular I'd be interested in any UIDynamicItemBehavior that adds elasticity.
EDIT:
You may also want to go the other way. Start with a very basic dynamics system and add components from yours until you can reproduce the problem. For instance, the following does converge quite quickly (logging "pause"):
#interface PTLViewController () <UIDynamicAnimatorDelegate>
#property (nonatomic, strong) UIDynamicAnimator *animator;
#end
#implementation PTLViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100,100,100,100)];
view.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:view];
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
self.animator.delegate = self;
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:#[view]];
collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:collisionBehavior];
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:#[view]];
[self.animator addBehavior:gravityBehavior];
}
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator {
NSLog(#"pause");
}
#end
To your question about getting all item velocities, I don't know of an easy way to do that. Unfortunately, UIDynamicAnimator doesn't directly know all of its items. This is indirectly because UIDyanamicBehavior doesn't include an items property. If this bothers you as much as it does me, consider duping radar://15054405.
But there is a solution if you just want to know the current linear velocity of specific items. Just add a UIDynamicItemBehavior with a custom action to log it:
UIDynamicItemBehavior *dynamicItemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:#[view]];
__weak UIDynamicItemBehavior *weakBehavior = dynamicItemBehavior;
dynamicItemBehavior.action = ^{
NSLog(#"Velocity: %#", NSStringFromCGPoint([weakBehavior linearVelocityForItem:view]));
};
[self.animator addBehavior:dynamicItemBehavior];
I had a similar issue recently. In the end I used a UICollisionBehavior with boundaries instead of items (because otherwise the moving items were bumping the others...) and implement the delegate method collisionBehavior:beganContactForItem:withBoundaryIdentifier:atPoint: to know when I should remove the gravity
UICollisionBehavior *collide = [[[UICollisionBehavior alloc] initWithItems:borders] retain];
[collide addItem:movingItem];
[collide setCollisionMode:UICollisionBehaviorModeBoundaries];
[collide setTranslatesReferenceBoundsIntoBoundary:YES];
If you find a better solution, let me know :) !
My problem is the same. My animator never comes to rest so once started, my app consumes 3 to 4% CPU forever. My views all appear to stop moving within 1/2 second. So rather than figure out why I'm not reaching equilibrium, I just hit it with a hammer and kill the animator with a timer. I give it 2 seconds.
- (void)createAnimator {
if (_timer) {
[_timer invalidate];
}
if (_animator) {
[_animator removeAllBehaviors];
}
_animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
// create all behaviors
// kill the animator in 2 seconds
_timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:#selector(killAnimator:) userInfo:nil repeats:NO];
}
- (void)killAnimator:(NSTimer *)timer {
[_animator removeAllBehaviors];
_animator = nil;
_timer = nil;
}
Related
I have a second window I create to show a custom notification in my app:
#property (strong, nonatomic) UIWindow *window2;
And that's how I make it appear:
self.window2 = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, SCREEN_BOUNDS.size.width, 50)];
self.window2.backgroundColor = [UIColor blackColor];
self.window2.windowLevel = UIWindowLevelAlert;
self.window2.rootViewController = custonViewController;
[self.window2 makeKeyAndVisible];
The problem is that no touch event inside custonViewController works.
I tried everything, even subclass UIWindow and override the (void)sendEvent:(UIEvent *)event. But it doesn't work either.
How can I make my appdeledate to know the window2 has been touched/tapped?
Any ideas?
Nevermind. I found out what happened.
I was animating the UIWindow to appear and disappear automatically, in a sequence of animations after the completion from the previous one with delay set with some seconds.
It locked any touch events during the whole sequence of animations.
So i separated the animations and put it in async threads, so it would receive touches normally.
bye!
I am adding an animated UIImageView (an explosion) to a view but it seems to not be retained. Here is the implementation of the Explosion class, a subclass of UIImageView:
#implementation Explosion
#define EXPLOSION_DIMENSION 20
- (instancetype)initAtPoint:(CGPoint)point {
if (self = [super initWithFrame:CGRectMake(0, 0, EXPLOSION_DIMENSION, EXPLOSION_DIMENSION)]) {
self.center = point;
self.image = [UIImage imageNamed:#"explosion.png"];
}
return self;
}
- (void)boom {
self.alpha = 0;
[UIView animateWithDuration:0.5
delay:1.0
options: UIViewAnimationOptionCurveLinear
animations:^{
self.alpha = 1;
}completion:^(BOOL finished){
NSLog(#"here");
// [self removeFromSuperview];
}];
}
#end
When hitting a breakpoint on NSLog(#"here"), the debugger shows no self. Huh? Any reference to self in the completion code causes a crash so I can't do a [self removeFromSuperview] there.
Here is the code in the view where it is instantiated:
Explosion * explosion = [[Explosion alloc] initAtPoint:p];
[_explosionImageViews addObject:explosion];
[self addSubview:explosion];
[explosion boom];
NSLog(#"subviews: %d array: %d", self.subviews.count, _explosionImageViews.count);
The view contains 8 other subviews. The output looks like this:
subviews: 9 array: 1
subviews: 10 array: 2
subviews: 9 array: 3
Note that the number of subviews resets to 9. Nowhere do I remove the explosion from its superview, it just seems to magically go away (garbage collection?). Sometimes the subviews.count gets up to 23 before it magically resets to 9.
I put in the _explosionImageViews array just to retain the objects so they would not go out of scope even though it seems that being added to the subviews of the view should already be doing this.
The animation is supposed to stay around for 0.5 seconds but stays much longer. Then several of them all disappear at the same time. Again, garbage collection?
Any ideas what is going on?
You're right--your explosion object is not being retained.
To you retain it, use a strong property:
#property (strong, nonatomic) Explosion *explosion;
Then, instantiate it in your implementation, like so:
self.explosion = [[Explosion alloc] initAtPoint:p];
Of course, if your explosionImageViews are being retained, then follow that property, to see if it is being released somewhere.
I hope that helps!
I'm playing with UIKitDynamics, and it's really neat. Currently I have this code:
CGPoint center = [newSelectedLabel center];
[self.animator removeBehavior:self.snapBehavior];
self.snapBehavior = [[UISnapBehavior alloc] initWithItem:self.indicatorView snapToPoint:center];
self.snapBehavior.damping = 0.67;
[self.animator addBehavior:self.snapBehavior];
[self.delegate didChangeToIndex:sender.tag];
It's a simple snap behavior, and it works quite well. For various reasons, though, I want to know when the system goes to a 'rest' state - i.e. everything stops moving. I'm fine with either a property on the animator, or a delegate method.
How can I do this?
After some more poking around, it seems that the UIDynamicAnimatorDelegate does this - the - (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator method seems to be called when the system is at a rest state.
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self];
self.animator.delegate = self;
and
#pragma mark - UIDynamicAnimator Delegate
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator
{
NSLog(#"pause");
}
- (void)dynamicAnimatorWillResume:(UIDynamicAnimator *)animator
{
NSLog(#"resume");
}
seem to work - resume is logged when the animation starts, and pause is logged within a second of the animation stopped.
Additionally, the running property on the UIDynamicAnimator itself seems to mirror the calls to the delegate methods - it's 1 when willResume is called, and it's 0 when didPause is called.
The animator has a property "running" that tells you when the animator is at rest.
The views associated with an animator’s behaviors can change position or change transform only when the animator is running. For optimization purposes, iOS can pause and then restart an animator. Use this method if you need to check whether or not your views are currently subject to changes in position or transform.
Swift 4.2 :
//MARK:- UIDynamicAnimator Delegate
extension YourViewController: UIDynamicAnimatorDelegate {
func dynamicAnimatorDidPause(_ animator: UIDynamicAnimator) {
// pause
}
func dynamicAnimatorWillResume(_ animator: UIDynamicAnimator) {
// resume
}
}
I have a few custom UIView objects that all handle drawing like this:
- (void)drawRect:(CGRect)rect {
// ^ I init the layers 1 and 2
[self.layer insertSublayer:layer1 atIndex:0]; // 1 or more
[self.layer insertSublayer:layer2 atIndex:1];
}
They also have a - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; with nothing else inside but a NSLog.
I add them all inside my main ViewController like so:
- (void)viewDidLoad
{
[super viewDidLoad];
CustomView *myViewWith1Layer = [[CustomView alloc] initWithFrame:CGRectMake(20, 20, 440, 260)];
[self.view addSubview:myViewWith1Layer];
CustomViewLayered *myViewWith2Layer = [[CustomViewLayered alloc] initWithFrame:CGRectMake(40, 260, 200, -120)];
[self.view addSubview:myViewWith2Layers];
}
When I run my app, if I tap on a view that has only a single layer - I get my NSLog to show, and everything's fine. If, on the other hand, I tap on views with 1+ layers, the app crashes (objc_msgSend log shows up with "EXC_BAD_ACCES (code=1, address=..."). I guess this is somehow related with ARC, which I have enabled.
How do I add multiple layers to a view, without it being messed up by ARC?
I don't think that this is an ARC problem, but creating and inserting the layers in drawRect
is wrong. This should be done (only once) in the init method of the view, e.g. in initWithFrame.
In my case, the solution was definitely related to ARC.
When initialising my delegates, I immediately assigned them to the layer.delegate property, and ARC would remove the object from existence immediately after that.
So for each layer, I add a strong #property (strong, nonatomic) delegatesClass *delegatesName and initialise straight to the property. After that, I assign the layer.delegate = self.delegatesName.
This did solve the issue, although I'm not sure if it is the right way to do things.
I'm currently taking my first shaky steps in ios development. I'm trying to animate a free falling ball. I have a button connected to an IBAction, and and UIImageView containing an image of my ball.
Inside my action, I have a while loop that's timed using NSTimeInterval, and based on the time it takes it calculates a new position until the ball reaches 'the ground'(Yes, I realize this is probably the least optimal way to do it. But I'm having a hard time grasping the syntax (and therein my problem probably lays), the optimisation will have to come later). From what I can understand, NSTimeInterval returns the elapsed time in seconds, so even though it will increment incredibly small steps, it should work. But I may have a serious case of brain fart.
So far so good. But when I tap the button, the ball moves straight from it's starting point to it's finishing point without an animation.
-(IBAction)doshit:(id)sender{
int G = 10;
CGPoint center = [myImage center];
NSDate *startTime = [NSDate date];
NSTimeInterval T;
T = fabs([startTime timeIntervalSinceNow]);
while (center.y<480)
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:T];
center.y=center.y+((G*T*T)/2);
[myImage setCenter:center];
T = fabs([startTime timeIntervalSinceNow]);
[UIView commitAnimations];
}
}
I welcome all suggestions! =)
One way to do it is to use a CADisplayLink to provide the timing -- it is tied to the display refresh rate of 1/60 of a second. If you 're using auto layout (which is on by default now), it is better to animate a constraint, rather then set a frame. So, this example uses a button at the top of the screen, whose constraint to the top of the view is connected to the IBOutlet topCon.
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (weak,nonatomic) IBOutlet NSLayoutConstraint *topCon;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self performSelector:#selector(startDisplayLink) withObject:nil afterDelay:2];
}
- (void)startDisplayLink {
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink {
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink {
static BOOL first = YES;
static double startTime = 0;
if (first) {
startTime = displayLink.timestamp;
}
first = NO;
double T = (double)displayLink.timestamp - startTime;
self.topCon.constant += ((10 * T * T)/2);
if (self.topCon.constant > 420) {
[self stopDisplayLink];
}
}
As Carl notes, you cannot perform animations by repeatedly changing things in the middle of a method call. See What is the most robust way to force a UIView to redraw? for more discussion on that.
As you may suspect, not only is this non-optimal, it actively fights iOS. iOS has many easy-to-use techniques for performing smooth animations without resorting to timers of any kind. Here is one simple approach:
- (IBAction)dropAnimate:(id)sender {
[UIView animateWithDuration:3 animations:^{
self.circleView.center = CGPointMake(100, 300);
}];
}
In the animations block, you change the thing you want to change to the final value (myImage.center in your case). UIKit will sample the current value and the final value, and figure out a path to get you there in the time you requested. For a full, runnable example with a few other features (like chained animations), see the example code from iOS:PTL Chapter 9.
The above code will use the default timing function. Once you understand that, you can move on to customizing the timing function. See Animation Pacing in the Animation Types and Timing Programming Guide. Also How to create custom easing function with Core Animation? and Parametric acceleration curves in Core Animation. But I would get your head around simple, default animations first.