I have a text view (textView) and a button (sendButton).
The button's bottom constraint is constraint to the view's bottom.
The textView becomes the first responder in viewDidAppear.
So when i present the Controller, the keyboard goes up and the button animates along with it.
Here's the code:
override func viewDidLoad() {
super.viewDidLoad()
setupSendButton()
dismissKeyboard()
}
override func viewDidAppear(_ animated: Bool) {
textView.becomeFirstResponder()
}
func setupSendButton() {
self.view.addSubview(sendButton)
sendButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
sendButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
sendButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
sendButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
sendButton.translatesAutoresizingMaskIntoConstraints = false
}
// TextView Delegate Method
func textViewDidBeginEditing(_ textView: UITextView) {
// Animation begins after textView did begin editing
animateSendButton(bottomConstraint: -216)
}
At this point everything works fine.
My problem is that when i dismiss the keyboard and end editing, I want to animate back so that the button's bottom constraint is the view's bottom constraint again.
But that doesn't work.
// TextView Delegate Method
func textViewDidEndEditing(_ textView: UITextView) {
// Animation begins after textView did end editing (it doesn't)
animateSendButton(bottomConstraint: 0)
}
// function to dismiss keyboard and end editing
func dismissKeyboard() {
let touch = UITapGestureRecognizer(target: self, action: #selector(tapGesture))
self.view.addGestureRecognizer(touch)
}
#objc func tapGesture(gesture: UITapGestureRecognizer){
// Ends editing and dismisses keyboard
self.view.endEditing(true)
}
Animate Button Funktion: animateSendButton(bottomConstraint: CGFloat)
func animateSendButton(bottomConstraint: CGFloat) {
sendButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: bottomConstraint).isActive = true
UIView.animate(withDuration: 0.55, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
You must delete previous bottom constraint before of adding other
or change
var bottomCon:NSLayoutConstraint!
//////
func setupSendButton() {
self.view.addSubview(sendButton)
sendButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
sendButton.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
sendButton.heightAnchor.constraint(equalToConstant: 60).isActive = true
self.bottomCon = sendButton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
self.bottomCon.active= true
sendButton.translatesAutoresizingMaskIntoConstraints = false
}
func animateSendButton(bottomConstraint: CGFloat) {
self.bottomCon.constant = bottomConstraint
UIView.animate(withDuration: 0.55, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
Related
I made a toast view with a UIImageView in it, what I want to do is everytime the user taps the image view, the toast dismisses itself. Naturally I configured a UITapGestureRecognizer to my image view, but the selector function is not getting called. Here's what I did:
class ToastView: UIView {
private var icon: UIImageView!
init() {
super.init(frame: .zero)
setupViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
}
override func layoutSubviews() {
super.layoutSubviews()
setupConstraints()
}
private func setupViews() {
translatesAutoresizingMaskIntoConstraints = false
layer.cornerRadius = 8
isUserInteractionEnabled = true
icon = UIImageView()
icon.translatesAutoresizingMaskIntoConstraints = false
icon.image = UIImage(named: "someImage")
icon.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(iconHandler))
icon.addGestureRecognizer(tapGesture)
addSubview(icon)
}
private func setupConstraints() {
NSLayoutConstraint.activate([
topAnchor.constraint(equalTo: superview!.topAnchor, constant: 55),
leadingAnchor.constraint(equalTo: superview!.leadingAnchor, constant: 16),
trailingAnchor.constraint(equalTo: superview!.trailingAnchor, constant: -16),
icon.heightAnchor.constraint(equalToConstant: 16),
icon.widthAnchor.constraint(equalToConstant: 16),
icon.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
icon.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
#objc private func iconHandler(_ sender: UITapGestureRecognizer) {
// This function is not called
print("handle icon")
}
}
After some research, I tried to give the ToastView a gesture recognizer instead of the image view. So I did give the tap gesture recognizer when showing the toast in my custom UIViewController class like this:
class CustomViewController: UIViewController {
private var isShowingToast: Bool = false
private lazy var toast: ToastView = {
let toast = ToastView()
toast.isUserInteractionEnabled = true
toast.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissToast)))
return toast
}()
func showToastWithMessage() {
if !isShowingToast {
view.addSubview(toast)
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: { [weak self] in
self?.toast.alpha = 1
self?.toast.frame.origin.y += 10
self?.isShowingToast = true
}, completion: { _ in
UIView.animate(withDuration: 0.5, delay: 5.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: { [weak self] in
self?.toast.alpha = 0
self?.toast.frame.origin.y -= 10
}, completion: { [weak self] _ in
self?.isShowingToast = false
self?.toast.removeFromSuperview()
})
})
}
}
#objc private func dismissToast() {
// This functions does not get called as well
print("dismiss")
}
}
Unfortunately the dismiss function does not print to the console. Is there anyway to resolve this?
Looks like this occurs because of your animation. View is all the time in animation status and block tap gesture. U can try call it with delay instead of adding delay for your animation.
func showToastWithMessage() {
if !isShowingToast {
view.addSubview(toast)
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: { [weak self] in
self?.toast.alpha = 1
self?.toast.frame.origin.y += 10
self?.isShowingToast = true
}, completion: { _ in
print("Completion")
})
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: { [weak self] in
self?.toast.alpha = 0
self?.toast.frame.origin.y -= 10
}, completion: { [weak self] _ in
self?.isShowingToast = false
self?.toast.removeFromSuperview()
})
}
}
}
This way view going to animate status after 5 sec not with 5 sec delay.
This is how your controller look like:
class CustomViewController: UIViewController {
private var isShowingToast: Bool = false
lazy var toast: ToastView = {
let toast = ToastView()
toast.isUserInteractionEnabled = true
toast.backgroundColor = .red
toast.translatesAutoresizingMaskIntoConstraints = false
toast.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissToast)))
return toast
}()
override func viewDidLoad() {
super.viewDidLoad()
// Add Toast constraints
view.addSubview(toast)
toast.heightAnchor.constraint(equalToConstant: 200).isActive = true
toast.widthAnchor.constraint(equalTo: toast.heightAnchor).isActive = true
toast.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
toast.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
func showToastWithMessage() {
if !isShowingToast {
view.addSubview(toast)
UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: { [weak self] in
self?.toast.alpha = 1
self?.toast.frame.origin.y += 10
self?.isShowingToast = true
}, completion: { _ in
UIView.animate(withDuration: 0.5, delay: 5.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: { [weak self] in
self?.toast.alpha = 0
self?.toast.frame.origin.y -= 10
}, completion: { [weak self] _ in
self?.isShowingToast = false
self?.toast.removeFromSuperview()
})
})
}
}
#objc private func dismissToast() {
// This functions does not get called as well
print("dismiss")
}
}
And this is your class:
class ToastView: UIView {
private var icon = UIImageView()
init() {
super.init(frame: .zero)
setupViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
}
override func layoutSubviews() {
super.layoutSubviews()
setupConstraints()
}
private func setupViews() {
translatesAutoresizingMaskIntoConstraints = false
layer.cornerRadius = 8
isUserInteractionEnabled = true
icon.translatesAutoresizingMaskIntoConstraints = false
icon.image = UIImage(named: "profilo")?.withRenderingMode(.alwaysOriginal) //put your image here
icon.isUserInteractionEnabled = true
icon.layer.cornerRadius = 8
icon.layer.masksToBounds = true //set image masked round corner
icon.clipsToBounds = true
icon.contentMode = .scaleAspectFill //set image content mode
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(iconHandler))
icon.addGestureRecognizer(tapGesture)
}
private func setupConstraints() {
// Setup the constraints for the subviews
addSubview(icon)
icon.topAnchor.constraint(equalTo: topAnchor).isActive = true
icon.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
icon.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
icon.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
#objc private func iconHandler(_ sender: UITapGestureRecognizer) {
// This function is not called
print("handle icon")
}
}
this is the result:
I have made a simple example of a UIView inside a UIViewController, so that when I tap on the inner view, it increases and decreases its height. That works fine, but I can't make the height transition animate.
Here is the view controller, followed by the view:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let theView = MyView()
theView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(theView)
NSLayoutConstraint.activate([
theView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 50.0),
theView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant:0.0),
theView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: 0.0),
theView.heightAnchor.constraint(equalToConstant: 925)
])
}
}
class MyView: UIView {
fileprivate var closedChanger = NSLayoutConstraint()
fileprivate var isOpen = false
init() {
super.init(frame: CGRect.zero)
setupView()
}
fileprivate func setupView() {
self.backgroundColor = .gray
self.translatesAutoresizingMaskIntoConstraints = true
closedChanger = self.heightAnchor.constraint(equalToConstant: 150.0)
NSLayoutConstraint.activate([
closedChanger // start off with it shorter
])
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:))))
}
#objc func handleTap(sender: UITapGestureRecognizer) {
UIView.animate(withDuration: 1.0, animations: {
self.closedChanger.constant = self.isOpen ? self.closedChanger.constant - 25 : self.closedChanger.constant + 25
// self.layoutIfNeeded()
})
isOpen.toggle()
}
}
The layoutIfNeeded doesn't affect it. Is a UIView not allowed to animate its own height change?
Your are kind of right when you say that a view can't animate itself. The problem is that you need to animate the change to the containing view, since that the is the view hierarchy that is changing.
You need to make the change to the constraint outside of the animation block, and then call layoutIfNeeded on the superview in the animation block.
#objc func handleTap(sender: UITapGestureRecognizer) {
self.closedChanger.constant = self.isOpen ? self.closedChanger.constant - 25 : self.closedChanger.constant + 25
UIView.animate(withDuration: 1.0, animations: {
self.superview?.layoutIfNeeded()
})
isOpen.toggle()
}
I'm getting in trouble on UIView Animation. When I click into one of UITextFields basically everything works fine until I select another UITextField after being selected the other one, the fields move up and down then return to the same place after I select the other UITextField.
What am I doing wrong? What I expected is to move the whole UIStackView containing my fields into up to avoid the keyboard to cover it all. Also, to keep the animation static when I click into the other UITextField, just returning to the default position when the keyboard got dismissed.
class LoginViewController: UIViewController {
var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
viewTapped()
setupScreen()
setupViews()
setConstraints()
}
private func setupScreen() {
self.view.backgroundColor = .systemPink
}
private func setConstraints() {
self.textFieldLogin.heightAnchor.constraint(equalToConstant: 50).isActive = true
self.textFieldLogin.widthAnchor.constraint(equalToConstant: 190).isActive = true
//
self.textFieldSenha.heightAnchor.constraint(equalToConstant: 50).isActive = true
self.textFieldSenha.widthAnchor.constraint(equalToConstant: 190).isActive = true
//
self.loginButton.widthAnchor.constraint(equalToConstant: 145).isActive = true
self.loginButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
//
self.stackView.heightAnchor.constraint(equalToConstant: 200).isActive = true
self.stackView.widthAnchor.constraint(equalToConstant: 200).isActive = true
self.stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
self.stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
}
private func setupViews() {
self.view.addSubview(self.stackView)
self.viewTapped()
}
private lazy var textFieldLogin: UITextField = {
let textFieldLogin = UITextField()
textFieldLogin.tag = 1
textFieldLogin.translatesAutoresizingMaskIntoConstraints = false
textFieldLogin.layer.cornerRadius = 3.7
textFieldLogin.textAlignment = .center
textFieldLogin.placeholder = "Usuário"
textFieldLogin.backgroundColor = .white
textFieldLogin.delegate = self
return textFieldLogin
}()
private lazy var textFieldSenha: UITextField = {
let textFieldSenha = UITextField()
textFieldSenha.tag = 2
textFieldSenha.translatesAutoresizingMaskIntoConstraints = false
textFieldSenha.layer.cornerRadius = 3.7
textFieldSenha.textAlignment = .center
textFieldSenha.placeholder = "Senha"
textFieldSenha.backgroundColor = .white
textFieldSenha.delegate = self
return textFieldSenha
}()
private lazy var loginButton: UIButton = {
let loginButton = UIButton()
loginButton.translatesAutoresizingMaskIntoConstraints = false
loginButton.layer.cornerRadius = 3.8
loginButton.titleLabel?.font = UIFont(name: "Arial", size: 19)
loginButton.setTitle("Entrar", for: .normal)
loginButton.setTitleColor(.systemGreen, for: .normal)
loginButton.backgroundColor = .white
return loginButton
}()
private lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [self.textFieldLogin, self.textFieldSenha, self.loginButton])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.backgroundColor = .systemBlue
stackView.axis = .vertical
stackView.distribution = .equalSpacing
return stackView
}()
private func viewTapped() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
self.view.addGestureRecognizer(tapGesture)
}
#objc func handleTap(sender: UITapGestureRecognizer) {
UIView.animate(withDuration: 1.1, delay: 0, usingSpringWithDamping: 5.1, initialSpringVelocity: 5.0, options: .curveEaseIn, animations: {
self.stackView.frame.origin.y = self.stackView.frame.origin.y + 130
self.textFieldLogin.resignFirstResponder()
self.textFieldSenha.resignFirstResponder()
}, completion: nil)
}
}
extension LoginViewController: UITextFieldDelegate {
func textFieldDidBeginEditing(_ textField: UITextField) {
UIView.animate(withDuration: 1.1, delay: 0, usingSpringWithDamping: 5.1, initialSpringVelocity: 5.0, options: .curveEaseIn, animations: {
self.stackView.frame.origin.y = self.stackView.frame.origin.y - 130
}, completion: nil)
}
func textFieldDidEndEditing(_ textField: UITextField) {
UIView.animate(withDuration: 1.1, delay: 0, usingSpringWithDamping: 5.1, initialSpringVelocity: 5.0, options: .curveEaseIn, animations: {
self.stackView.frame.origin.y = self.stackView.frame.origin.y + 130
}, completion: nil)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
I used the following code into textFieldDidBeginEditing and everything works fine! Thank you all.
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded() //Think I forget this method
self.stackView.frame.origin.y = self.stackView.frame.origin.y - 130
}
I am trying to animate a view on screen in a similar way to how action sheets appear.
My router presents CustomCardViewController which has an overlay background.
After a short delay I'd like containerView too animate into view from the bottom.
Instead what is happening however is it just appears in place. There is no animation between the transition.
final class CustomCardViewController: UIViewController {
private let backgroundMask: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .init(white: 0, alpha: 0.3)
return view
}()
private lazy var containerView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
view.transform = .init(translationX: 0, y: view.frame.height)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
backgroundMask.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTapToDismiss)))
modalPresentationStyle = .overFullScreen
[backgroundMask, containerView].forEach(view.addSubview(_:))
NSLayoutConstraint.activate([
backgroundMask.topAnchor.constraint(equalTo: view.topAnchor),
backgroundMask.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backgroundMask.bottomAnchor.constraint(equalTo: view.bottomAnchor),
backgroundMask.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 24),
containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -24),
containerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -24),
containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200)
])
UIView.animate(withDuration: 5, delay: 0.33, options: .curveEaseOut, animations: {
self.containerView.transform = .identity
}, completion: nil)
}
}
private extension CustomCardViewController {
#objc func onTapToDismiss() {
dismiss(animated: false, completion: nil)
}
}
You need to call your animation block from another lifecycle method. You can't trigger this from viewDidLoad as the view has only loaded, there is nothing on screen yet.
Try using viewDidAppear
For animation, you need to update the constant of the constrain that you want to animate. Here, since you're trying to animate from the bottom, you need to update a vertical constrain in this case, the bottom constraint. Here's the code for animation:
final class CustomCardViewController: UIViewController {
//..
override func viewDidLoad() {
super.viewDidLoad()
NSLayoutConstraint.activate([
// remove bottom constraint from here
])
containerViewBottomConstraint = containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 250)
containerViewBottomConstraint?.isActive = true
}
var containerViewBottomConstraint: NSLayoutConstraint? // declare bottom constraint
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
containerViewBottomConstraint?.constant = -24
UIView.animate(withDuration: 5, delay: 0.33, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
})
}
}
I have extended UIView:
class A: UIView {
private var _height: NSLayoutConstraint!
private var _centerLabel: UILabel = {
let lb = UILabel()
lb.text = "Some text"
lb.adjustsFontSizeToFitWidth = true
lb.textAlignment = .center
lb.translatesAutoresizingMaskIntoConstraints = false
return lb
}()
func open() {
self._height.constant = 50
self.superview?.layoutIfNeeded()
}
func close() {
UIView.animate(withDuration: 1, delay: 1, options: .curveLinear, animations: {
self._height.constant = 0
self.superview?.layoutIfNeeded()
}, completion: nil)
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(_centerLabel)
_height = self.heightAnchor.constraint(equalToConstant: 0)
NSLayoutConstraint.activate([
_centerLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16),
_centerLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16),
_centerLabel.topAnchor.constraint(lessThanOrEqualTo: self.topAnchor, constant: 16),
_centerLabel.bottomAnchor.constraint(greaterThanOrEqualTo: self.bottomAnchor, constant: -16),
_height
])
}
}
MainViewController:
class MainViewController: UIViewController {
#IBOutlet weak var a: A!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func open(_ sender: Any) {
a.open()
}
#IBAction func close(_ sender: Any) {
a.close()
}
}
If I call close(), instance of UIView A animates correctly, all other constraint attached to it as well. However, content of A (_centerLabel) disappears immediatelly, its height is not animated at all. Why?