iOS, trigger UIViewAnimation once at slider value n? - ios

I have a set of sliders, I'm using Value Changed to feed a number to a % indicator. I'm also using this value to check if the slider is below a certain point. If it is, I want to run a UIViewAnimation (which I am, it's all working fine). However, the animation gets called constantly if the slider is moved below the threshold, meaning the items being animated go from point a to point b then back again over and over. So, can I trigger the animation once only at the threshold point?
This is how I get my value in pixels:
_sizeSliderRange = _sizeSlider.frame.size.width - _sizeSlider.currentThumbImage.size.width;
_sizeSliderOrigin = _sizeSlider.frame.origin.x + (_sizeSlider.currentThumbImage.size.width / 4.0);
_sizeSliderValueToPixels = (_sizeSlider.value * _sizeSliderRange) + _sizeSliderOrigin;
And I use a conditional inside the linked Value Changed IBAction function to checkt he value and do the work:
if (_sizeThumbX < 85) { //if within 60px of the left margin we animate the label to sit float left
[UIView transitionWithView:_sizeLabel duration:0.25f options:UIViewAnimationCurveEaseInOut animations:^(void) { etc etc
Thanks.

Like #Luis said, just use a BOOL property like this:
if (_sizeThumbX < 85) { //if within 60px of the left margin we animate the label to sit float left
if (!self.passedBelowThreshold) {
[UIView transitionWithView:_sizeLabel duration:0.25f options:UIViewAnimationCurveEaseInOut animations:^(void) { /* ... */ }
}
}
self.passedBelowThreshold = _sizeThumbX < 85;

Your code is working according to your logic that is everytime the slider value is changed and it is below 85 the animation will be invoked .You can have animation triggered only once in the follwing way :-
1>YOu can keep an absolute value at which animation occurs.Something like _sizeThumbX == 85
2>or you canhave counter of how many times the value changes . In a different function count and store the value of slider change.If the slider value lies in the 85 range do not increase the counter value and in your animation part check the counter flag and slider's current position if slider is still in below 85 range do not invoke animation if counter value is already 1 that is animation is already fired else invoke and increase the animation counter.
3>I am not ware of you conditions as you have not mentioned clearly but ithink you want to invoke the animation again if your slider goes beyond the range and comes back again , in that case make the count to zero (slider crosses the specified range) .

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.

(Swift, EarlGrey) move slider to the middle

I'm writing some tests and I need to go to the middle of a slider. I would like to get minimumValue and maximumValue of the slider, like on a picture below:
These values may change so every time I need to get them in my code. Later, I just want to get average of these two values and move the slider to the middle using a method
performAction:grey_moveSliderToValue(valueToMove)
How can I get minumumValue and maximumValue in my code? Is this the best approach or is there any better to move playhead to the middle of the slider?
You can use EarlGrey's GREYActionBlock to create a new action that slides to half
// Create a new action that slides to half.
GREYActionBlock *slideToHalfAction = [GREYActionBlock actionWithName:#"slideToHalf"
constraints:grey_kindOfClass([UISlider class])
performBlock:^BOOL(id element, NSError *__strong *errorOrNil) {
UISlider *slider = element;
float minValue = slider.minimumValue;
float maxValue = slider.maximumValue;
float halfValue = (minValue + maxValue) / 2;
return [grey_moveSliderToValue(halfValue) perform:element error:errorOrNil];
}];
// Use the new action on the slider.
[[EarlGrey selectElementWithMatcher:grey_accessibilityID(#"slider4")] performAction:slideToHalfAction];

Animate a custom property's value?

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.

Observing change in frame of a UIView during animation

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