I want to show the View for a few seconds and make it disappear from the screen.
If I wait 10 seconds on this screen
I want to make this view go off screen.
How can I animate the view slowly descending down?
code...
fileprivate func setupViewToAnimate(view: UIView) {
UIView.animate(withDuration: 2, delay: 10, options: .curveEaseInOut , animations: {
}) { _ in
}
}
You can use Timer to schedule the dismiss of your snackBar/notification view. it will be triggered after the completion of animation for showing your view. Please check the following code for the same.
private let keyWindow = UIApplication.shared.keyWindow!
let snackbarView = UIView()
fileprivate func setupViewToAnimate() {
let targetYPost = snackbarView.frame.origin.y
let dismissTimerLength = 3.0
snackbarView.frame.origin.y = keyWindow.frame.height
UIView.animate(withDuration: 0.4, animations: {
self.snackbarView.frame.origin.y = targetYPost
}, completion: {
_ in
Timer.scheduledTimer(timeInterval: TimeInterval(dismissTimerLength), target: self, selector: #selector(self.hideSnackbarView), userInfo: nil, repeats: false)
})
}
//After 3.4 sec snack bar will be hidden
#objc private func hideSnackbarView() {
UIView.animate(withDuration: 0.4, animations: {
self.snackbarView.frame.origin.y = self.keyWindow.frame.height
}, completion: {
_ in
self.snackbarView.isHidden = true
})
}
Output:-
I would suggest something like this:
private func setupViewToAnimate(view: UIView, bottomConstraint: NSLayoutConstraint) {
UIView.animate(withDuration: 2, delay: 10, options: .curveEaseInOut , animations: {
// Set the bottomConstraint to minus the height of the view so it gets entierly hidden
bottomConstraint.constant = -view.frame.height
// Tell the view to re-layout its subviews again so we have something to animate
self.view.layoutIfNeeded()
}) { _ in
// If the animation is completed, we can remove the view from the superview since we don't need it anymore
view.removeFromSuperview()
}
}
Related
When my app stat I want to show buttons animation. In storyboard my buttons have height = 50 and y = -50. I have this action in code.
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad()
{
super.viewDidLoad()
buttonAnimation()
}
func buttonAnimation()
{
self.buttonTopConstraint.constant += self.ButtonHeightConstraint.constant
UIView.animate(withDuration: 1.5, delay: 0.0, options: [], animations:
{
self.view.layoutIfNeeded()
}
)
}
Animation work well. But when app start my background imageView in storyboard and different imageView animated too. But I need to have only animation for button. How to fix it?
Don't call animation in viewDidLoad because at that time all views of your controller are going to set.
Use viewDidAppear to call animation method.
override func viewDidAppear()
{
buttonAnimation()
}
Use CGAffineTransform to animate your button. Remove y=-50 from storyboard and keep it at its desired place. Then call this function in viewWillAppear and viewDidAppear :
func animateButton() {
yourButton.transform = CGAffineTransform(translationX: yourButton.frame.origin.x, y: yourButton.frame.origin.y - 50)
UIView.animate(withDuration: 1.5, delay: 0.0, options: [], animations: {
self.yourButton.transform = .identity
}, completion: nil)
}
I'm trying to code an animation behind the profile picture, in order to encourage the user to click on it.
It's a circle which become bigger and smaller then.
override func layoutSubviews() {
super.layoutSubviews()
UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.bouncedView.transform = CGAffineTransform(scaleX:
1.3, y: 1.3)
}, completion: nil)
}
The problem is that, when I go to another viewController, the animation is stopped and the circle stays like you can the on the screen shot. Do you know how I could avoid this issue ?
Do a transform to .identity on ViewDidAppear. Something similar to below code:
class HomeController: UIViewController {
#IBOutlet weak var viewToAnimate: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
self.viewToAnimate.transform = .identity
animateView()
}
func animateView(){
UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.viewToAnimate.transform = CGAffineTransform(scaleX:
1.3, y: 1.3)
}, completion: nil)
}
}
The problem as you might have guessed is that the UIView.animate is only called on the ViewDidLoad method and since we don't have access that code while returning to this ViewController from another, it is better to start the animation in the ViewWillAppear method.
If the same issue occurs when you switch between tabs, then please make a separate UIView subclass for the view that you want to animate and proceed as follows:
class AnimateView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
}
override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
self.transform = .identity
animateView()
}
func animateView(){
UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.transform = CGAffineTransform(scaleX:
1.3, y: 1.3)
}, completion: nil)
}
}
Here, you have taken the animate function to the UIView object and whenever the view appears, the animation will be reset.
Well, I don't yet have a satisfying answer yet (at least that would satisfy me), but I would like to add at least some insight to which I was able to get by a bit of experimenting.
First of all, it seems that the animation gets ended when the viewController is not currently the one presented. In your case that means that the animation stops and finishes with the state, in which the view is 1.3 times bigger, and stops repeating. layoutSubviews gets called only when you present it the first time. At least the viewController.viewDidLayoutSubviews gets called only at the beginning and not when you go back (so layoutSubviews will not get executed when the view reappears) - so the animation won't get restarted.
I tried to move the animation to the viewDidAppear of a UIViewController - that did not work either, because stopping animation resulted in state in which the view was already scaled 1.3 times. Resetting the state to CGAffineTransform.identity before creating the animation did work.
Now as I said, this can serve you as a workaround on how to get it working, however, I think what you really are looking for is some hook that would tell your view (not viewController) that it got presented again. But the following minimal example can at least help others to take a look at it and not start from scratch.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
let animator = UIViewPropertyAnimator(duration: 1, timingParameters: UICubicTimingParameters(animationCurve: .linear))
init(title: String) {
super.init(nibName: nil, bundle: nil)
self.title = title
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
let animatableView = UIView()
override func loadView() {
let view = UIView()
view.backgroundColor = .white
animatableView.frame = CGRect(x: 150, y: 400, width: 100, height: 100)
animatableView.backgroundColor = UIColor.magenta
view.addSubview(animatableView)
self.view = view
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.animatableView.transform = CGAffineTransform.identity
UIView.animate(withDuration: 1.0, delay: 0, options: [.repeat, .autoreverse], animations: {
self.animatableView.transform = CGAffineTransform(scaleX:
1.3, y: 1.3)
}, completion: nil)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("\(self.title) >> Lay out")
}
}
let tabBar = UITabBarController()
tabBar.viewControllers = [MyViewController(title: "first"), MyViewController(title: "second"), MyViewController(title: "third")]
tabBar.selectedIndex = 0
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = tabBar
EDIT
I added few lines to check if the self.animatableView.layer.animationKeys() contains any animations, it seems that switching tabs removes the animation from the view's layer - so you have to find a way to add the animation everytime the view reappears.
EDIT 2
So I would go with #SWAT's answer and use willMove(toWindow:) instead of layoutSubviews.
I have a UIView EmptyCollectionView, which I display when my UICollectionView is empty. The way I have this working is that I create the UIView and addSubview in viewDidLoad of my ViewController, then change toggle isHidden property of the view (as well as the collectionview) as needed.
I'd like to polish things up a little now I have the core function working, and I wan't to add some subtle animation to the subviews contained in my empty view, such as making the contained imageview bounce on display.
So my question is, what is the best way to detect when the UIView is being shown (i.e. is there a viewDidAppear type callback I could use)?
Supplementary question: I'm new to this... Is adding the empty view and toggling the isHidden property a good way of doing this? Or should I be doing it a different way? (i.e. should I instead be creating and destroying the view as needed, rather than keeping it around)
Thanks
The best way in my opinion is to extend UIView
extension UIView {
func fadeIn(_ duration: TimeInterval = 0.2, onCompletion: (() -> Void)? = nil) {
self.alpha = 0
self.isHidden = false
UIView.animate(withDuration: duration,
animations: { self.alpha = 1 },
completion: { (value: Bool) in
if let complete = onCompletion { complete() }
}
)
}
func fadeOut(_ duration: TimeInterval = 0.2, onCompletion: (() -> Void)? = nil) {
UIView.animate(withDuration: duration,
animations: { self.alpha = 0 },
completion: { (value: Bool) in
self.isHidden = true
if let complete = onCompletion { complete() }
}
)
}
}
So you just have to call view.fadeIn() for a default 0.2 sec animation, or view.fadeIn(1) to make it last one second.
You can even add a completion event:
view.fadeOut(0.5, onCompletion: {
print("Animation completed, do whatever you want")
})
This works, I hope it can help you. To hide view:
UIView.animate(withDuration: 0.3/*Animation Duration second*/, animations: {
self.EmptyCollectionView.alpha = 0
}, completion: {
(value: Bool) in
self.EmptyCollectionView.isHidden = true
})
To show view:
self.EmptyCollectionView.isHidden = false
UIView.animate(withDuration: 0.3, animations: {
self.EmptyCollectionView.alpha = 1
}, completion: nil)
Swift 4.2 extension to allow animation when setting isHidden on any UIView class:
extension UIView {
func setIsHidden(_ hidden: Bool, animated: Bool) {
if animated {
if self.isHidden && !hidden {
self.alpha = 0.0
self.isHidden = false
}
UIView.animate(withDuration: 0.25, animations: {
self.alpha = hidden ? 0.0 : 1.0
}) { (complete) in
self.isHidden = hidden
}
} else {
self.isHidden = hidden
}
}
}
You can animate the alpha property of EmptyCollectionView to either 0 to hide or 1 to show
UIView.animate(withDuration: 0.5) {
self.EmptyCollectionView.alpha = 0
}
Also make sure that isOpaque property is set to False to enable Transparency of the view
Swift 5
import UIKit
extension UIView {
func animateSetHidden(_ hidden: Bool, duration: CGFloat = CATransaction.animationDuration(), completion: #escaping (Bool)->() = { _ in}) {
if duration > 0 {
if self.isHidden && !hidden {
self.alpha = 0
self.isHidden = false
}
UIView.animate(withDuration: duration, delay:0, options: .beginFromCurrentState) {
self.alpha = hidden ? 0 : 1
} completion: { c in
if c {
self.isHidden = hidden
}
completion(c)
}
} else {
self.isHidden = hidden
self.alpha = hidden ? 0 : 1
completion(true)
}
}
}
I have a viewController where am showing image for adding the zooming functionality I added the scrollView in viewController and inside of ScrollView I added ImageView everything is working fine expect of one thing, am hiding, and showing the bars (navigation bar + tab bar) on tap but when hiding them my imageView moves upside see the below images
See here's the image and the bars.
Here I just tapped on the view and bars got hidden but as you can see my imageView is also moved from its previous place, this is what I want to solve I don't want to move my imageView.
This is how am hiding my navigation bar:
func tabBarIsVisible() ->Bool {
return self.tabBarController?.tabBar.frame.origin.y < CGRectGetMaxY(self.view.frame)
}
func toggle(sender: AnyObject) {
navigationController?.setNavigationBarHidden(navigationController?.navigationBarHidden == false, animated: true)
setTabBarVisible(!tabBarIsVisible(), animated: true)
}
any idea how can I hide and show the bars without affecting my other Views?
The problem is that you need to set the constraint of your imageView to your superView, not TopLayoutGuide or BottomLayoutGuide.
like so:
Then you can do something like this to make the it smootly with animation:
import UIKit
class ViewController: UIViewController {
#IBOutlet var imageView: UIImageView!
var barIsHidden = false
var navigationBarHeight: CGFloat = 0
var tabBarHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ViewController.hideAndShowBar))
view.addGestureRecognizer(tapGesture)
navigationBarHeight = (self.navigationController?.navigationBar.frame.size.height)!
tabBarHeight = (self.tabBarController?.tabBar.frame.size.height)!
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func hideAndShowBar() {
print("tap!!")
if barIsHidden == false {
UIView.animateWithDuration(0.5, delay: 0.0, options: .CurveEaseOut, animations: {
// fade animation
self.navigationController?.navigationBar.alpha = 0.0
self.tabBarController?.tabBar.alpha = 0.0
// set height animation
self.navigationController?.navigationBar.frame.size.height = 0.0
self.tabBarController?.tabBar.frame.size.height = 0.0
}, completion: { (_) in
self.barIsHidden = true
})
} else {
UIView.animateWithDuration(0.5, delay: 0.0, options: .CurveEaseOut, animations: {
// fade animation
self.navigationController?.navigationBar.alpha = 1.0
self.tabBarController?.tabBar.alpha = 1.0
// set height animation
self.navigationController?.navigationBar.frame.size.height = self.navigationBarHeight
self.tabBarController?.tabBar.frame.size.height = self.tabBarHeight
}, completion: { (_) in
self.barIsHidden = false
})
}
}
}
Here is the result:
I have created an example project for you at: https://github.com/khuong291/Swift_Example_Series
You can see it at project 37
Hope this will help you.
I want to add a UILabel to the view which slides down when an error occurs to send error message to user. The prototype of it is like the one Facebook or Instagram shows. Here is the codes I have worked out so far:
func sendErrorMessage(errorString: String) {
self.errorLabel.text = errorString
UIView.animateWithDuration(1, animations: {
self.errorLabel.frame.height = 30 //Cannot assign to the result of this expression
self.topView.layoutIfNeeded()
}, completion: {
(finished: Bool) in var timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector(), userInfo: nil, repeats: false)
})
}
errorLabel is now already in storyboard but with the height of 0 and topView is the superview of errorLabel. I am quite new to these methods so I am stuck here. I don't understand why that error occurred and what the selector should do here. There is also a step that I haven't done here that the errorLabel should slide up to disappear after three seconds and that's why I need a timer here.
Plus: Is there any difference if I create a new errorLabel whenever it is needed instead of make it ready before in storyboard? I mean in app performance of memory management.
UPDATE
I need errorLabel in many ViewControllers, so following the idea of #Sajjon, I tried to subclass UILabel. Here is my subclass ErrorLabel:
class ErrorLabel: UILabel {
var errorString: String?
func sendErrorMessage() {
self.text = errorString
showErrorLabel()
let timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: "hideErrorLabel", userInfo: nil, repeats: false)
}
func animateFrameChange() {
UIView.animateWithDuration(1, animations: { self.layoutIfNeeded() }, completion: nil)
}
func showErrorLabel() {
let oldFrame = self.frame
let newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.height + 30, oldFrame.width)
self.frame = newFrame
self.animateFrameChange()
}
func hideErrorLabel() {
let oldFrame = self.frame
let newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.height - 30, oldFrame.width)
self.frame = newFrame
self.animateFrameChange()
}
}
Generally speaking, CGRect objects used by views for their frame or bounds properties are immutable. Instead of trying to directly modify the view's frame, create a new CGRect which contains the desired final size and position and assign that to the view's frame in the animation block.
//Don't do this.
myView.frame.size.height = 30;
// Do this instead.
CGRect oldRect = myView.frame;
CGRect newFrame = CGRectMake(oldRect.origin.x, oldRect.origin.y, oldRect.size.width, 30);
[UIView animateWithDuration:0.4 animations:^{
myView.frame = newFrame;
};
Also, I would strongly suggest making the label show and hide my changing its y position on and off the top of the screen, rather than changing the height to 0, which can have some unintended layout consequences
You ought to use autolayout for this, when showing the errorLabel (or some container view that it is inside), I would give the height constraint your wished value (30). And hiding it again by giving the constraint a value of 0.
I would create an UIView extension and put code for animating this height change.
This is untested code, but will give you an idea of how to achieve it.
class MyViewController: UIViewController {
#IBOutlet weak var errorLabelHeightConstraint: NSLayoutConstraint!
private let errorLabelHeightVisible: CGFloat = 30
private let hideDelay: NSTimeInterval = 3
private var timer: NSTimer!
func sendErrorMessage(errorString: String) {
errorLabel.text = errorString
showOrHideErrorView(false)
}
func showOrHideErrorView(hide: Bool = true, animated: Bool = true) {
if hide {
errorLabelHeightConstraint.constant = 0
} else {
errorLabelHeightConstraint.constant = errorLabelHeightVisible
}
let automaticallyHideErrorViewClosure: () -> Void = {
/* Only scheduling hiding of error message, if we just showed it. */
if !hide {
dispatch_async(dispatch_get_main_queue(), {
() -> Void in
automaticallyHideErrorMessage()
})
}
}
if animated {
view.animateConstraintChange(completion: {
(finished: Bool) -> Void in
automaticallyHideErrorViewClosure()
})
} else {
view.layoutIfNeeded()
automaticallyHideErrorViewClosure()
}
}
/* Selector method */
func hideError() {
showOrHideErrorView()
}
func automaticallyHideErrorMessage() {
if timer != nil {
if timer.valid {
timer.invalidate()
}
timer = nil
}
timer = NSTimer.scheduledTimerWithTimeInterval(hideDelay, target: self, selector: "hideError", userInfo: nil, repeats: false)
}
}
extension UIView {
static var standardDuration: NSTimeInterval { get { return 0.3 } }
func animateConstraintChange(duration: NSTimeInterval = standardDuration, completion: ((Bool) -> Void)? = nil) {
UIView.animate(durationUsed: duration, animations: {
() -> Void in
self.layoutIfNeeded()
}, completion: completion)
}
}
you need to set Frame of UILable instead of its height only.
func sendErrorMessage(errorString: String) {
self.errorLabel.text = errorString
UIView.animateWithDuration(1, animations: {
self.errorLabel.setFrame = CGRectmake(self.errorLabel.frame.origin.x,self.errorLabel.frame.origin.y,self.errorLabel.frame.size.width,30)
self.topView.layoutIfNeeded()
}, completion: {
(finished: Bool) in var timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector(), userInfo: nil, repeats: false)
})
}
For Change height Try This:
self.errorLabel.frame.size.height = 30