I'm trying to animate a view's alpha property after I've sent it some values via a ReactiveSwift Signal Producer.
The following is how I am currently doing it sans animations.
// Somewhere in View Model (for all code below)
let shouldShowShutter = MutableProperty<Bool>(false)
// In my View
self.shutterButton.reactive.alpha <~ self.viewModel.shouldShowShutter.map({ (show) -> CGFloat in
return show ? 1:0.0
})
I can inelegantly animate the view via:
self.viewModel.shouldShowShutter.producer.startWithSignal { (observer, disposable) in
observer.map({ (show) -> CGFloat in
return show ? 1:0.0
}).observeValues({ [unowned self] (alpha) in
UIView.animate(withDuration: 0.5, animations: {
self.shutterButton.alpha = alpha
})
})
}
But I should be able to animate the views with ReactiveAnimation:
self.shutterButton.reactive.alpha <~ self.viewModel.shouldShowShutter.map({ (show) -> CGFloat in
return show ? 1:0.0
}).animateEach(duration: 0.2).join(.Concat)
The question: ReactiveCocoaLayout and ReactiveAnimation both don't seem to work anymore as of the time I am asking the question, at least not with the Swift 3 or ReactiveCocoa 5 as they depend a lot on the legacy RACSignals.
Is there a more elegant way to animate signal streams to ReactiveCocoa components with ReactiveCocoa 5?
I didn't know about ReactiveAnimation before, but I really liked the approach.
So I updated it for RAC 5.0 / Swift 3.2, have a look at my fork. I'll also create a pull request, lets have a look if we can get the project back to life.
I've included a small iOS Demo project to demonstrate the use:
label.reactive.center <~ SignalProducer.timer(interval: .seconds(1), on: QueueScheduler.main)
.map { _ in return self.randomPoint() }
// In order to demonstrate different flatten strategies,
// the animation duration is larger than the animation interval,
// thus a new animation begins before the running animation is finished
.animateEach(duration: 1.5, curve: .EaseInOut)
// With the .concat flatten strategy, each animations are concatenated.
// Each animation finisheds, before the next one starts.
// This also means, that animations are queued
.flatten(.concat)
// With the .merge flatten strategy, each animation is performed immediately
// If an animation is currently running, it is cancelled and the next animation starts from the current animation state
//.flatten(.merge)
Related
I have successfully created a custom animation in a drawing in a UIView by using a repeating Timer to increment the alpha of the drawing every few milliseconds.
Now, I want to achieve an easing out animation (deceleration) with my drawing. I would like to do this by firing a new timer with a longer interval every time the Timer is called, so that the alpha increments slower, resulting in deceleration.
I know that there is an easeOut animation from CAMediaTiming, but I would like to know if there is any built in function to get the decelerating numbers. For example, if I pass in a constant of 10, every time I call the function I can get decelerating numbers like 15, 18, 20, 21, 21.5, etc.
There is a built-in way to use various sorts of animation curves in standard UIView animations.
var yourView = UIView() // Or whatever your view might be
var yourView.alpha = 0 // Initial alpha value
// Call the animation method. Note options (which can be an array) which applies an ease-out curve to the animation
UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveEaseOut, animations: {
yourView.alpha = 1.0 // Final value of alpha
}, completion: nil)
Other animation curves are available, such as .curveEaseIn and .curveEaseInOut as well as other options. You can read more about animation options here.
You can use the completion handler closure to chain animations too.
If you are insisting on implementing your own means of animation using a timer (which I would not recommend), post some code so that I can consider a way to apply a curve to the changing values.
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.
THE PROBLEM
Running the same code on iOS10 and iOS11 my UIViewPropertyAnimator has a different behaviour just after changing of his .isReversed property.
Everything is ok on iOS10. The animation problem happens on iOS11
CONDITIONS
It's true for any animations, not only for a particular one, and it is verifiable both by watching the animation and within the code.
It happens both on simulators and real devices.
DETAILS
Once created a UIViewPropertyAnimator with his animation, during its running I just call .pauseAnimation() and change the .isReversed property to true. After that I resume the animation calling:
continueAnimation(withTimingParameters parameters: UITimingCurveProvider?, durationFactor: CGFloat)
at this point on iOS10 the animation smoothly changes his verse, on iOS11 it stops immediately and reverses itself with a bit frames lag.
If in code I check the value of .fractionComplete (called on my UIViewPropertyAnimator object it gives me back the completion of the animation in his percent value, starting from 0.0 and ending at 1.0)
just after .continueAnimation(...
- On iOS 10 it remains for a few moments like if the animation is continuing and only after some fractions of time jumps to his complementary.
- On iOS 11 it jumps suddenly on his complementary
On the documentation there are non updates related to this, just a couple of new properties for the UIViewPropertyAnimator but not used because I'm targeting iOS10
Could be a bug or I'm missing something!?
Little update: just tested, same behaviour on iOS 11.0.1 and on iOS 11.1 beta1
As linked in the comment, this happens only with a non-linear curve!
I have been fighting this for quite a while as well, but then I noticed the scrubsLinearly property that was added to UIViewPropertyAnimator in iOS 11:
Defaults to true. Provides the ability for an animator to pause and scrub either linearly or using the animator’s current timing.
Note that the default of this property is true, which seems to cause a conflict with using a non-linear animation curve. That might also explain why the issue is not present when using a linear timing function.
Setting scrubsLinearly to false, the animator seems to work as expected:
let animator = UIViewPropertyAnimator(duration: 0.25, curve: .easeOut) {
...
}
animator.scrubsLinearly = false
On iOS 11, fractionComplete will be reversed (that is, 1 - originalFractionComplete) after you reverse animation by animator.isReversed = true.
Spring animation that have less than 0.1s duration will complete instantly.
So you may originally want the reversed animation runs 90% of the entire animation duration, but on iOS 11, the reversed animation actually runs 10% duration because isReversed changed, and that 10% duration is less than 0.1s, so the animation will be completed instantly and looks like no animation happened.
How to fix?
For iOS 10 backward compatibility, copy the fractionComplete value before you reverse animation and use it for continueAnimation.
e.g.
let fraction = animator.fractionComplete
animator.isReversed = true
animator.continueAnimation(...*fraction*...)
I have tried many solutions, but no one didn't work for me. I wrote my solution and everything is fine now. My solution:
take an image of the screen and show it
finish animation
start new animation for old state
pause the animation and set progress (1 - original progress)
remove screen image and continue animation
switch pan.state {
...
case .ended, .cancelled, .failed:
let velocity = pan.velocity(in: view)
let reversed: Bool
if abs(velocity.y) < 200 {
reversed = progress < 0.5
} else {
switch state {
case .shown:
reversed = velocity.y < 0
case .minimized:
reversed = velocity.y > 0
}
}
if reversed {
let overlayView = UIScreen.main.snapshotView(afterScreenUpdates: false)
view.addSubview(overlayView)
animator?.stopAnimation(false)
animator?.finishAnimation(at: .end)
startAnimation(state: state.opposite)
animator?.pauseAnimation()
animator?.fractionComplete = 1 - progress
overlayView.removeFromSuperview()
animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0.5)
} else {
animator?.continueAnimation(withTimingParameters: nil, durationFactor: 0)
}
And the animation curve option must be linear.
animator = UIViewPropertyAnimator(duration: 0.3, curve: .linear) {
startAnimation()
}
I have a superview A and a subview B. In A's drawRect(_:) I do some line drawing that depends on the position and size of subview B. In class A (but not in its drawRect(_:)) I also have an animation block that moves and scales B.
The problem is that the drawing in drawRect(_:) always happens before the animation takes place, either using the final position and size of the subview or using its initial position and size, but never using the intermediate values.
Case 1: drawing uses the final state
The image below is a screen shot of the simulator while the animation that scales the subview is in progress. Note how the lines are already drawn in their final state.
The code for the line drawing is in A's drawRect(_:). This is the code for the animation (it's in class A so self refers to the superview):
UIView.animateWithDuration(3.0, delay: 0.5,
options: UIViewAnimationOptions.CurveLinear,
animations: {
[unowned self] in
self.subviewWidthConstraint.constant = 0.75 * CGRectGetWidth(self.bounds)
self.layoutIfNeeded()
self.setNeedsDisplay() // this has no effect since self's bounds don't change
}) { (completed) -> Void in }
Case 2: drawing uses the initial state
Looking around for answers I found a suggestion to use self.layer.displayIfNeeded() in place of self.setNeedsDisplay() in the animation block, so I tried this as well
func animateFrame()
{
UIView.animateWithDuration(3.0, delay: 0.5,
options: UIViewAnimationOptions.CurveLinear,
animations: {
[unowned self] in
self.subviewWidthConstraint.constant = 0.75 * CGRectGetWidth(self.bounds)
self.layoutIfNeeded()
self.layer.displayIfNeeded() // the only difference is here
}) { (completed) -> Void in }
}
but it results in the following. Again, this is a screen shot while the animation that scales up the subview is in progress. Now, the lines are again drawn before the animation starts but using the subview's initial position and size.
I think I understand case 1. The entire animation block is queued for execution but its end result is already computed by the time the animation starts so it's no surprise that drawInRect(_:) is using the final state.
I don't understand case 2 but that's because I have little experience with dealing directly with the view layer.
Either way, the effect I'm looking to achieve is that the lines are drawn to the vertices of the subview while it's being moved and/or scaled. Any suggestions or pointers to documentation are much appreciated. Thanks!
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.