UIView animating to wrong position after being scaled while using constraints - ios

So I'm trying to animate an UILabel to match another UILabel size and position. At first I was only working with the animation of the position part using constraints like this:
private lazy var constraintsForStateA: [NSLayoutConstraint] = [
firstLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
firstLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
]
private lazy var constraintsForStateB: [NSLayoutConstraint] = [
firstLabel.leadingAnchor.constraint(equalTo: secondLabel.leadingAnchor),
firstLabel.topAnchor.constraint(equalTo: secondLabel.topAnchor)
]
So I basically have two arrays with these constraints above (constraintsForStateA and constraintsForStateB), and when I need to animate the position of my firstLabel I just do something like:
// From state A to state B
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
NSLayoutConstraint.deactivate(self._constraintsUnselected)
NSLayoutConstraint.activate(self._constraintsSelected)
self.layoutIfNeeded()
} completion: { _ in
self.firstLabel.alpha = 0
self.secondLabel.alpha = 1
}
}
So far this has been working exactly as I was expecting, but the part with the size is giving me some trouble, since I can't apply the same strategy to the text size I'm using a transform like this:
let scaleX = secondLabel.bounds.width / firstLabel.bounds.width
let scaleY = secondLabel.bounds.height / firstLabel.bounds.height
and then in the same .animate method above from state A to state B I do this:
firstLabel.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
and from state B to state A:
firstLabel.transform = .identity
Which works as I want but the problem I'm facing is that the position is no longer in the expected place. I think this is happening because the transformation is happening having in consideration the anchorPoint at the center of the label. I've tried sort of blindly making it work changing the anchorPoint to be at (0,0), but it's not working either. I've lost 2 days on this already, any help or guidance is welcome!

When you animate, it would be much simpler if you just forget about the constraints and just deal with frames:
NSLayoutConstraint.deactivate(self.constraintsForStateA)
firstLabel.translatesAutoresizingMaskIntoConstraints = true
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
firstLabel.frame = secondLabel.frame
}
Then to transition from state B to A:
firstLabel.translatesAutoresizingMaskIntoConstraints = false
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
NSLayoutConstraint.activate(self.constraintsForStateA)
self.layoutIfNeeded()
}

Related

iOS - Exchanging position of two UIViews inside Animation block

I've been having a strong headache with this problem, because I know for a fact should be a simple solution but I can't find the right answer.
The idea is that I have two UIViews (let's call them A and B). All I want to do is to swap their positions on button press, by the A view going down to B position, and B view going up to A position.
Here's what I'm trying to achieve.
I'm trying to achieve that by just swapping the .center of the elements that I need (as A and B are both UIViews that cointains a bunch of elements inside them). This is inside an animation block - UIView.animate
However, what is currently happening is that the animation is performing in exactly the opposite way: The A view would go up the screen, then re-appear in the bottom side of the screen and end up in the desired position (B initial position). Same thing with the B view, it's currently going all the way to the bottom of the screen, then re-appear at the top, and finish the animation in the desired position (A initial position).
This is what is currently happening.
This is my code so far (note that ownAccountTransferView is defined in another file, and below code is placed in my UIViewController that stores that view). I call this function on button press, after swapping the the data (like labels and such) between the two cards.
fileprivate func performSwitchAnimation() {
// NOTE: ownAccountTransferView is a reference to my view, think of it like self.view
let fromAccountCardCenter =
self.ownAccountTransferView.fromAccountCardView.center
let fromAccountTypeLabelCenter =
self.ownAccountTransferView.fromAccountTypeLabel.center
let fromAccountTypeViewCenter =
self.ownAccountTransferView.fromAccountTypeView.center
let fromAccountBalanceLabelCenter =
self.ownAccountTransferView.fromAccountBalanceLabel.center
let fromAccountNameLabelCenter =
self.ownAccountTransferView.fromAccountNameLabel.center
let fromAccountChevronCenter =
self.ownAccountTransferView.fromAccountChevronView.center
let toAccountCardCenter =
self.ownAccountTransferView.toAccountCardView.center
let toAccountTypeLabelCenter =
self.ownAccountTransferView.toAccountTypeLabel.center
let toAccountTypeViewCenter =
self.ownAccountTransferView.toAccountTypeView.center
let toAccountBalanceLabelCenter =
self.ownAccountTransferView.toAccountBalanceLabel.center
let toAccountNameLabelCenter =
self.ownAccountTransferView.toAccountNameLabel.center
let toAccountChevronCenter =
self.ownAccountTransferView.toAccountChevronView.center
UIView.animate(withDuration: 1, delay: 0, options: []) {
self.ownAccountTransferView.switchAccountsButton.isUserInteractionEnabled = false
self.ownAccountTransferView.fromAccountCardView.center = toAccountCardCenter
self.ownAccountTransferView.fromAccountTypeLabel.center = toAccountTypeLabelCenter
self.ownAccountTransferView.fromAccountTypeView.center = toAccountTypeViewCenter
self.ownAccountTransferView.fromAccountBalanceLabel.center = toAccountBalanceLabelCenter
self.ownAccountTransferView.fromAccountNameLabel.center = toAccountNameLabelCenter
self.ownAccountTransferView.fromAccountChevronView.center = toAccountChevronCenter
self.ownAccountTransferView.toAccountCardView.center = fromAccountCardCenter
self.ownAccountTransferView.toAccountTypeLabel.center = fromAccountTypeLabelCenter
self.ownAccountTransferView.toAccountTypeView.center = fromAccountTypeViewCenter
self.ownAccountTransferView.toAccountBalanceLabel.center = fromAccountBalanceLabelCenter
self.ownAccountTransferView.toAccountNameLabel.center = fromAccountNameLabelCenter
self.ownAccountTransferView.toAccountChevronView.center = fromAccountChevronCenter
} completion: { isDone in
if isDone {
self.ownAccountTransferView.switchAccountsButton.isUserInteractionEnabled = true
}
}
}
So, how can I make the animation work the way I want - A view going to the bottom and B view going to the top?
Thanks in advance.
UPDATE: I fixed it by ussing transform. Tried messing around with animating constraints A LOT, tried every possible interaction between top/bottom constraints, but it would either not do anything at all or just bug my view and send it to hell. I even was insisting with animating constraints in support calls with my coworkers. And so were they.
However, after googling despair measures, I ended up using viewController.myView.myLabel.transform = CGAffineTransform(translationX: 0, y: 236).
Of course, you can reduce this to: view.transform = CGAffineTransform(translationX: 0, y: 236).
The value is arbitrary, and I'm not sure if it can some day break or cause undesired behaviour. I tested it on iPhone 8 and iPhone 13, and in both the animations performs just ok (A view goes down, B goes up, click switch and do the opposite, etc...).
Side note: If you gonna use this, you need to NOT copy/paste the values when you need your views to "come back" to where they were at the beginning. I used 236 and -236 for A and B respectively, but to restore them I need to use -2 and 2 or it super bugs out. Idk why tho.
But it works!
Thanks all
Enclose both the views in a parent view and do the following:
if let firstIndex = view.subviews.firstIndex(of: view1), let secondIndex = view.subviews.firstIndex(of: view2) {
let firstViewFrame = view1.frame
UIView.animate(withDuration: 1, animations: {
self.view1.frame = self.view2.frame
self.view2.frame = firstViewFrame
self.view.exchangeSubview(at: firstIndex, withSubviewAt: secondIndex)
})
}
Check out the answer here.
EDIT: I was able to achieve the said animation using Autolayout constraints. Basically what I did was have fixed constraints for two views initially. And have two sets of changing constraints - startConstraints and endConstraints. Simply activating and deactivating the constraints did the job. Check out the demo. Here's my code:
class ViewController: UIViewController {
var view1: UIView = {
let v = UIView()
v.backgroundColor = .red
return v
}()
var view2: UIView = {
let v = UIView()
v.backgroundColor = .blue
return v
}()
var button: UIButton = {
let b = UIButton()
b.setTitle("Animate", for: .normal)
b.setTitleColor(.black, for: .normal)
return b
}()
var fixedConstraints: [NSLayoutConstraint]!
var startConstraints: [NSLayoutConstraint]!
var endConstraints: [NSLayoutConstraint]!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(view1)
view.addSubview(view2)
view.addSubview(button)
view1.translatesAutoresizingMaskIntoConstraints = false
view2.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
fixedConstraints = [
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
view1.centerXAnchor.constraint(equalTo: view.centerXAnchor),
view2.centerXAnchor.constraint(equalTo: view.centerXAnchor),
view1.heightAnchor.constraint(equalToConstant: 100),
view1.widthAnchor.constraint(equalToConstant: 100),
view2.heightAnchor.constraint(equalToConstant: 100),
view2.widthAnchor.constraint(equalToConstant: 100),
]
startConstraints = [
view1.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: view2.bottomAnchor),
]
startConstraints.append(contentsOf: fixedConstraints)
NSLayoutConstraint.activate(startConstraints)
button.addTarget(self, action: #selector(animateSwitchingOfViews), for: .touchUpInside)
}
#objc
private func animateSwitchingOfViews(){
endConstraints = [
view2.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0),
view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo:view1.bottomAnchor)
]
endConstraints.append(contentsOf: fixedConstraints)
UIView.animate(withDuration: 2, animations: {
NSLayoutConstraint.deactivate(self.startConstraints)
NSLayoutConstraint.activate(self.endConstraints)
self.view.layoutIfNeeded()
})
}
}
UPDATE: I fixed it by ussing transform. Tried messing around with animating constraints A LOT, tried every possible interaction between top/bottom constraints, but it would either not do anything at all or just bug my view and send it to hell. I even was insisting with animating constraints in support calls with my coworkers. And so were they. However, after googling despair measures, I ended up using viewController.myView.myLabel.transform = CGAffineTransform(translationX: 0, y: 236).
Of course, you can reduce this to: view.transform = CGAffineTransform(translationX: 0, y: 236). The value is arbitrary, and I'm not sure if it can some day break or cause undesired behaviour. I tested it on iPhone 8 and iPhone 13, and in both the animations performs just ok (A view goes down, B goes up, click switch and do the opposite, etc...).
Side note: If you gonna use this, you need to NOT copy/paste the values when you need your views to "come back" to where they were at the beginning. I used 236 and -236 for A and B respectively, but to restore them I need to use -2 and 2 or it super bugs out. Idk why tho. But it works! Thanks all
Assuming you want to "swap the centers" and you don't want to modify the constraints / constants, you can use transforms.
Instead of hard-coding values, though, you can do it like this:
// if the views are in their "original" positions
if ownAccountTransferView.transform == .identity {
let xOffset = fromAccountTypeView.center.x - ownAccountTransferView.center.x
let yOffset = fromAccountTypeView.center.y - ownAccountTransferView.center.y
UIView.animate(withDuration: 1.0, animations: {
self.ownAccountTransferView.transform = CGAffineTransform(translationX: xOffset, y: yOffset)
self.fromAccountTypeView.transform = CGAffineTransform(translationX: -xOffset, y: -yOffset)
})
} else {
UIView.animate(withDuration: 1.0, animations: {
self.ownAccountTransferView.transform = .identity
self.fromAccountTypeView.transform = .identity
})
}

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

iOS: jumping uiview when rotating via CATransform3DRotate

I try to rotate a uiview (height & width of 100) with an angle of 90 degrees with an own anchor point. After changing the anchor point I translate my view so that both changes cancel each other out.
When I rotate the view with (Double.pi) it animates as expected but when I change to (Double.pi/2) the view is jumping up.
testView.layer.anchorPoint = CGPoint(x: 0.5, y: 1)
testView.layer.transform = CATransform3DMakeTranslation(0, -50, 0)
UIView.animate(withDuration: 2.3, delay: 0, options: .curveLinear, animations: { () -> Void in
var allTransofrom = self.testView.layer.transform
var c3d = CATransform3DIdentity
c3d.m34 = 2.5 / (2000)
let rot = CATransform3DRotate(c3d, CGFloat(Double.pi/2), 1, 0, 0)
allTransofrom = CATransform3DConcat(allTransofrom, rot)
self.testView.layer.transform = allTransofrom
}, completion: {(finished) in
} )
full code here
I was able to found a solution by myself but
I still don't know where the jump is coming from.
Instead of doing a translation via CGAffineTransform.translatedBy or CATransform3DMakeTranslation I simply adapted my NSLayoutConstraint.
testViewAnchors[0].constant = testViewAnchors[0].constant + CGFloat(50)
In my case an animation for the "translation" wasn't necessary but as it is possible to animate NSLayoutConstraints it shouldn't be a problem.

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

shrink width of button with animation issue

I want to replicate this animation in my project
So what I did try is:
To transform scale X .. but the problem is it also shrinks the title of a button.
self.transform = CGAffineTransform.identity
UIView.animate(withDuration: duration, delay: delayTime,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.7,
options: [.curveEaseIn],
animations: {
self.transform = CGAffineTransform(scaleX: 0.5, y: 1.0)
}, completion: nil)
This is what it gives (title of button also shrinks)
Use of CASpringAnimation
let shrinkAnim = CASpringAnimation(keyPath: "bounds.size.width")
shrinkAnim.damping = 0.7
shrinkAnim.initialVelocity = 0.7
shrinkAnim.fromValue = frame.width
shrinkAnim.toValue = width
shrinkAnim.dura[![enter image description here][3]][3]tion = duration
shrinkAnim.timingFunction = getTimingFunction(curve: curve)
shrinkAnim.fillMode = kCAFillModeForwards
shrinkAnim.isRemovedOnCompletion = false
layer.add(shrinkAnim, forKey: shrinkAnim.keyPath)
So it can change the width but also position of the title
So my question is whats going wrong or what I need to add to replicate first image?
My constraints for button is pinned to left, right and bottom edges and fix height. and more thing is I am making a class for this so I cant change constants because I have to use this in many screens .. so I want one stop solution.
Don't use CGAffineTransform, rather take and NSLayoutConstraint attribute outlet for Button's width and change its constant inside animation block/closure.
UIView.animate(withDuration: duration, delay: delayTime,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.7,
options: [.curveEaseIn],
animations: {
self.buttonWidthConstraint.constant = desired_width_here
}, completion: nil)
Put your button inside a UIView
your view tree would then look like this
Superview > UIView > Button
By doing this you now have a fixed width size your button can follow which is the uiview
It would look like this
And then animate using
//let's say the current left and right constraint are 8
leftConstraint.constant = 50
rightConstraint.constant = 50
UIView.animate ... {
self.view.layoutIfNeeded()
}
By using this you'd only need to set the UIView's frame to your desired frame and have the button follow suit and your animations would just be set proportionate to how you'd code it
//let's say the current left and right constraint are 8
// get view frame
...
// calculate distance
var calculatedDistance = ......
// set distance
let distanceToAnimate = calculatedDistance
leftConstraint.constant = distanceToAnimate
rightConstraint.constant = distanceToAnimate
UIView.animate ... {
self.view.layoutIfNeeded()
}
Have you tried content content hugging and compression resistance. Can you please try doing below at highest priorty.
Apply the transform on the frame of the button. When you scale an UIView in animation it does not take consideration of subviews/layout it will just uniformly scale whatever is being drawn.
self.transform = CGAffineTransform.identity
UIView.animate(withDuration: duration, delay: delayTime,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.7,
options: [.curveEaseIn],
animations: {
self.frame = CGRectMake(<#CGFloat x#>, <#CGFloat y#>, <#CGFloat width#>, <#CGFloat height#>)
}, completion: nil)

Resources