Animate specific constraint changes by layoutIfNeeded() - ios

I have programmatically constructed the views and then animating them. There is a chain of view animation and the last one is the button to be animated.
Whilst every thing is working fine as soon as I animate the button by changing the constraints(these constraints are independent of others and no other view has any hinged to it) by calling layoutIfNeeded(), all the views changes and go to their initial position of the animation.
I even tried bunch of other methods such as changing alpha values etc or making it disappear. but every single time, it changes the whole view.
Now, I am not able to figure out the exact cause. Below is the code of it.
Is there a way to change just a specific constraint with layoutIfNeeded() or any other way to handle this functionality?
import UIKit
var nextButton: UIButton!
var nextButtonHeightConstraint: NSLayoutConstraint?
var nextButtonWidthConstraint: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
addNextButton()
}
// ..... function trigerred here.
#IBAction func triggerChange() {
performCaptionAnimation()
}
func addNextButton() {
nextButton = UIButton()
nextButton.translatesAutoresizingMaskIntoConstraints = false
nextButton.clipsToBounds = true
nextButton.setTitle("Next", for: .normal)
nextButton.backgroundColor = .white
nextButton.isUserInteractionEnabled = true
nextButton.titleLabel!.font = AvernirNext(weight: .Bold, size: 16)
nextButton.setTitleColor(.darkGray, for: .normal)
nextButton.roundAllCorners(withRadius: ActionButtonConstraint.height.anchor/2)
nextButton.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(nextButtonPressed)))
view.addSubview(nextButton)
nextButtonHeightConstraint = nextButton.heightAnchor.constraint(equalToConstant: ActionButtonConstraint.height.anchor)
nextButtonWidthConstraint = nextButton.widthAnchor.constraint(equalToConstant: ActionButtonConstraint.width.anchor)
NSLayoutConstraint.activate([
nextButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: ActionButtonConstraint.right.anchor),
nextButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: ActionButtonConstraint.bottom.anchor),
nextButtonHeightConstraint!, nextButtonWidthConstraint!
])
}
func performCaptionAnimation() {
UIView.animate(withDuration: 0.4, delay: 0.0, options: [.curveEaseInOut], animations: {
self.bannerTwoItemContainer.alpha = 1
self.bannerTwoItemContainer.frame.origin.x -= (self.bannerTwoItemContainer.frame.width/2 + 10)
}) { (done) in
if done {
UIView.animate(withDuration: 0.4, delay: 0.0, options: [.curveEaseInOut], animations: {
self.bannerOneItemContainer.alpha = 1
self.bannerOneItemContainer.frame.origin.x += self.bannerOneItemContainer.frame.width/2 + 10
}) { (alldone) in
if alldone {
// All changes here ......................
self.changeNextButtonContents()
}
}
}
}
}
// ------ This animation has issues and brings all the animation to it's initial point
func changeNextButtonContents() {
self.nextButtonHeightConstraint?.constant = 40
self.nextButtonWidthConstraint?.constant = 110
UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: [.curveEaseIn], animations: {
self.nextButton.setTitleColor(.white, for: .normal)
self.nextButton.setTitle("Try Now", for: .normal)
self.nextButton.titleLabel?.font = AvernirNext(weight: .Bold, size: 18)
self.nextButton.backgroundColor = blueColor
self.nextButton.roundAllCorners(withRadius: 20)
self.view.layoutIfNeeded()
}, completion: nil)
}

The problem is that your first animations change their view’s frame, as in self.bannerTwoItemContainer.frame.origin.x. But you cannot change the frame of something that is positioned by constraints. If you do, then as soon as layout occurs, the constraints reassert themselves. That is exactly what you are doing and exactly what happens. You say layoutIfNeeded and the constraints, which you never changed, are obeyed.
Rewrite your first animations to change their view’s constraints, not their frame, and all will be well.

Related

UIButton with shadow rotating without shadow rotating

Currently I have a button that draws its properties from a UIButton extension.
func mapControllerButtons(image: String, selector: Selector) -> UIButton {
let button = UIButton()
button.setImage(UIImage(named: image)!.withRenderingMode(.alwaysOriginal), for: .normal)
button.layer.shadowColor = UIColor.black.cgColor
button.layer.shadowOffset = CGSize(width: 0.0, height: 3.0)
button.layer.shadowRadius = 5
button.layer.shadowOpacity = 0.5
button.layer.masksToBounds = false
button.addTarget(self, action: selector, for: .touchUpInside)
return button
}
}
This creates a shadow underneath the button, however I have another function that rotates the button around, to signal to the user that data is being loaded from an API.
Currently what I am doing is creating the button with a shadow and overlaying it with a button that does not have a shadow. The shadowless button is the one that will be tapped and rotate.
I have a gut feeling that there's gotta be a better way of doing this than creating two buttons having one eat memory just so it can be a placeholder (for the shadow effect).
How can I go about creating a button that can rotate with animateWith func while the shadow stays in its place?
Here is my animation code block.
func animateRefreshButton() {
UIView.animate(withDuration: 1.0, delay: 0.0, options: .repeat, animations: {
self.refreshButton.transform = CGAffineTransform(rotationAngle: .pi)
})
}
You could try rotating the button's imageView instead of the whole button:
func animateRefreshButton() {
UIView.animate(withDuration: 1.0, delay: 0.0, options: .repeat, animations: {
self.refreshButton.imageView?.transform = CGAffineTransform(rotationAngle: .pi)
})
}

UITapGestureRecognizer doesn't work after resizing UIView

I want to programmatically move and expand the size of a UIView on tap and then, if the UIView is tapped again, animate it back to its original size and position.
I can get the UIView to expand and move on the first tap. However, taps aren't registered at all after that initial animation.
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:)))
myView.heightAnchor.constraint(equalToConstant: 50).isActive = true
myView.widthAnchor.constraint(equalToConstant: 50).isActive = true
myView.isUserInteractionEnabled = true
myView.addGestureRecognizer(tapRecognizer)
Below is my function that is called by handleTap to perform the initial animation on the first tap. It runs fine. I translate the position of myView and then change the frame size to make it bigger.
fileprivate func expandView(_ sender: UITapGestureRecognizer) {
UIView.animate(withDuration: 0.5, animations: {
let translate = CGAffineTransform(translationX: 100, y: 50 )
sender.view?.transform = translate
sender.view?.backgroundColor = .green
}) { (_) in
UIView.animate(withDuration: 0.5, animations: {
// change frame size of view to make it bigger
sender.view?.frame = CGRect(x: 100, y: 50, width: 100, height: 100)
})
}
}
I tried commenting out the animation that changes the frame size, leaving only the XY translation. That also doesn't allow handleTap to fire again when tapping myView.

Animate multiple UIViews alpha

I have multiple labels placed on scroll view like:
private func drawDates(_ multiplier: Double){
contentView.subviews.forEach{it in
it.removeFromSuperview()
}
let maximumVisibleDates = 6.0
let dateLabelOffset = Double(UIScreen.main.bounds.size.width / CGFloat(maximumVisibleDates))
let doublePoints = data.findClosestDatePointsForInterval(interval: dateLabelOffset,
multiplier: multiplier)
print(doublePoints)
let datesToShow = data.datesForPoints(points: doublePoints, multiplier: multiplier)
for (index, value) in datesToShow.enumerated(){
if index == datesToShow.count - 1 {
continue
}
let label = UILabel.init(frame: CGRect.init(x: CGFloat(doublePoints[index]),
y: Dimensions.chartHeight.value + Offsets.small.value,
width: Dimensions.dateLabelWidth.value,
height: Dimensions.dateLabelHeight.value))
label.text = redableString(value)
label.font = UIFont.systemFont(ofSize: FontSizes.yLabel.value)
label.textColor = Settings.isLightTheme() ? Colors.LightTheme.textGray.value : Colors.DarkTheme.textGray.value
label.sizeToFit()
contentView.addSubview(label)
}
}
When i call:
contentView.subviews.forEach{it in
it.removeFromSuperview()
}
Actually i want to fade "old" labels from old function call and then add new labels animated. How to do that?
There are 3 ways to remove old view and add new view with animation.
Solution 1:
Set new view alpha to 0 and add it to super view.
Do animation.
Remove old view after animation.
In this case, use for loop in animations block to set alpha, and in completion block to remove old views.
let viewToAdd = ...
viewToAdd.alpha = 0
contentView.addSubview(viewToAdd)
UIView.animate(withDuration: 1, animations: {
viewToRemove.alpha = 0
viewToAdd.alpha = 1
}, completion: { (_) in
viewToRemove.removeFromSuperview()
})
Solution 2:
Call transition(with:duration:options:animations:completion:) method. Use .transitionCrossDissolve as options value.
In this case, use for loop in animations block to remove old views and add new views.
UIView.transition(with: contentView,
duration: 1,
options: .transitionCrossDissolve,
animations: {
viewToRemove.removeFromSuperview()
contentView.addSubview(viewToAdd)
}, completion: nil)
Solution 3:
If the number of old views is equal to the number of new views, call transition(from:to:duration:options:completion:) method. Use .transitionCrossDissolve as options value.
In this case, use for loop to call the method.
UIView.transition(from: viewToRemove,
to: viewToAdd,
duration: 1,
options: .transitionCrossDissolve,
completion: nil)
You could leverage the UIView animation API:
https://developer.apple.com/documentation/uikit/uiview/1622515-animatewithduration

Animate Constraint Relative to Another One

I setup an animation to hide one switch/label when another one is turned on. Simultaneously the switch that was just turned on move up. This works great with the simple explanation here.
However, when I try to move the switch/label back down after it is turned off it doesn't budge. The other switch reappears fine, but the top constraint change doesn't fire.
I'm relatively new to doing this type of setup and animating all programmatically and after spending an hour on this I'm stumped. Is it because I'm animating a top constraint relative to another one? How does that matter if it works the first time around? Even though the alpha of the hidden switch is set to zero, its frame is still there, right? Or am I doing something simple stupidly?
// Works Perfectly!
func hideVeg() {
self.view.layoutIfNeeded()
UIView.animate(withDuration: 1, delay: 0, options: [.curveEaseIn], animations: {
self.vegetarianSwitch.alpha = 0
self.vegetarianLabel.alpha = 0
self.veganSwitch.topAnchor.constraint(equalTo: self.vegetarianSwitch.bottomAnchor, constant: -30).isActive = true
self.view.layoutIfNeeded()
})
}
// Showing the label and switch works, but the topAnchor constraint never changes!
func showVeg() {
self.view.layoutIfNeeded()
UIView.animate(withDuration: 1, delay: 0, options: [.curveEaseIn], animations: {
self.vegetarianSwitch.alpha = 1
self.vegetarianLabel.alpha = 1
// This is the constraint that doesn't change.
// This is exactly what it was set to before the other hideVeg() runs.
self.veganSwitch.topAnchor.constraint(equalTo: self.vegetarianSwitch.bottomAnchor, constant: 40).isActive = true
self.view.layoutIfNeeded()
})
}
The issue here is that you are not modifying the constraints but actually creating new constraints with each animation. What you want to do instead is just create the constraint once (you can do it in code or in Interface Builder and drag and outlet). You can then just change the .constant field of the existing constraint in your animation block.
The constant needs to be changed with the animation, not creating an entirely new constraint. The old constraint still exists causing the issue.
var veganTopConstraint = NSLayoutConstraint()
// Top Constraint set up this way so it can be animated later.
veganTopConstraint = veganSwitch.topAnchor.constraint(equalTo: vegetarianSwitch.bottomAnchor, constant: 40)
veganTopConstraint.isActive = true
func hideVeg() {
UIView.animate(withDuration: 1, delay: 0, options: [.curveEaseIn], animations: {
self.vegetarianSwitch.alpha = 0
self.vegetarianLabel.alpha = 0
self.veganTopConstraint.constant = -30
self.view.layoutIfNeeded()
})
}
func showVeg() {
self.view.layoutIfNeeded()
UIView.animate(withDuration: 1, delay: 0, options: [.curveEaseIn], animations: {
self.vegetarianSwitch.alpha = 1
self.vegetarianLabel.alpha = 1
self.veganTopConstraint.constant = 40
self.view.layoutIfNeeded()
})
}

Animate the 'backgroundColor' for a UIButton in iOS using Swift3

I would like to animate the backgroundColor property of my UIButtons but I'm not sure where to begin. I've never used Core Animation, I'm not sure if I even need it for something so basic?
I've styled the buttons using a class and a method that looks something like this:
redTheme.swift
func makeRedButton() {
self.layer.backgroundColor = myCustomRedcolor
}
You can do it with UIView Animation as the following :
Note: Simply don't forget to set the background to clear before you perform the animation otherwise it won't work. That's because animation need a starting and ending point, starting point is clear color to Red. Or alpha 0 to alpha 1 or the other way around.
let button = UIButton(frame: CGRect(x: 0, y: 100, width: 50, height: 50))
button.layer.backgroundColor = UIColor.clear.cgColor
self.view.addSubview(button)
func makeRedButton() {
UIView.animate(withDuration: 1.0) {
button.layer.backgroundColor = UIColor.red.cgColor
}
}
Swift 4 - A simple way to animate any change on your UIButton or any other UIView:
UIView.transition(with: button, duration: 0.3, options: .curveEaseInOut, animations: {
self.button.backgroundColor = .red
self.button.setTitle("ERROR", for: .normal)
self.button.setTitleColor(.white, for: .normal)
})
For some reason animating button.layer.backgroundColor
had zero effect for my custom uibutton hence I animated alpha like so:
func provideVisualFeedback(_ sender:UIButton!)
{
sender.backgroundColor = UIColor.whatever
sender.alpha = 0
UIView .animate(withDuration: 0.1, animations: {
sender.alpha = 1
}, completion: { completed in
if completed {
sender.backgroundColor = UIColor.white
}
})
}
and then
#IBAction func buttonAction(_ sender:NumberPadButton!)
{
provideVisualFeedback(sender)
do your thing once animation was setup with the helper above

Resources