Animate Constraint Relative to Another One - ios

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()
})
}

Related

NSLayoutConstraint animation glitch

I've decided to switch to constraints and face up with animation problems. In autoresizing mask world everything works fine. UIView is attached to right top. Content inside use autoresizing mask.
Animate code:
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) {
self.constraints.first(where: {$0.firstAttribute == .height})?.constant = dstSize.height
self.constraints.first(where: {$0.firstAttribute == .width})?.constant = dstSize.width
self.setNeedsLayout()
self.layoutIfNeeded()
}
You need to move constraint's change outside the animation block
self.constraints.first(where: {$0.firstAttribute == .height})?.constant = dstSize.height
self.constraints.first(where: {$0.firstAttribute == .width})?.constant = dstSize.width
UIView.animate(withDuration: 0.5, delay: 0, options: [.curveEaseInOut]) {
self.superView!.layoutIfNeeded()
}
Update
In your Github attached code you need to re-layout the main view not the container
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut) {
self.view.layoutIfNeeded()
}
A few things:
It's good practice to make properties for the constraints you want to update later;
Only the layoutIfNeeded call have to be inside an animation block;
It looks like setNeedsLayout is unnecessary. Have you tried to remove it?

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

Constraint is not equal to the height

I write this following code :
if !self.headerIsCollapsed{
self.headerIsCollapsed = true
self.heightProfilView.constant -= (self.view.bounds.height / 5)
UIView.animate(withDuration: 0.3, animations: {
self.imageUserProfil.layer.opacity = 0
self.view.layoutIfNeeded()
})
print(self.heightProfilView.constant)
print(self.topUserProfilView.bounds.height)
}
My question is: Why the value of the two print() is not the same ?
I need the value of self.topUserProfilView.bounds.height Before animate this for another function, would there be another way to catch this value before?
thanks
try this: it should work
if !self.headerIsCollapsed{
self.headerIsCollapsed = true
self.heightProfilView.constant -= (self.view.bounds.height / 5)
UIView.animate(withDuration: 0.3, animations: {
self.imageUserProfil.layer.opacity = 0
self.view.layoutIfNeeded()
}) { (isCompleted) in
print(self.heightProfilView.constant)
print(self.topUserProfilView.bounds.height)
}
}
P.S: This solution would not work if you added the heightProfilView constraint relative to the any view height as here you are changing the constant but in the IB you have assigned the multiplier so in that case the default constant value is 0. So, this approach will not work for the relative height constraint. as we can't change the multiplier of a constraint.
Maybe you have constraints taking priority. Check out your console output when you collapse the row, there should be some loud output involving constraints.
Also try hiding imageUserProfil
self.imageUserProfil.layer.opacity = 0
self.imageUserProfil.isHidden = true
You need completion block to do it...Just replace your animation method as below
UIView.animate(withDuration: 0.3, animations: {
self.imageUserProfil.layer.opacity = 0
self.view.layoutIfNeeded()
}) { (isDone) in
print(self.heightProfilView.constant)
print(self.topUserProfilView.bounds.height)
}
Hope it will work.

UIView animation with autolayout and child views

I have a weird problem that seems to be fairly easy to solve. I think I could use some workaround to be able to have the behavior that I want but I want to know if there is a better way to do it.
I have a view called contentView (blue in the image) that it will expand its height using a UIView.animation, this is no problem at all and works as expected.
The problem here is that this view has a child component (a button) that has an autolayout constraint to its bottom equal to 22 like this:
This is the autolayout constraint:
If I do the height resize without the animation it works fine, the view height change and the button are always 22 points of the bottom of the contentView. But when I use an animation to make this change more smoothy and user-friendly the button moves to the end position before the animation even start.
I want to know how I could achieve a smooth animation here but with the button moving along its parent view
The part of the code that handles the animation is pretty straightforward but I'll post it in here:
#IBAction func openDetail(_ sender: ExpandCourseDetail) {
let rotation = sender.getOpen() ? CGAffineTransform.identity : CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 0.5, delay: 0.1, options: [.curveEaseInOut], animations: {
sender.transform = rotation
}, completion: {
success in
sender.setOpen(!sender.getOpen())
})
UIView.animate(withDuration: 1.0, delay: 0.5, options: [.curveEaseInOut], animations: {
self.contentView.frame.size.height = sender.getOpen() ? self.contentView.frame.height - 300 : self.contentView.frame.height + 300
}, completion: nil)
}
As a side note, the button itself has an animation that rotates the button 180 degrees to show the user that the view is expanding.
Thank you so much for your help.
It's super easy with constraints, just create a superView height constraint IBOutlet and change its constant value.
#IBAction func btnPressed(_ sender: UIButton) {
self.toggleButton.isSelected = !sender.isSelected
//Animation starts here
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.7) {
if self.toggleButton.isSelected {
//transform button
self.toggleButton.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
//change your ParentView height to desired one
self.constContentViewHeight.constant = self.view.frame.size.height - 220
self.view.layoutIfNeeded()
} else {
self.toggleButton.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi*2))
// Set height constraint to original value
self.constContentViewHeight.constant = 250
self.view.layoutIfNeeded()
}
}
}
I have created a demo, check it out.
The issue you are facing is due to two animation blocks. So I have changed some lines and put both button transformation and height animation into one animation block.
func openDetail(_ sender: ExpandCourseDetail) {
let isOpen = sender.getOpen() ? CGAffineTransform.identity : CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 1.0, delay: 0.5, options: [.curveEaseInOut], animations: {
sender.transform = rotation
self.contentView.frame.size.height = self.contentView.frame.height + (isOpen ? 300 : -300)
}, completion: { (success) in
sender.setOpen(!isOpen)
})
}

Animate DatePicker hide/show in StackView

How to properly animate datePicker appearance/disappearance in stackView? Currently I tried like this:
UIView.animateWithDuration(0.3, animations: {
self.datePickerView.hidden = !self.datePickerView.hidden
})
This causes problems with hiding animation - it starts nicely and then in the end datePickerView flashes a little bit at the top of where datePicker was. Any suggestions?
I had the same issue and solved it this way:
Put your Picker in a view (we will call it pickerContainerView)
Set a 216 height constraint to your pickerContainerView (picker default height)
Set the constraint priority to 999 to quiet "UISV-hiding" constraint warning
Add "leading", "trailing" and "center vertically" constraints from your picker to the pickerContainerView
animate hide of the pickerContainerView :
Swift 2
UIView.animateWithDuration(0.3, animations: {
self.pickerContainerView.hidden = !self.pickerContainerView.hidden
})
Swift 3, 4, 5
UIView.animate(withDuration: 0.3, animations: {
self.pickerContainerView.isHidden = !self.pickerContainerView.isHidden
})
Using a container to hold the picker and setting clipsToBounds = true worked for me.
I'm using PureLayout, but it should work with IB too.
startRangePickerContainer = UIView()
startRangePickerContainer.clipsToBounds = true
startRangePickerContainer.backgroundColor = UIColor.cyan
stackView.addArrangedSubview(startRangePickerContainer)
startRangePickerContainer.autoPinEdge(toSuperviewEdge: .leading)
startRangePickerContainer.autoSetDimension(.height, toSize: 216)
startRangePickerContainer.autoPinEdge(toSuperviewEdge: .leading)
startRangePickerContainer.autoPinEdge(toSuperviewEdge: .trailing)
startRangePicker = UIDatePicker()
startRangePickerContainer.addSubview(startRangePicker)
startRangePicker.autoCenterInSuperview()
To animate:
UIView.animate(withDuration: 0.3, animations: {
self.startRangePickerContainer.isHidden = !self.startRangePickerContainer.isHidden
})

Resources