Animate a custom property's value? - ios

I can easily animate something like the position or size of a UIView. But how can I animate a "custom" variable (such as experiencePoints) to achieve interpolation of values that are not associated with a UIView.
// The variable being animated
CGFloat experiencePoints = 0;
// Pseudo-code
[experiencePoints animateTo:200 duration:2 timingFunction:someTimingFunction];
With this code, if I accessed experiencePoints while it was animating, I would get a value between 0 and 200 based on how long the animation has been going.
Bonus question: Is it possible to use a CAAnimation to do what I want?

You can go with Presentation layer with CADisplayLink and by adding observer on animation status you can increment the variable upto your final one. Observer will observe current status of Animation which will eventually provide you a way to get current value of that VAR.

Ended up using Facebook's POP animation library, which allows the animation of any property on an object. I recommend it!
Here is a quote from the readme, explaining how to do it:
The framework provides many common layer and view animatable properties out of box. You can animate a custom property by creating a new instance of the class. In this example, we declare a custom volume property:
prop = [POPAnimatableProperty propertyWithName:#"com.foo.radio.volume" initializer:^(POPMutableAnimatableProperty *prop) {
// read value
prop.readBlock = ^(id obj, CGFloat values[]) {
values[0] = [obj volume];
};
// write value
prop.writeBlock = ^(id obj, const CGFloat values[]) {
[obj setVolume:values[0]];
};
// dynamics threshold
prop.threshold = 0.01;
}];
anim.property = prop;

"Animating" something is simply moving a value from a starting position to an ending position over time. To "animate" a float from one value to another over a duration you could just run a timer and incrementally change it. Or possibly you could store the begin and end time of the "animation" and whenever the value is accessed, compare those to the actual time to come up with a value.

Related

Observe UIView frame while animating [duplicate]

I want to observe changes to the x coordinate of my UIView's origin while it is being animated using animateWithDuration:delay:options:animations:completion:. I want to track changes in the x coordinate during this animation at a granular level because I want to make a change in interaction to another view that the view being animated may make contact with. I want to make that change at the exact point of contact. I want to understand the best way to do something like this at a higher level:
-- Should I use animateWithDuration:... in the completion call back at the point of contact? In other words, The first animation runs until it hits that x coordinate, and the rest of the animation takes place in the completion callback?
-- Should I use NSNotification observers and observe changes to the frame property? How accurate / granular is this? Can I track every change to x? Should I do this in a separate thread?
Any other suggestions would be welcome. I'm looking for a abest practice.
Use CADisplayLink since it is specifically built for this purpose. In the documentation, it says:
Once the display link is associated with a run loop, the selector on the target is called when the screen’s contents need to be updated.
For me I had a bar that fills up, and as it passed a certain mark, I had to change the colors of the view above that mark.
This is what I did:
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.frameInterval = 3
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
UIView.animateWithDuration(1.2, delay: 0.0, options: [.CurveEaseInOut], animations: {
self.viewGaugeGraph.frame.size.width = self.graphWidth
self.imageViewGraphCoin.center.x = self.graphWidth
}, completion: { (_) in
displayLink.invalidate()
})
func animationDidUpdate(displayLink: CADisplayLink) {
let presentationLayer = self.viewGaugeGraph.layer.presentationLayer() as! CALayer
let newWidth = presentationLayer.bounds.width
switch newWidth {
case 0 ..< width * 0.3:
break
case width * 0.3 ..< width * 0.6:
// Color first mark
break
case width * 0.6 ..< width * 0.9:
// Color second mark
break
case width * 0.9 ... width:
// Color third mark
break
default:
fatalError("Invalid value observed. \(newWidth) cannot be bigger than \(width).")
}
}
In the example, I set the frameInterval property to 3 since I didn't have to rigorously update. Default is 1 and it means it will fire for every frame, but it will take a toll on performance.
create a NSTimer with some delay and run particular selector after each time lapse. In that method check the frame of animating view and compare it with your colliding view.
And make sure you use presentationLayer frame because if you access view.frame while animating, it gives the destination frame which is constant through out the animation.
CGRect animationViewFrame= [[animationView.layer presentationLayer] frame];
If you don't want to create timer, write a selector which calls itself after some delay.Have delay around .01 seconds.
CLARIFICATION->
Lets say you have a view which you are animating its position from (0,0) to (100,100) with duration of 5secs. Assume you implemented KVO to the frame of this view
When you call the animateWithDuration block, then the position of the view changes directly to (100,100) which is final value even though the view moves with intermediate position values.
So, your KVO will be fired one time at the instant of start of animation.
Because, layers have layer Tree and Presentation Tree. While layer tree just stores destination values while presentation Layer stores intermediate values.
When you access view.frame it will always gives the value of frame in layer tree not the intermediate frames it takes.
So, you had to use presentation Layer frame to get intermediate frames.
Hope this helps.
UIDynamics and collision behaviours would be worth investigating here. You can set a delegate which is called when a collision occurs.
See the collision behaviour documentation for more details.

How to make a particle image bigger with a scale effect with SceneKit?

I am looking for the same effect as we have in SpriteKit for the emitter particles, the scale effect that can make a particle image bigger or smaller depending on the time. (a simple red circle for example, getting bigger and disappearing after 1 second.) I cannot find the same scale option as we can find in SpriteKit. The image can be bigger and stay bigger, but it would not change depending on the time then.
Would someone know a good way to do this?
Thanks
EDIT:
None of these attempts worked, would you know why?
func addParticleSceneKit(){
println("add")
var fire = SCNParticleSystem(named: "circle1.scnp", inDirectory: "art.scnassets/Particles")
fire.particleSize = 5
emitter.addParticleSystem(fire) //emitter is a SCNNode
/*
let bigger = SCNAction.runBlock { (node) -> Void in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
SCNTransaction.setAnimationDuration(1)
fire.propertyControllers = [SCNParticlePropertySize : 10.0]
})
}
emitter.runAction(bigger)
*/
//SCNTransaction.begin()
//SCNTransaction.setAnimationDuration(1)
//fire.propertyControllers = [SCNParticlePropertySize : 10.0]
//SCNTransaction.commit()
}
SCNParticleSystem has properties like
// Specifies the initial size of the particle. Animatable.
#property(nonatomic) CGFloat particleSize;
// Specifies the initial size variation of the particle. Animatable.
#property(nonatomic) CGFloat particleSizeVariation;
if you need more control you can also provide your own particle property controller for the key "SCNParticlePropertySize". For example to specify the how the size should be animated over the particle life duration.
see
// Property controllers.
// The keys for this directionary are listed in the "Particle Properties Name" section.
// The values are instances of SCNParticlePropertyController
#property(nonatomic, copy) NSDictionary *propertyControllers;
Wow. Just wow. You've thrown a lot of code at the wall just to see what sticks, but have you looked in the documentation?
The method description for SCNParticlePropertyController's initializer includes a code example that does almost exactly what you're asking for — it animates particle sizes. Reproduced here:
// 1. Create and configure an animation object.
let animation = CAKeyframeAnimation()
animation.values = [ 0.1, 1.0, 3.0, 0.5 ]
// 2. Create a property controller from the animation object.
let sizeController = SCNParticlePropertyController(animation: animation)
// 3. Assign the controller to a particle system, associating it with a particle property.
particleSystem.propertyControllers = [ SCNParticlePropertySize: sizeController ]
If you only need a from size and a to size instead of keyframes, you can use a CABasicAnimation in step 1.

UIDynamicItem update transform manually

I know that external change to center, bounds and transform will be ignored after UIDynamicItems init.
But I need to manually change the transform of UIView that in UIDynamicAnimator system.
Every time I change the transform, it will be covered at once.
So any idea? Thanks.
Any time you change one of the animated properties, you need to call [dynamicAnimator updateItemUsingCurrentState:item] to let the dynamic animator know you did it. It'll update it's internal representation to match the current state.
EDIT: I see from your code below that you're trying to modify the scale. UIDynamicAnimator only supports rotation and position, not scale (or any other type of affine transform). It unfortunately takes over transform in order to implement just rotation. I consider this a bug in UIDynamicAnimator (but then I find much of the implementation of UIKit Dynamics to classify as "bugs").
What you can do is modify your bounds (before calling updateItem...) and redraw yourself. If you need the performance of an affine transform, you have a few options:
Move your actual drawing logic into a CALayer or subview and modify its scale (updating your bounds to match if you need collision behaviors to still work).
Instead of attaching your view to the behavior, attach a proxy object (just implement <UIDyanamicItem> on an NSObject) that passes the transform changes to you. You can then combine the requested transform with your own transform.
You can also use the .action property of the UIDynamicBehavior to set your desired transform at every tick of the animation.
UIAttachmentBehavior *attachment = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:item.center];
attachment.damping = 0.8f;
attachment.frequency = 0.8f;
attachment.action = ^{
CGAffineTransform currentTransform = item.transform;
item.transform = CGAffineTransformScale(currentTransform, 1.2, 1.2)
};
You would need to add logic within the action block to determine when the scale should be changed, and by how much, otherwise your view will always be at 120%.
Another way to fix this (I think we should call it bug) is to override the transform property of the UIView used. Something like this:
override var transform: CGAffineTransform {
set (value) {
}
get {
return super.transform
}
}
var ownTransform: CGAffineTransform. {
didSet {
super.transform = ownTransform
}
}
It's a kind of a hack, but it works fine, if you don't use rotation in UIKitDynamics.

Get current CAAnimation transform value

I want to access get the value of the transform scale at a point in time. Here is the animation creation :
CABasicAnimation *grow = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
grow.toValue = #1.5f;
grow.duration = 1;
grow.autoreverses = YES;
grow.repeatCount = HUGE_VALF;
[view.layer addAnimation:grow forKey:#"growAnimation"];
I'd like to get, for example when a user presses a button, the current size of the view.
Logging the frame or the bounds always returns constant values. Any help would be much appreciated !
In Core Animation if an animation is "in flight" on a layer, the layer has a second layer property known as presentationLayer. That layer contains the values of the in-flight animation.
Edited
(With a nod to Mohammed, who took the time to provide an alternate answer for Swift)
Use code like this:
Objective-C
CGFloat currentScale = [[layer.presentationLayer valueForKeyPath: #"transform.scale"] floatValue];
Swift:
let currentScale = layer.presentation()?.value(forKeyPath: "transform.scale") ?? 0.0
Note that the Swift code above will return 0 if the layer does not have a presentation layer. That's a fail case that you should program for. It might be better to leave it an optional and check for a nil return.
Edit #2:
Note that as Kentzo pointed out in their comment, reading a value from an in-flight animation will give you a snapshot at an instant in time. The new value of the parameter you read (transform.scale in this example) will start to deviate from the reading you get. You should either pause the animation, or use the value you read quickly and get another value each time you need it.
The CALayer documentation describes presentationLayer quite clearly:
The layer object returned by this method provides a close approximation of the layer that is currently being displayed onscreen. While an animation is in progress, you can retrieve this object and use it to get the current values for those animations.
swift version of #Duncan C answer will be:
let currentValue = someView.layer.presentation()?.value(forKeyPath: "transform.scale")
Swift 5: The nicest answer, in a function from Hacking With Swift
func scale(from transform: CGAffineTransform) -> Double {
return sqrt(Double(transform.a * transform.a + transform.c * transform.c))
}

How to enforce synchronization between UISlider value and thumb position?

I'm using a custom UISlider. I save the value after user input using slider.value. After a while, I need to restore the position of the slide to its saved value using [slider setValue:savedValue].
But I noticed some discrepancies between the position at save time and the position after restoration.
After a while I found out that if the user moves the thumb very quickly, it goes further than the actual value (Some kind of momentum/inertia effect). How can I make sure that one value corresponds to one thumb position?
Assuming you're catching UIControlEventTouchUpInside to dictate 'finished', use UIControlEventEditingDidEnd instead.
Failing that, you could key-value observe value, at each change cancelling future saves and, if tracking is false, scheduling a new for 0.5 seconds from now. E.g.
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(storeValue) object:nil];
if(!slider.tracking)
{
[self performSelector:#selector(storeValue) withObject:nil afterDelay:0.5];
}
(or explicitly store whether a save is scheduled if you find any performance problem with that, I guess, but don't overcomplicate it unless you have to)
Are you treating the value as an integer?
Because it (the UISlider object) stores a float, treating it as an integer can cause this behaviour..
Perhaps re-set it in the action where you process the change, I've found this necessary before once or twice..
Eg (this hooked up to the sliders (value changed) controlEvent....
-(IBAction) sliderMoved:(id)sender{
UISlider *sld = (UISlider *)sender;
CGFloat val = sld.value;
val += 0.49; /// rounder...
int result = (int) val;
[sld setValue:(CGFLoat)result animated:YES];
///// etc process entry...
}
This kind of treatment turns the slider into a multi-position "switch", it snaps to whole number values.. You could be doing it with fabsf(...) function if you want to remain in the float territory too...

Resources