Frames changing suddenly instead of animating [iOS] - ios

I have a view with a scroll view in my app (a percentage calculator). I am trying to animate the view to fly out and then fly in again from the left albeit with different contents, on the press of a button.
Here's my code:
func next()
{
UIView.animateWithDuration(0.5, animations: {
self.mainView.frame = CGRectMake(-500, self.view.bounds.height/2-self.mainHeight/2, self.mainWidth, self.mainHeight)
self.mainView.center.x -= 600
}) { (success) in
self.mainView.center.x += 1200
}
UIView.animateWithDuration(0.5, animations: {
self.mainView.frame = CGRectMake(self.view.bounds.width/2-self.mainWidth/2, self.view.bounds.height/2-self.mainHeight/2, self.mainWidth, self.mainHeight)
}) { (success) in
print("Animation Successful!")
self.drawView()
}
}
I don't understand why, but the animation doesn't occur, the view disappears completely and I see "Animation Successful" in the logs.
To be clear, I have created the view programmatically.
Here's the code in viewDidLoad(); the view displays correctly when it's called:
mainHeight = self.view.bounds.height * 0.85
mainWidth = self.view.bounds.width * 0.85
let viewHeight = self.view.bounds.height
let viewWidth = self.view.bounds.width
mainView.frame = CGRectMake(viewWidth/2-mainWidth/2, viewHeight/2-mainHeight/2, mainWidth, mainHeight)
mainView.layer.cornerRadius = 8
mainView.layer.masksToBounds = true
mainView.backgroundColor = getColor(0, green: 179, blue: 164)
self.view.addSubview(mainView)
scrollView.frame = CGRectMake(0, 0, mainWidth, mainHeight)
scrollView.contentSize = CGSizeMake(mainWidth, 800)
self.mainView.addSubview(scrollView)
contentView.frame = CGRectMake(0, 0, mainWidth, 800);
contentView.backgroundColor = getColor(0, green: 179, blue: 164)
contentView.layer.masksToBounds = true
contentView.clipsToBounds = true
self.scrollView.addSubview(contentView)

Place self.layoutViewIfNeeded() before frame/center changes.
Additionally you may need to put your next animation within a completion handler of the next animation.
func next() {
UIView.animateWithDuration(0.5, animations: {
self.view.layoutIfNeeded() // add this
self.mainView.frame = CGRectMake(-500, self.view.bounds.height/2-self.mainHeight/2, self.mainWidth, self.mainHeight)
self.mainView.center.x -= 600
}) { (success) in
self.view.layoutIfNeeded() // add this
self.mainView.center.x += 1200
// your next animation within a completion handler of previous animation
UIView.animateWithDuration(0.5, animations: {
self.mainView.frame = CGRectMake(self.view.bounds.width/2-self.mainWidth/2, self.view.bounds.height/2-self.mainHeight/2, self.mainWidth, self.mainHeight)
}) { (success) in
print("Animation Successful!")
self.drawView()
}
}
}
I haven't tested it yet, perhaps some changes or lines ordering is needed.

In Swift 3, I had to set the frame and then call layoutIfNeeded().
For example, an animation with spring:
UIView.animate(withDuration: 0.3, delay: 0.1, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: {
self.subView.frame = CGRect(x: 0, y: 0, width: 500, height: 200)
self.view.layoutIfNeeded()
}) { (_) in
// Follow up animations...
}

Related

Swift UIKit - Sequential animation function

I have a func UIView.animate (2 imageViews, up/down, pulsating 4 and 2 times sequentially), which is forking fine.
Problem is that i need to run this func sequentially, all animations executed -> next lap of execution -> N lap of execution for my needs (it can be 5 or it can be 25 etc).
How can i do it?
func upDown() {
runButton.isEnabled = false
UIView.animate(
withDuration: 0.5,
delay: 0,
options: [.autoreverse, .repeat]) {
UIView.modifyAnimations(withRepeatCount: 4, autoreverses: true) {
self.arrowDownImageView.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
self.arrowDownImageView.tintColor = .green
self.downLabel.backgroundColor = .green
}
} completion: { _ in
self.arrowDownImageView.transform = .identity
self.downLabel.transform = .identity
self.downLabel.backgroundColor = .black
self.arrowDownImageView.tintColor = .black
UIView.animate(
withDuration: 0.5,
delay: 0,
options: [.autoreverse, .repeat]) {
UIView.modifyAnimations(withRepeatCount: 2, autoreverses: true) {
self.arrowUpImageView.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
self.arrowUpImageView.tintColor = .blue
self.upLabel.backgroundColor = .blue
}
} completion: { _ in
self.arrowUpImageView.transform = .identity
self.upLabel.transform = .identity
self.upLabel.backgroundColor = .black
self.arrowUpImageView.tintColor = .black
self.runButton.isEnabled = true
}
}
}
I tried to call my func upDown() through other UIView.animate (+ .modifyAnimations) func (executed 1 time only), call it with for-in-loop (also wrong, like i clicked X-times on a button). Should i rebuild it with some other method, not UIView.animate? In this case which one? Pods are forbidden in my case. I'm planning (need) to place my upDown() inside a new func to run it sequentially. Any other ideas? Any other methods? Thanks!
It sounds like you want your upDown function called multiple times. You can add a count parameter to upDown. Then in the final completion handler you can call upDown with the next smaller number. When the count gets to zero you can exit.
Here's your upDown method with only two lines added plus the new default parameter. Added the guard at the beginning and the recursive call at the very end in the last completion block.
func upDown(count: Int = 1) {
guard count > 0 else { return } // stop when we get to zero
runButton.isEnabled = false
UIView.animate(
withDuration: 0.5,
delay: 0,
options: [.autoreverse, .repeat]) {
UIView.modifyAnimations(withRepeatCount: 4, autoreverses: true) {
self.arrowDownImageView.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
self.arrowDownImageView.tintColor = .green
self.downLabel.backgroundColor = .green
}
} completion: { _ in
self.arrowDownImageView.transform = .identity
self.downLabel.transform = .identity
self.downLabel.backgroundColor = .black
self.arrowDownImageView.tintColor = .black
UIView.animate(
withDuration: 0.5,
delay: 0,
options: [.autoreverse, .repeat]) {
UIView.modifyAnimations(withRepeatCount: 2, autoreverses: true) {
self.arrowUpImageView.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
self.arrowUpImageView.tintColor = .blue
self.upLabel.backgroundColor = .blue
}
} completion: { _ in
self.arrowUpImageView.transform = .identity
self.upLabel.transform = .identity
self.upLabel.backgroundColor = .black
self.arrowUpImageView.tintColor = .black
self.runButton.isEnabled = true
upDown(count - 1) // Run it again
}
}
}
With those few changes you can call upDown() if you only want it to run once or you can call it as upDown(count: 5) if you want it to run 5 times (or whatever number you pass in).

UITableView "jump up" during custom presentation animation

I am trying to create custom transition but I have a problem with "jumping" tableView. As you can see, just before transition, tableView hide under navigationBar which is wrong. How can I fix it?
Animation code:
UIView.animate(withDuration: transitionDuration, delay: 0, options: animationOptions, animations: { [weak self] in
guard let self = self else { return }
fromVC.view.transform = CGAffineTransform(scaleX: 0.9, y: 0.855)
fromVC.view.layer.cornerRadius = self.cornerRadius
fromVC.view.clipsToBounds = true
self.containerView.frame.origin.y -= self.containerHeight
self.topView.frame.origin.y -= self.containerHeight
self.backdropView.alpha = self.backdropAlpha
}, completion: { _ in
transitionContext.completeTransition(true)
})
and how it looks right now:

Play animation wait 2 seconds and repeat

I'm using UIView.animate to animate a simple UIView.
Starting conditions: width = height = 30, color = blue (I set this in the Storyboard)
I want to increase the size to w=50, h=50 and make it white, while it has an appropriate cornerRadius. Then I reset the view and do the same animation again.
Here is my code:
class ViewController: UIViewController {
#IBOutlet weak var swipeRightView: UIView!
#IBOutlet weak var doubleTab: UIView!
override func viewDidLoad() {
super.viewDidLoad()
for _ in 0...2 {
startAnimation()
sleep(5)
self.doubleTab.layer.removeAllAnimations()
startAnimation()
}
}
func startAnimation() {
self.doubleTab.layer.cornerRadius = 15
UIView.animate(withDuration: 1.5, delay: 0, options: [.curveLinear], animations: {
self.doubleTab.backgroundColor = UIColor.white
self.doubleTab.frame.size.width += 20
self.doubleTab.frame.size.height += 20
self.doubleTab.layer.cornerRadius = self.doubleTab.frame.size.width / 2
}) { (_) in
self.doubleTab.backgroundColor = UIColor(displayP3Red: 0.34, green: 0.61, blue: 0.88, alpha: 1)
self.doubleTab.frame.size = CGSize(width: 30, height: 30)
self.doubleTab.layer.cornerRadius = self.doubleTab.frame.size.width / 2
UIView.animate(withDuration: 1.5, delay: 0, options: [.curveLinear], animations: {
self.doubleTab.backgroundColor = UIColor.white
self.doubleTab.frame.size.width += 20
self.doubleTab.frame.size.height += 20
self.doubleTab.layer.cornerRadius = self.doubleTab.frame.size.width / 2
}, completion: nil)
}
}
}
This works fine but i want to repeat the whole animation. When the whole animation is finished I want to wait 2 seconds and then repeat it, then wait 2 seconds and repeat and so on. As you can see in viewDidLoad I tried to repeat the animation with a for-loop but this has no affect or a wrong behavior. A repeat - while has also no effect. So, how can i repeat this animation?
Any help is highly appreciated.
In this case, you could call the startAnimation function recursively, which means call it again after it's completed, which will call itself again after it completed and so on.
So you could basically add , completion: { _ in self.startAnimation() } in the last block where you've currently set completion: nil.
If you wanted to add a delay, you could add the delay in the function as parameter and make it 0 by default for the first animation cycle.
The full code would then be:
func startAnimation(delay: TimeInterval = 0) {
self.doubleTab.layer.cornerRadius = 15
UIView.animate(withDuration: 1.5, delay: delay, options: [.curveLinear], animations: {
self.doubleTab.backgroundColor = UIColor.white
self.doubleTab.frame.size.width += 20
self.doubleTab.frame.size.height += 20
self.doubleTab.layer.cornerRadius = self.doubleTab.frame.size.width / 2
}) { (_) in
self.doubleTab.backgroundColor = UIColor(displayP3Red: 0.34, green: 0.61, blue: 0.88, alpha: 1)
self.doubleTab.frame.size = CGSize(width: 30, height: 30)
self.doubleTab.layer.cornerRadius = self.doubleTab.frame.size.width / 2
UIView.animate(withDuration: 1.5, delay: 0, options: [.curveLinear], animations: {
self.doubleTab.backgroundColor = UIColor.white
self.doubleTab.frame.size.width += 20
self.doubleTab.frame.size.height += 20
self.doubleTab.layer.cornerRadius = self.doubleTab.frame.size.width / 2
}, completion: { _ in
self.startAnimation(delay: 2)
})
}
}

Swift UITabBarController hide with animation

I'm trying to add animation to my tabBarController when hidden. Im able to accomplish this effect with the navigationBarController by using self.navigationController?.isNavigationBarHidden = true. I'm able to hide the tabBar by using self.tabBarController?.tabBar.isHidden = true but i do not get the animation how can I do this thank you in advance.
You could change the tab bar's frame inside an animation, so something like:
func hideTabBar() {
var frame = self.tabBarController?.tabBar.frame
frame?.origin.y = self.view.frame.size.height + (frame?.size.height)!
UIView.animate(withDuration: 0.5, animations: {
self.tabBarController?.tabBar.frame = frame!
})
}
func showTabBar() {
var frame = self.tabBarController?.tabBar.frame
frame?.origin.y = self.view.frame.size.height - (frame?.size.height)!
UIView.animate(withDuration: 0.5, animations: {
self.tabBarController?.tabBar.frame = frame!
})
}
Which sets the tab bar just below the visible screen, so that it slides up/down from the bottom.
I've developed a util extension for UIViewController
Swift 4 compatible:
extension UIViewController {
func setTabBarHidden(_ hidden: Bool, animated: Bool = true, duration: TimeInterval = 0.3) {
if animated {
if let frame = self.tabBarController?.tabBar.frame {
let factor: CGFloat = hidden ? 1 : -1
let y = frame.origin.y + (frame.size.height * factor)
UIView.animate(withDuration: duration, animations: {
self.tabBarController?.tabBar.frame = CGRect(x: frame.origin.x, y: y, width: frame.width, height: frame.height)
})
return
}
}
self.tabBarController?.tabBar.isHidden = hidden
}
}
Improvement of the response of #Luca Davanzo. If the bar is already hidden, it will continue hiding it and moving it lower. Also get rid of the return, so the state of the tabbar.hidden changes when the animation happens.
So I added a check:
extension UIViewController {
func setTabBarHidden(_ hidden: Bool, animated: Bool = true, duration: TimeInterval = 0.5) {
if self.tabBarController?.tabBar.isHidden != hidden{
if animated {
//Show the tabbar before the animation in case it has to appear
if (self.tabBarController?.tabBar.isHidden)!{
self.tabBarController?.tabBar.isHidden = hidden
}
if let frame = self.tabBarController?.tabBar.frame {
let factor: CGFloat = hidden ? 1 : -1
let y = frame.origin.y + (frame.size.height * factor)
UIView.animate(withDuration: duration, animations: {
self.tabBarController?.tabBar.frame = CGRect(x: frame.origin.x, y: y, width: frame.width, height: frame.height)
}) { (bool) in
//hide the tabbar after the animation in case ti has to be hidden
if (!(self.tabBarController?.tabBar.isHidden)!){
self.tabBarController?.tabBar.isHidden = hidden
}
}
}
}
}
}
}
In case if you need to toggle it from hide to visible and vice versa:
func toggleTabbar() {
guard var frame = tabBarController?.tabBar.frame else { return }
let hidden = frame.origin.y == view.frame.size.height
frame.origin.y = hidden ? view.frame.size.height - frame.size.height : view.frame.size.height
UIView.animate(withDuration: 0.3) {
self.tabBarController?.tabBar.frame = frame
}
}
Swift 4 solution:
tabBarController?.tabBar.isHidden = true
UIView.transition(with: tabBarController!.view, duration: 0.35, options: .transitionCrossDissolve, animations: nil)
Here is a simple extension :
func setTabBar(hidden:Bool) {
guard let frame = self.tabBarController?.tabBar.frame else {return }
if hidden {
UIView.animate(withDuration: 0.3, animations: {
self.tabBarController?.tabBar.frame = CGRect(x: frame.origin.x, y: frame.origin.y + frame.height, width: frame.width, height: frame.height)
})
}else {
UIView.animate(withDuration: 0.3, animations: {
self.tabBarController?.tabBar.frame = UITabBarController().tabBar.frame
})
}
}
So I've been playing around for 3 days with this now, finding out that the one that worked for me in my code was Adriana's post from 14th Sept 2018. But I was not sure how to use the coding once copied into my Project. So, after much experimenting I found that the way I could use this func was to put the following into into the respective swipe actions.
setTabBarHidden(false)
setTabBarHidden(true)
My next step is to try to get the swipe actions working while using UIScrollView in the same UIView at the same time.
You have to add UIView transitionWithView class func
Swift 2
func hideTabBarWithAnimation() -> () {
UIView.transitionWithView(tableView, duration: 1.0, options: .TransitionCrossDissolve, animations: { () -> Void in
self.tabBarController?.tabBar.hidden = true
}, completion: nil)
}
Swift 3, 4, 5
func hideTabBarWithAnimation() -> () {
UIView.transition(with: tableView, duration: 1.0, options: .transitionCrossDissolve, animations: { () -> Void in
self.tabBarController?.tabBar.isHidden = true
}, completion: nil)
}

Create a continuously rotating square on the screen using animateKeyframesWithDuration

I tried to use the code below to create a continuously rotating square on the screen. But I don't know why the rotational speed is changing. How could I change the code to make the rotational speed invariable? I tried different UIViewKeyframeAnimationOptions, but seems none of them work.
override func viewDidLoad() {
super.viewDidLoad()
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 40, height: 40)
square.backgroundColor = UIColor.redColor()
self.view.addSubview(square)
let duration = 1.0
let delay = 0.0
let options = UIViewKeyframeAnimationOptions.Repeat
UIView.animateKeyframesWithDuration(duration, delay: delay, options: options, animations: {
let fullRotation = CGFloat(M_PI * 2)
UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1/3, animations: {
square.transform = CGAffineTransformMakeRotation(1/3 * fullRotation)
})
UIView.addKeyframeWithRelativeStartTime(1/3, relativeDuration: 1/3, animations: {
square.transform = CGAffineTransformMakeRotation(2/3 * fullRotation)
})
UIView.addKeyframeWithRelativeStartTime(2/3, relativeDuration: 1/3, animations: {
square.transform = CGAffineTransformMakeRotation(3/3 * fullRotation)
})
}, completion: {finished in
})
}
I faced same problem before, this is how I make it work:
Swift 2
let raw = UIViewKeyframeAnimationOptions.Repeat.rawValue | UIViewAnimationOptions.CurveLinear.rawValue
let options = UIViewKeyframeAnimationOptions(rawValue: raw)
Swift 3,4,5
let raw = UIView.KeyframeAnimationOptions.repeat.rawValue | UIView.AnimationOptions.curveLinear.rawValue
let options = UIView.KeyframeAnimationOptions(rawValue: raw)
I figured this problem out just before gave it up. I don't think there is doc about it, but it just work.
That's really odd... UIView.animateKeyframesWithDuration isn't working as I would expect it to with UIViewKeyframeAnimationOptions.CalculationModeLinear|UIViewKeyframeAnimationOpti‌​ons.Repeat passed in with options.
If you use the non-block method of creating a keyframe animation (see below) the rotation repeats as expected.
If I find out why the block-based option isn't working I'll try and remember to update answer here too!
override func viewDidLoad() {
super.viewDidLoad()
let square = UIView()
square.frame = CGRect(x: 55, y: 300, width: 40, height: 40)
square.backgroundColor = UIColor.redColor()
self.view.addSubview(square)
let fullRotation = CGFloat(M_PI * 2)
let animation = CAKeyframeAnimation()
animation.keyPath = "transform.rotation.z"
animation.duration = 2
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeForwards
animation.repeatCount = Float.infinity
animation.values = [fullRotation/4, fullRotation/2, fullRotation*3/4, fullRotation]
square.layer.addAnimation(animation, forKey: "rotate")
}
Add UIViewKeyframeAnimationOptionCalculationModeLinear do your keyframe options. The default behavior for UIView animations is to "ease in/out" of the animation. i.e., start slow, go up to speed, then slow down again just near the end.
AFAIU, this is happening because the default animation curve is UIViewAnimationOption.CurveEaseInOut.
Unfortunately, UIViewKeyframeAnimationOptions doesn't have options for changing the curve, but you can add them manually!
Use this extension:
Swift 2
extension UIViewKeyframeAnimationOptions {
static var CurveEaseInOut: UIViewKeyframeAnimationOptions { return UIViewKeyframeAnimationOptions(rawValue: UIViewAnimationOptions.CurveEaseInOut.rawValue) }
static var CurveEaseIn: UIViewKeyframeAnimationOptions { return UIViewKeyframeAnimationOptions(rawValue: UIViewAnimationOptions.CurveEaseIn.rawValue) }
static var CurveEaseOut: UIViewKeyframeAnimationOptions { return UIViewKeyframeAnimationOptions(rawValue: UIViewAnimationOptions.CurveEaseOut.rawValue) }
static var CurveLinear: UIViewKeyframeAnimationOptions { return UIViewKeyframeAnimationOptions(rawValue: UIViewAnimationOptions.CurveLinear.rawValue) }
}
Swift 3, 4, 5
extension UIView.KeyframeAnimationOptions {
static var curveEaseInOut: UIView.KeyframeAnimationOptions { return UIView.KeyframeAnimationOptions(rawValue: UIView.AnimationOptions.curveEaseInOut.rawValue) }
static var curveEaseIn: UIView.KeyframeAnimationOptions { return UIView.KeyframeAnimationOptions(rawValue: UIView.AnimationOptions.curveEaseIn.rawValue) }
static var curveEaseOut: UIView.KeyframeAnimationOptions { return UIView.KeyframeAnimationOptions(rawValue: UIView.AnimationOptions.curveEaseOut.rawValue) }
static var curveLinear: UIView.KeyframeAnimationOptions { return UIView.KeyframeAnimationOptions(rawValue: UIView.AnimationOptions.curveLinear.rawValue) }
}
Now you can use curve options in UIView.animateKeyframesWithDuration method
Swift 2
let keyframeOptions: UIViewKeyframeAnimationOptions = [.Repeat, .CurveLinear]
UIView.animateKeyframesWithDuration(duration, delay: delay, options: keyframeOptions, animations: {
// add key frames here
}, completion: nil)
Swift 3, 4, 5
let keyframeOptions: UIView.KeyframeAnimationOptions = [.repeat, .curveLinear]
UIView.animateKeyframes(withDuration: duration, delay: delay, options: keyframeOptions, animations: {
// add key frames here
}, completion: nil)

Resources