I've taken a look at the answers in the following StackOverflow question but none seem to work for me: on the execution of the completion block, instead of performing the animation again, the program spews out "complete" ad infinitum without animating the view at all.
How can I repeat animation (using UIViewPropertyAnimator) certain number of times?
This is my AnimatorFactory class:
class AnimatorFactory {
#discardableResult
static func rotateRepeat(view: UIView) -> UIViewPropertyAnimator {
let rotate = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 1.0, delay: 0.0, options: [.curveLinear], animations: {
view.transform = CGAffineTransform(rotationAngle: .pi)
}, completion: { _ in
print("complete")
self.rotateRepeat(view: view)
})
return rotate
}
}
It is called as you'd expect with AnimatorFactory.rotateRepeat(view: <someView>)
However, the problem as mentioned above occurs. What I'd expect is that the view would rotate repeatedly until some time that I decide to change or stop it; this is exactly the reason that I have chosen to use UIViewPropertyAnimator instead of UIView.animate(withDuration:animations).
What's the best way then to create interactive, repeatable UIView animations? Much appreciated.
Your code is working fine. The trouble is that your animation does nothing after the first time. You say:
view.transform = CGAffineTransform(rotationAngle: .pi)
The first time, we change the rotation from 0 to pi. That is a change, so there is animation. But after that we just keep saying “stay at pi” over and over. We are at pi and you say to stay there, so there is no change to animate.
What you want each animation to do is add pi, not be pi.
As #matt suggested, I was merely setting the rotation of the view to .pi over and over. So in the completion block I have now set the transform to .identity before kicking the animation off again.
class AnimatorFactory {
#discardableResult
static func rotateRepeat(view: UIView) -> UIViewPropertyAnimator {
let rotate = UIViewPropertyAnimator(duration: 1.0, curve: .linear)
rotate.addAnimations {
view.transform = CGAffineTransform(rotationAngle: .pi)
}
rotate.addCompletion{ _ in
view.transform = .identity
self.rotateRepeat(view: view)
}
rotate.startAnimation()
return rotate
}
}
I found this code online that implements the rubber band effect when panning a view:
#IBAction func viewDragged(sender: UIPanGestureRecognizer) {
let yTranslation = sender.translationInView(view).y
if (hasExceededVerticalLimit(topViewConstraint.constant)){
totalTranslation += yTranslation
topViewConstraint.constant = logConstraintValueForYPoisition(totalTranslation)
if(sender.state == UIGestureRecognizerState.Ended ){
animateViewBackToLimit()
}
} else {
topViewConstraint.constant += yTranslation
}
sender.setTranslation(CGPointZero, inView: view)
}
func logConstraintValueForYPoisition(yPosition : CGFloat) -> CGFloat {
return verticalLimit * (1 + log10(yPosition/verticalLimit))
}
The resulting effect is shown in the gif below:
However, I have trouble understanding how this code works, and reproducing this effect in my own projects. For instance, one of the things I do not understand is, when panning the green view upwards yTransition is going to be negative and negative numbers do not have logarithms (in the logConstraintValueForYPoisition(:) method). I would really appreciate it if someone could explain to me how this code works step by step.
The original post can be found here.
The log is not what you're thinking of. In fact, the snippet is incomplete. The repo can be found here.
The bouncing animation is here:
func animateViewBackToLimit() {
self.topViewConstraint.constant = self.verticalLimit
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 10, options: UIViewAnimationOptions.AllowUserInteraction, animations: { () -> Void in
self.view.layoutIfNeeded()
self.totalTranslation = -200
}, completion: nil)
}
The log portion is for moving the green rectangle up. Once you reach an upward threshold (hasExceededVerticalLimit(topViewConstraint.constant)) you want the rectangle to stop moving as quick as you don't want it to keep up with your finger, you do this by calling logConstraintValueForYPoisition.
Note that if you have a positive value x, log(x) < x.
Note: I’ve already checked the following stack overflow issues:
27907570, 32229252, 26118141, 31604300
All I am trying to do is fade animate in a view (by alpha) when called by an IBAction attached to a button. Then reverse when a button on the view is hit.
My wrinkle may be that I'm using a secondary view that is on the ViewDock in the storyboard View. The view is added to the subview at the time of viewDidLoad where the frame/bounds are set to the same as the superview (for a full layover)
The reason this is done as an overlay view since it is a tutorial indicator.
The result (like many others who've listed this problem) is that the view (and contained controls) simply appears instantly and disappears as instantly. No fade.
I have tried animationWithDuration with delay, with and without completion, with transition, and even started with the old UIView.beginAnimations.
Nothing is working. Suggestions warmly welcomed.
The code is about as straight forward as I can make it:
Edit: Expanded the code to everything relevant
Edit2: TL;DR Everything works with the exception of UIViewAnimateWithDuration which seems to ignore the block and duration and just run the code inline as an immediate UI change. Solving this gets the bounty
#IBOutlet var infoDetailView: UIView! // Connected to the view in the SceneDock
override func viewDidLoad() {
super.viewDidLoad()
// Cut other vDL code that isn't relevant
setupInfoView()
}
func setupInfoView() {
infoDetailView.alpha = 0.0
view.addSubview(infoDetailView)
updateInfoViewRect(infoDetailView.superview!.bounds.size)
}
func updateInfoViewRect(size:CGSize) {
let viewRect = CGRect(origin: CGPointZero, size: size)
infoDetailView.frame = viewRect
infoDetailView.bounds = viewRect
infoDetailView.layoutIfNeeded()
infoDetailView.setNeedsDisplay()
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
updateInfoViewRect(size)
}
func hideInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.0
},
completion:
{ (finished) in
return true
}
)
AFLog.exit(thisClass)
}
func showInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.75
},
completion:
{ (finished) in
return true
}
)
AFLog.exit(thisClass)
}
// MARK: - IBActions
#IBAction func openInfoView(sender: UIButton) {
showInfoView()
}
#IBAction func closeInfoView(sender: UIButton) {
hideInfoView()
}
Please note, I started with the following:
func showInfoView() {
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.infoDetailView.alpha = 0.75
})
}
func hideInfoView() {
UIView.animateWithDuration(2.0, animations: { () -> Void in
self.infoDetailView.alpha = 0.00
})
}
If you infoDetailView is under auto layout constraints you need to call layoutIfNeeded on the parent view inside animateWithDuration:
func showInfoView() {
self.view.layoutIfNeeded() // call it also here to finish pending layout operations
UIView.animate(withDuration: 2.0, animations: {
self.infoDetailView.alpha = 0.75
self.view.layoutIfNeeded()
})
}
Theoretically this should not be needed if you just change the .alpha value, but maybe this could be the problem in this case.
There are several strange things I can see,
first, remove:
infoDetailView.layoutIfNeeded()
infoDetailView.setNeedsDisplay()
Usually you don't need to call those methods manually unless you know exactly what you are doing.
Also, when you are changing the size:
infoDetailView.frame = viewRect
infoDetailView.bounds = viewRect
You never need to set both bounds and frame. Just set frame.
Also, you should probably make sure that the view actually doesn't ignore the frame by setting:
infoDetailView.translatesAutoresizingMaskIntoConstraints = true
Instead of resetting the frame, just set autoresize mask:
infoDetailView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
Resulting in:
override func viewDidLoad() {
super.viewDidLoad()
// Cut other vDL code that isn't relevant
setupInfoView()
}
func setupInfoView() {
infoDetailView.alpha = 0.0
infoDetailView.translatesAutoresizingMaskIntoConstraints = true
infoDetailView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
infoDetailView.frame = view.bounds
view.addSubview(infoDetailView)
}
func hideInfoView() {
...
}
I think this should actually help because immediate animations are often connected to size problems.
If the problem persists, you should check whether the infoDetailView in your animation is the same object as the infoDetailView you are adding to the controller.
For others looking to start an animation immediately when a view loads...
The animation won't work if you call UIView.animate(...) inside viewDidLoad. Instead it must be called from the viewDidAppear function.
override func viewDidAppear(_ animated: Bool) {
UIView.animate(withDuration: 3) {
self.otherView.frame.origin.x += 500
}
}
If the animation does not seem to execute then consider examining the state of each of your views, before you enter the animation block. For example, if the alpha is already set to 0.4 then the animation that adjusts your view alpha, will complete almost instantly, with no apparent effect.
Consider using a keyframe animation instead. This is what a shake animation in objective c looks like.
+(CAKeyframeAnimation*)shakeAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
animation.values = #[[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-10.0, 0.0, 0.0)],
[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(10.0, 0.0, 0.0)]];
animation.autoreverses = YES;
animation.repeatCount = 2;
animation.duration = 0.07;
return animation;
}
Here is a post that shows you how to adjust alpha with keyframes https://stackoverflow.com/a/18658081/1951992
Make sure infoDetailView's opaque is false.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIView_Class/#//apple_ref/occ/instp/UIView/opaque
This property provides a hint to the drawing system as to how it should treat the view. If set to true, the drawing system treats the view as fully opaque, which allows the drawing system to optimize some drawing operations and improve performance. If set to false, the drawing system composites the view normally with other content. The default value of this property is true.
Try Below code. Just play with alpha and duration time to perfect it.
Hide func
func hideInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.8
},
completion:
{ (finished) in
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.4
},
completion:
{ (finished) in
self.infoDetailView.alpha = 0.0
}
)
}
)
AFLog.exit(thisClass)
}
Show func
func showInfoView() {
AFLog.enter(thisClass)
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.3
},
completion:
{ (finished) in
UIView.animateWithDuration(
2.0,
animations:
{
self.infoDetailView.alpha = 0.7
},
completion:
{ (finished) in
self.infoDetailView.alpha = 1.0
}
)
}
)
AFLog.exit(thisClass)
}
I've replicated your code and it work well, it's all ok.
Probably you must control constraints, IBOutlet and IBActions connections. Try to isolate this code into a new project if it's necessary.
Update: my code
and my storyboard and project folder photo:
Every object (view and buttons) are with default settings.
I've commented all AFLog lines (probably it's only any more "verbose mode" to help you) , the rest of your code is ok and it do what do you aspected from it, if you press open button the view fade in, and when you tap close button the view fade out.
PS Not relevant but i'm using xCode 7.3 , a new swift 2.2 project.
Use this code:
Swift 2
UIView.animateWithDuration(0.3, animations: { () -> Void in
self.infoDetailView.alpha = 0.0
})
Swift 3, 4, 5
UIView.animate(withDuration: 0.3, animations: { () -> Void in
self.infoDetailView.alpha = 0.0
})
Have you tried changing your showInfoView() to something more like toggleInfoView?
func toggleInfoView() {
let alpha = CGFloat(infoDetailView.alpha == 0 ? 1 : 0)
infoDetailView.alpha = alpha //this is where the toggle happens
}
It says that if your view's alpha is 0, then change it to 1. Else, make it 0.
If you need that to happen in an animation, try
#IBAction func openInfoView(sender: UIButton) {
UIView.animate(withDuration: 2.0, animations: {
self.toggleInfoView() //fade in/out infoDetailView when animating
})
}
You'll still want to keep that infoDetailView.alpha = 0.0 where you have it, coming from the viewDidLoad.
For UILabel component try to changes layer's background color instead.
Try this (Tested on Swift 4):
UIView.animate(withDuration: 0.2, animations: {
self.dateLabel.layer.backgroundColor = UIColor.red.cgColor;
})
Had a similar issue with animation not being performed.
Changed the function call use perform(aSelector: Selector, with: Any?, afterDelay: TimeInterval) in the form of perform(#selector(functionThatDoesAnimationOfAlphaValue), with: nil, afterDelay: 0) and it worked. Even with a TimeInterval set to 0.
In case someone else comes here wondering for a solution.
i have found this code which is responsible for animating a UIView but unfortunately the code does not work and i can not figure the reason (maybe an older version of swift)
this is the code :
(this is helper function according to the creator)
func moveView(#view:UIView, toPoint destination:CGPoint, completion☹()->())?) {
//Always animate on main thread
dispatch_async(dispatch_get_main_queue(), { () Void in
//Use UIView animation API
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping:
0.6, initialSpringVelocity: 0.3, options:
UIViewAnimationOptions.AllowAnimatedContent, animations: { () ->
Void in
//do actual move
view.center = destination
}, completion: { (complete) -> Void in
//when animation completes, activate block if not nil
if complete {
if let c = completion {
c()
}
}
})
})
}
and this is the animation
//Create your face object (Just a UIImageView with a face as the image
var face = Face();
face.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
//find our trajectory points
var center = CGPointMake(self.view.frame.size.width/2, self.view.frame.size.height/2);
var left = CGPointMake(center.x *-0.3, center.y)
var right = CGPointMake(center.x *2.2, center.y)
//place our view off screen
face.center = right
self.view.addSubview(face)
//move to center
moveView(view: face, toPoint: center) { () -> () in
//Do your Pop
face.pop()
// Move to left
moveView(view: face, toPoint: left, completion: { () -> () in
}
}
and i quote from the creator of the code
General Steps: Create a new face on the right edge of the screen. Make
the face visible. Move the face to the middle of the screen. Pop the
face Start the process with the next face. Move the first face to the
left as soon as the new face gets to the middle.
Actual slide animation Once again, we will do the following here: Move
view off screen on the right Move to center Pop Move to left
To get the repeating effect, just call this method on a timer
and a summary :
UIView’s animation API is very powerful. Both the pop and movement
animations use depend on this API. If you’re stuck with trying to
create an animation, UIView animation block is usually a good place to
start.
NOTE : im a beginner in IOS development if anyone can please explain the code for me
Indeed this moveView method had a few issues, one being it was written for Swift 1 (but there were also some typos, faulty characters and useless operations).
Here's the fixed version:
func moveView(view view:UIView, toPoint destination: CGPoint, afterAnim: ()->()) {
//Always animate on main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
//Use UIView animation API
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping:
0.6, initialSpringVelocity: 0.3, options:
UIViewAnimationOptions.AllowAnimatedContent, animations: { () -> Void in
//do actual move
view.center = destination
}, completion: { (complete) -> Void in
//if and when animation completes, callback
if complete {
afterAnim()
}
})
})
}
You can use it like this now:
moveView(view: face, toPoint: center) {
//Do your Pop
face.pop()
// Move to left
moveView(view: face, toPoint: left) {
// Do stuff when the move is finished
}
}
Observe the differences between your version and mine to understand what was obsolete/wrong and how I fixed it. I'll help if you're stuck.
I've been having loads of trouble with this. I'm trying to get 4 buttons to come in from off screen. Initially it works, however, I have a separate button that changes the buttons image afterwards (think trivia game). I just want them to animate like below and be done with it. But they seem to be disappearing and displaying behaviour I cannot account for. Is it bad practice to animate buttons? Do I need to declare the buttons new center origin somehow?
override func viewDidLayoutSubviews() {
ButtonA.center = CGPointMake(ButtonA.center.x - 100, ButtonA.center.y)
ButtonB.center = CGPointMake(ButtonB.center.x - 100, ButtonB.center.y)
ButtonC.center = CGPointMake(ButtonC.center.x - 100, ButtonC.center.y)
ButtonD.center = CGPointMake(ButtonD.center.x - 100, ButtonD.center.y)
}
override func viewDidAppear(animated: Bool) {
UIView.animateWithDuration(0.2, animations: {
self.ButtonA.center = CGPointMake(self.ButtonA.center.x + 100, self.ButtonA.center.y)
})
UIView.animateWithDuration(0.4, animations: {
self.ButtonB.center = CGPointMake(self.ButtonB.center.x + 100, self.ButtonB.center.y)
})
UIView.animateWithDuration(0.6, animations: {
self.ButtonC.center = CGPointMake(self.ButtonC.center.x + 100, self.ButtonC.center.y)
})
UIView.animateWithDuration(0.8, animations: {
self.ButtonD.center = CGPointMake(self.ButtonD.center.x + 100, self.ButtonD.center.y)
})
}
It's also been really difficult to track down the cause of the issue in the debugger. Even with break points I can't get to the point where they disappear. It takes me to assembly code.