I'm attempting to smoothly transition between two images by using a UIImageView and by animating the transition, but it seems that the transition isn't working as expected as a white screen appears instead of the new image fading in.
I've used https://stackoverflow.com/a/42710002/7908770 for reference and can't figure out where I've gone wrong :(
class ViewController: UIViewController {
var bckgImageView = UIImageView(image: UIImage(named: "one"))
override func viewDidLoad() {
super.viewDidLoad()
bckgImageView.frame = self.view.frame
self.view.addSubview(bckgImageView)
animateBackground()
}
func animateBackground() {
UIView.transition(with: self.view,
duration: 5,
options: .transitionCrossDissolve,
animations: { self.bckgImageView.image = UIImage(named: "two") },
completion: nil)
}
AutoLayout not calculated yet when in viewDidLoad
You can set frame when layout changed in viewDidLayoutSubviews.
Try this
class ViewController: UIViewController {
var bckgImageView = UIImageView(image: UIImage(named: "one"))
override func viewDidLoad() {
super.viewDidLoad()
bckgImageView.frame = self.view.frame
self.view.addSubview(bckgImageView)
animateBackground()
}
func animateBackground() {
UIView.transition(with: self.view,
duration: 5,
options: .transitionCrossDissolve,
animations: { self.bckgImageView.image = UIImage(named: "two") },
completion: nil)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
bckgImageView.frame = self.view.frame
}
}
First, you're setting the frame of the image view to the frame of the "root" view, which is not-yet defined in viewDidLoad(). You're much better off using constraints and letting auto-layout size the view.
Second, if you are starting your animation in viewDidLoad() or even in viewDidLayoutSubviews(), the screen is not yet visible --- so you don't see the initial image (or you only see it very briefly, if the animation is short).
So, start your animation at viewDidAppear(), or, give it a little delay, as in the following:
class ViewController: UIViewController {
var bckgImageView = UIImageView(image: UIImage(named: "one"))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(bckgImageView)
bckgImageView.translatesAutoresizingMaskIntoConstraints = false
// constrain image view to all 4 sides
NSLayoutConstraint.activate([
bckgImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
bckgImageView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
bckgImageView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
bckgImageView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// start animation after 1 second
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
self.animateBackground()
})
}
func animateBackground() {
UIView.transition(with: self.view,
duration: 5,
options: .transitionCrossDissolve,
animations: { self.bckgImageView.image = UIImage(named: "two") },
completion: nil)
}
}
Related
I've got some subviews in my navigationBar. A black square, some titles and a blue bar. When pushed to a detail viewController, I want my subviews to hide (animated).
I added the subways to the navigation bar inside the main view controller using
self.navigationController?.navigationBar.addsubView(mySubview).
Currently, it looks like his:
What is the right way to hide (animated) those subviews in the detail viewController?
Your question is very interesting. I never thought about the navigation bar.
When UINavigationController is used as the root controller, all UIViewControllers are stored in its stack, meaning that all UIViewControllers share a navigationBar.
You added mySubView to navigationBar in the first UIViewController. If you want to hide it on the details page, you can search for subviews directly.
The first step, you need to give mySubView a tag, which can be a tag, or it can be a custom type, which is convenient for later judgment.
On the first page
import SnapKit
mySubView = UIView()
mySubView?.tag = 999
mySubView?.backgroundColor = .red
mySubView?.translatesAutoresizingMaskIntoConstraints = false
let navBar = navigationController?.navigationBar
navBar!.addSubview(mySubView!)
mySubView!.snp.makeConstraints { (make) in
make.left.equalTo(navBar!).offset(100)
make.centerY.equalTo(navBar!)
make.width.height.equalTo(50)
}
On the details page, I deleted isHidden and saved navigationBar with the attribute because navigationBar = nil during the gesture. If SnapKit is unfamiliar, take a few more minutes to learn.
var mySubView: UIView? = nil
var navBar: UINavigationBar?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// Do any additional setup after loading the view.
navigationController?.interactivePopGestureRecognizer?.addTarget(self, action: #selector(backGesture))
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
homeNavigationBarStatus()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
homeNavigationBarStatus()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if mySubView == nil {
for view: UIView in (navigationController?.navigationBar.subviews)! {
if view.tag == 999 {
mySubView = view
}
}
}
UIView.animate(withDuration: 1, delay: 0, options: .curveEaseOut, animations: {
self.detailNavigationBarStatus()
}, completion: nil)
}
func detailNavigationBarStatus(){
changingNavigationBarStatus(progress: 0)
}
func homeNavigationBarStatus(){
changingNavigationBarStatus(progress: 1.0)
}
func changingNavigationBarStatus(progress:CGFloat){
mySubView?.alpha = progress
mySubView?.snp.updateConstraints({ (make) in
make.left.equalTo(navBar!).offset(100 * progress)
})
}
#objc func backGesture(sender: UIScreenEdgePanGestureRecognizer) {
switch sender.state {
case .changed:
let x = sender.translation(in: view).x
progress = x / view.frame.width
changingNavigationBarStatus(progress: progress)
default:
break
}
}
However, using tag values is not elegant enough, you can create a specific class for mySubView, which can also be judged by class.
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'm looking for the best way to invert the place of two UIView, with animation if possible (first i need to change the place, animation is optionnal).
viewController :
So, i want view1 to invert his place with the view2. The views are set with autolayout in the storyboard at the init.
if state == true {
view1 at the top
view2 at the bot
} else {
view 2 at the top
view 1 at the top
}
I've tried to take view.frame.(x, y, width, height) from the other view and set it to the view but it doesn't work.
I think the best way to do this would be to have a topConstraint for both views connected to the header and then change their values and animate the transition. You can do it in a way similar to this:
class MyViewController: UIViewController {
var view1TopConstraint: NSLayoutConstraint!
var view2TopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
view1TopConstraint = view1.topAnchor.constraintEqualToAnchor(header.bottomAnchor, constant: 0)
view1TopConstraint.isActive = true
view2TopConstraint = view2.topAnchor.constraintEqualToAnchor(header.bottomAnchor, constant: view1.frame.height)
view2TopConstraint.isActive = true
}
func changeView2ToTop() {
UIView.animateWithDuration(0.2, animations: { //this will animate the change between 2 and 1 where 2 is at the top now
self.view2TopConstraint.constant = 0
self.view1TopConstraint.constant = view2.frame.height
//or knowing that view2.frame.height represents 30% of the total view frame you can do like this as well
// self.view1TopConstraint.constant = view.frame.height * 0.3
self.view.layoutIfNeeded()
})
}
You could also create the NSLayoutConstraint in storyboard and have an outlet instead of the variable I have created or set the top constraint for both views in storyboard at "remove at build time" if you are doing both. This way you won't have 2 top constraints and no constraint warrning
I Made an Example: https://dl.dropboxusercontent.com/u/24078452/MovingView.zip
I just created a Storyboard with 3 View, Header, View 1 (Red) and View 2 (Yellow). Then I added IBoutlets and animated them at viewDid Appear, here is the code:
import UIKit
class ViewController: UIViewController {
var position1: CGRect?
var position2: CGRect?
#IBOutlet weak var view1: UIView!
#IBOutlet weak var view2: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
position1 = view1.frame
position2 = view2.frame
UIView.animate(withDuration: 2.5) {
self.view1.frame = self.position2!
self.view2.frame = self.position1!
}
}
}
Hello #Makaille I have try to resolve your problem.
I have made an example, which will help you for your required implementation.
Check here: Source Code
I hope, it will going to help you.
#IBAction func toggle(_ sender: UIButton) {
sender.isUserInteractionEnabled = false
if(sender.isSelected){
let topPinConstantValue = layout_view2TopPin.constant
layout_view1TopPin.constant = topPinConstantValue
layout_view2TopPin.priority = 249
layout_view1BottomPin_to_View2Top.priority = 999
layout_view1TopPin.priority = 999
layout_view2BottomPin_ToView1Top.priority = 249
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
}, completion: { (value) in
if(value){
sender.isUserInteractionEnabled = true
}
})
} else {
let topPinConstantValue = layout_view1TopPin.constant
layout_view2TopPin.constant = topPinConstantValue
layout_view1TopPin.priority = 249
layout_view2BottomPin_ToView1Top.priority = 999
layout_view2TopPin.priority = 999
layout_view1BottomPin_to_View2Top.priority = 249
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
}, completion: { (value) in
if(value){
sender.isUserInteractionEnabled = true
}
})
}
sender.isSelected = !sender.isSelected
}
I'm a very new-to-code learner. I've been taking some online courses, and came across this project recently.
I basically copied the code as I saw it on the screen, as the project files weren't available to download.
The animation is supposed to bring the UILabels from outside the view and position them within the view.
What seems to happen however, is the labels are starting within the view screen and animate outward and beyond.
I've copied the project below. Any help is so very appreciated.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var helloWorld: UILabel!
#IBOutlet weak var secondLabel: UILabel!
#IBOutlet weak var hiddenLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
helloWorld.center.y -= view.bounds.height
secondLabel.center.y += view.bounds.height
hiddenLabel.alpha = 0.0
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// Animate Hello World label
UIView.animateWithDuration(1.5, animations: {
self.helloWorld.center.y += self.view.bounds.height
}, completion: { finished in
self.secondAnimation()
})
// Animate background color change
UIView.animateWithDuration(2.0, delay: 1.5, options: [], animations: {
self.view.backgroundColor = UIColor.yellowColor()
}, completion:nil)
}
// Animate second Label
func secondAnimation() {
UIView.animateWithDuration(1.5, delay: 0.0, options: [], animations: { () -> Void in
self.secondLabel.center.y -= self.view.bounds.height
}, completion:nil)
}
func backgroundColor() {
UIView.animateWithDuration(2.5, animations: {
self.view.backgroundColor = UIColor.blackColor()
}, completion: nil)
UIView.animateWithDuration(1.0, delay: 1.5, options: [], animations: {
self.hiddenLabel.alpha = 1.0
}, completion:nil)
}
}
I would check hello world.center.y value in viewWillAppear, before you subtract the view.bounds.height. If center.y value is large and positive, then the subtraction might be setting the center.y value to be inside of the view. If this is the case, then you would want to change the subtraction in viewWillAppear to addition and then the addition in the viewDidAppear animation to subtraction.
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.