I'm using UIStackView and it contains 3 UIView instances, which has fixed height
I'm trying to hide these subviews by clicking button
first and second view show/hide well with proper animation
but last view doesn't animate
class ViewController: UIViewController {
private var flag: Bool = true
#IBOutlet weak var targetView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func buttonDidTapped(_ sender: Any) {
flag = !flag
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
self.targetView.isHidden = !self.flag
}
}
}
The issue is the way stack views change their frames when hiding an arranged subview.
Easiest way to see what's happening:
set your Green view to Alpha: 0.5
toggle .isHidden on the Blue view
You'll see that the 50% translucent Green view "slides up over" the Blue view... the Blue view does not "shrink in height" during the animation.
To solve your specific issue, set Clips To Bounds to true on your stack view. Now, when you toggle .isHidden on your Green view, the animation will look correct.
That will not change the "slide over" appearance if you have translucent views, but that's a different issue.
As a side note, you can simplify your code and get rid of the flag like this:
UIView.animate(withDuration: 0.5) {
// not needed
//self.view.layoutIfNeeded()
self.targetView.isHidden.toggle()
}
Try change your code from:
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
self.targetView.isHidden = !self.flag
}
to:
self.targetView.isHidden = !self.flag
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
Looks like you animate before change.
Related
I notice that, if I perform add/ expand animation within an UIScrollView, it will cause unwanted scrolling behavior, when the UIScrollView fill with enough content to become scroll-able.
As you can see in the following animation, initially, the add/ expand animation works just fine.
When we have added enough item till the UIScrollView scrollable, whenever a new item is added, and UIScrollView will first perform scroll down, and then scroll up again!
My expectation is that, the UIScrollView should remain static, when add/ expand animation is performed.
Here's the code which performs add/ expand animation.
Add/ expand animation
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
// Clear off horizontal swipe in animation caused by addArrangedSubview
stackView.superview?.layoutIfNeeded()
customView.show()
// Perform expand animation.
UIView.animate(withDuration: 1) {
self.stackView.superview?.layoutIfNeeded()
}
}
Here's the constraint setup of the UIScrollView & added custom view item
Constraint setup
Custom view
class CustomView: UIView {
private var zeroHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.cornerRadius = stackView.frame.height / 2
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
zeroHeightConstraint = self.safeAreaLayoutGuide.heightAnchor.constraint(equalToConstant: 0)
zeroHeightConstraint.isActive = false
}
func hide() {
zeroHeightConstraint.isActive = true
}
func show() {
zeroHeightConstraint.isActive = false
}
}
Here's the complete source code
https://github.com/yccheok/add-expand-animation-in-scroll-view
Do you have any idea why such problem occur, and we can fix such? Thanks.
Because of the way stack views arrange their subviews, animation can be problematic.
One approach that you may find works better is to embed the stack view in a "container" view.
That way, you can use the .isHidden property when adding an arranged subview, and allow the animation to update the "container" view:
The "add view" function now becomes (I added a Bool so we can skip the animation on the initial add in viewDidLoad()):
func addCustomView(_ animated: Bool) {
let customView = CustomView.instanceFromNib()
stackView.addArrangedSubview(customView)
customView.isHidden = true
if animated {
DispatchQueue.main.async {
UIView.animate(withDuration: 1) {
customView.isHidden = false
}
}
} else {
customView.isHidden = false
}
}
And we can get rid of all of the hide() / show() and zeroHeightConstraint in the custom view class:
class CustomView: UIView {
#IBOutlet weak var borderView: UIView!
#IBOutlet weak var stackView: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
borderView.layer.masksToBounds = true
borderView.layer.borderWidth = 1
}
override func layoutSubviews() {
super.layoutSubviews()
borderView.layer.cornerRadius = borderView.bounds.height * 0.5
}
}
Since it's a bit difficult to clearly show everything here, I forked your project with the changes: https://github.com/DonMag/add-expand-animation-in-scroll-view
Edit
Another "quirk" of animating a stack view shows up when adding the first arranged subview (also, when removing the last one).
One way to get around that is to add an empty view as the first subview.
So, for this example, in viewDidLoad() before adding an instance of CustomView:
let v = UIView()
stackView.addArrangedSubview(v)
This will make the first arranged subview a zero-height view (so it won't be visible).
Then, if you're implementing removing custom views, just make sure you don't remove that first, empty view.
If your stack view has .spacing = 0 noting else is needed.
If your stack view has a non-zero spacing, add another line:
let v = UIView()
stackView.addArrangedSubview(v)
stackView.setCustomSpacing(0, after: v)
I did a little research on this and the consensus was to update the isHidden and alpha properties when inserting a view with animations.
In CustomView:
func hide() {
alpha = 0.0
isHidden = true
zeroHeightConstraint.isActive = true
}
func show() {
alpha = 1.0
isHidden = false
zeroHeightConstraint.isActive = false
}
In your view controller:
#IBAction func add(_ sender: Any) {
let customView = CustomView.instanceFromNib()
customView.hide()
stackView.addArrangedSubview(customView)
self.stackView.layoutIfNeeded()
UIView.animate(withDuration: 00.5) {
customView.show()
self.stackView.layoutIfNeeded()
}
}
Also, the constraints in your storyboard aren't totally correct. You are seeing a red constraint error because autolayout doesn't know the height of your stackView. You can give it a fake height and make sure that "Remove at build time" is checked.
Also, get rid of your scrollView contentView height constraint defined as View.height >= Frame Layout Guide.height. Autolayout doesn't need to know the height, it just needs to know how subviews inside of the contentView stack up to define its vertical content size.
Everything else looks pretty good.
I really can't figure out what's wrong with my code. Let me explain in detail what was my aim:
I have two UIView in the same UIViewController. They're called "redSquare" and "greenSquare".
When the UIViewController is presented I want to animate the redSquare in order to move on the Y-axis till it reaches the top-border of the greenSquare.
This is how I set the xCode project:
The behavior that I got is completely the opposite and I really can't understand why and what's happening:
Any tips or explanations for this?
Okay. Part of the problem is that you're aligning the center Y... which means that you're trying to break constraints with your animation.
Another part of the problem is that you are doing your animation in viewDidLoad, which totally runs before viewWillAppear and viewDidAppear get called.
For my own animations, I usually animate a constraint.
That is, get rid of the center-Y constraint for your red box and add a new constraint putting the red box some Y distance from the bottom of the superview. Connect this new constraint to an outlet and then you can animate like this:
#IBOutlet weak var redYConstraint : NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// This line sets the red box to be in the center Y of the green box
self.redYConstraint.constant = self.greenSquare.frame.midY
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 3.0, delay: 2.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.redYConstraint.constant = self.greenSquare.frame.maxY
self.view.layoutIfNeeded()
}, completion: nil)
}
I'm experiencing a glitch where my UITextField's text jumps to its final position (it doesn't animate) when animating the textfield's width constraint. Take a look at this gif:
When the "Grow" button is tapped, the textfield's width grows. But "hello world" jumps immediately to the center instead of gliding there. When the "Shrink" button is tapped, "hello world" jumps immediately back to the left.
My animation function looks like this:
func animateGrowShrinkTextFields(grow: Bool) {
if grow {
UIView.animate(withDuration: 0.5, animations: {
self.widthConstraint.constant = 330
self.view.layoutIfNeeded()
}, completion: nil)
} else {
UIView.animate(withDuration: 0.5, animations: {
self.widthConstraint.constant = 100
self.view.layoutIfNeeded()
}, completion: nil)
}
}
I have tried the following list suggestions; none of them worked.
I called self.view.layoutIfNeeded() and self.helloWorldTextField.layoutIfNeeded() before and within the animation block as suggested in this answer: https://stackoverflow.com/a/32996503/2179970
I tried self.view.layoutSubviews and self.helloWorldTextField.layoutSubview as suggested in this answer: https://stackoverflow.com/a/30845306/2179970
Also tried setNeedsLayout() UITextField text jumps iOS 9
I even tried changing the font as suggested here: https://stackoverflow.com/a/35681037/2179970
I tried resignFirstResponder (although though I never tap or edit the textfield in my tests, so it should not involve the firstResponder) as suggested here: https://stackoverflow.com/a/33334567/2179970
I tried subclassing UITextField as seen here: https://stackoverflow.com/a/40279630/2179970
I also tried using a UILabel and got the same jumpy result.
The following question is also very similar to mine but does not have an answer yet: UITextfield text position not animating while width constraint is animated
Here is my project on Github: https://github.com/starkindustries/ConstraintAnimationTest
Solution Demo
I've found a working solution. It feels a little hackish but it works. Here is a gif of the final result. Notice that helloWorldTextField has a blue border to show its location within the second textfield behind it.
Instructions
Make two textfields: helloWorldTextField (the original from the question) and borderTextField (a new textfield). Remove helloWorldTextFields's border and background color. Keep borderTextField's border and background color. Center helloWorldTextField within borderTextField. Then animate the width of borderTextField.
Github link and Code
Here is the project on Github: https://github.com/starkindustries/ConstraintAnimationTest
Here is the code within MyViewController class. Everything else is setup in the storyboard which can be viewed on Github at the link above.
class MyViewController: UIViewController {
// Hello World TextField Border var
#IBOutlet weak var borderTextFieldWidth: NSLayoutConstraint!
// Button Vars
#IBOutlet weak var myButton: UIButton!
var grow: Bool = false
func animateGrowShrinkTextFields(grow: Bool, duration: TimeInterval) {
if grow {
UIView.animate(withDuration: duration, animations: {
self.borderTextFieldWidth.constant = 330
self.view.layoutIfNeeded()
}, completion: { (finished: Bool) in
print("Grow animation complete!")
})
} else {
UIView.animate(withDuration: duration, animations: {
self.borderTextFieldWidth.constant = 115
self.view.layoutIfNeeded()
}, completion: { (finished: Bool) in
print("Shrink animation complete!")
})
}
}
#IBAction func toggle(){
let duration: TimeInterval = 1.0
grow = !grow
let title = grow ? "Shrink" : "Grow"
myButton.setTitle(title, for: UIControlState.normal)
animateGrowShrinkTextFields(grow: grow, duration: duration)
}
}
Notes and References
What led me to this solution was #JimmyJames's comment: "You are just animating the UITextField width, but the content inside is not animated."
I researched how to animate font changes and came across this question: Is there a way to animate changing a UILabel's textAlignment?
In that question #CSmith mentioned that "you can animate the FRAME, not the textAlignment" https://stackoverflow.com/a/19251634/2179970
The accepted answer in that question suggests to use a UILabel within another frame. https://stackoverflow.com/a/19251735/2179970
Hope this helps anyone else who comes across this problem. If anyone has another way to solve this, please post a comment or another answer. Thanks!
Another solution for the issue is set yourLabel.contentMode = .center on init, and animate in animation block as usually
So this is what I am trying to do:
Here is the initial screen:
When the bottom pointing arrow is clicked there is a smaller view that transitions from bottom to top like this:
And as you can see the view in the back is dimmed. And when I click on the back view, the smaller sized view goes away by animating downwards.
There were several things that I tried:
1. I tried to segue modally which seemed to animate properly, namely, from bottom to top, but it covers the entire back view.
2. I tried to make the modal view only half the parent size by trying to replicate this post: Present modal view controller in half size parent controller. However, it did not work.
3. So I decided to put a UIView on top of my back view like so:
And I connected the grey colored view with the #IBOutlet weak var messageView: UIView!. And I tried using this code: UIView.transitionWithView(messageView, duration: 1.0, options: UIViewAnimationOptions.CurveEaseIn, animations: nil, completion: nil). However, nothing seems to be happening. Any suggestions on how to accomplish this?
For the Animation of button:
Add autolayout constraint to the BottomLayout Guide.And create an IBOutlet as
#IBOutlet weak var bottomConstraint: NSLayoutConstraint!
var shouldAnimateView:Bool = true
On the Action of the button you need to animate the View using constraint
#IBAction func showOrHideViewBtn(sender: AnyObject) {
if shouldAnimateView {
self.bottomConstraint.constant = self.view.frame.size.height/2+5
UIView.animateWithDuration(Double(0.2), animations: {
self.bottomConstraint.constant = self.view.frame.size.height/2 - self.toanimateView.frame.size.height
self.view.layoutIfNeeded()
})
}else{
self.bottomConstraint.constant = self.view.frame.size.height/2 - self.toanimateView.frame.size.height
UIView.animateWithDuration(Double(0.2), animations: {
self.bottomConstraint.constant = self.view.frame.size.height/2+5
self.view.layoutIfNeeded()
})
}
shouldAnimateView = !shouldAnimateView
}
I have watched the WWDC 2012 presentations on Auto Layout and read the documentation on the view appearance calls.
So I thought I needed to perhaps wait a frame or a second after viewDidAppear just to be safe, but still didn't work. Here was my code:
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(animated)
view.layoutIfNeeded()
view.autoresizesSubviews = false
_textButton.frame = CGRectMake(0, 0, 0, 0)
println(_textButton.frame)
let delay:Double = 4*Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue())
{
self._textButton.frame = CGRectMake(0, 0, 0, 0)
println(self._textButton.frame)
}
}
This actually prints out (144.0,6.5,32.0,32.0) twice in a row. Which means even AFTER setting the frame to 0, it was set back to its constraint defaults.
Why is this?
I have another ViewController that looks almost the same as this one, with buttons having the same constraints. But when I close the view, I animate the buttons to slide out to the left with the following code:
#IBAction func takePhotoTap(sender: AnyObject)
{
UIView.animateWithDuration(0.5, animations: animations)
_camera.captureImage(receivePhoto)
}
func animations()
{
var height = CGFloat(_distanceBetweenTopAndMiddleBar)/2
_lowerLens.frame = CGRectMake(_lowerLens.frame.origin.x, _lowerLens.frame.origin.y, _lowerLens.frame.width,-height)
_upperLens.frame = CGRectMake(_upperLens.frame.origin.x, _upperLens.frame.origin.y, _upperLens.frame.width,height)
view.autoresizesSubviews = false
slideOffScreenLeft(_gridLinesButton)
slideOffScreenLeft(_swapCameraButton)
slideOffScreenLeft(_flashButton)
}
func slideOffScreenLeft(obj:UIView)
{
obj.frame = CGRectMake((-obj.frame.width), obj.frame.origin.y, obj.frame.width, obj.frame.height)
}
This works JUST FINE! When they hit the button, these buttons slide off the screen. However when I load the next view I want the buttons to slide in from the right. But, as you can see above, even waiting 4 seconds before trying to set the frames of the buttons has no effect.
Can you suggest what I can do to animate some buttons to slide in the from the screen when the view loads? Why are the constraints overriding my changes in the first case but when I animated a View Controller before closing it with UIView.animateWithDuration the constraints were overriden?
When using AutoLayout, it is best practice to create outlets for your constraints and then modify the constraints, rather than the frames of your objects. I was having a similar problem. This also gets rid of the need for the dispatch_after call.
For example:
#IBOutlet weak var buttonHeight: NSLayoutConstraint!
#IBOutlet weak var buttonWidth: NSLayoutConstraint!
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(animated)
// Modify the constraint rather than the frame
self.buttonHeight.constant = 0
self.buttonWidth.constant = 0
}
This will make the button 0x0. You can then do the same for the x,y position.