Preface: I took a peek at this post and it didn't solve my problem. Here's a link to a repo that shows the error I'm encountering.
I have a UIViewController with a vertical UIStackView containing nested horizontal UIStackViews.
There are two UIStackViews that are toggled. If UIStackViewNumber1 is displayed, UIStackViewNumber2 is hidden & vice versa. I originally set the view to display both, but it's too crowded.
UIStackView
stackViewThatDoesntMove
stackViewNumber1
stackViewNumber2
stackViewThatDoesntMove
Everything works fine with no AutoLayout errors if I don't hide anything. Each of the UIStackViews has nested stackViews containing buttons, labels, sliders, etc. I found the view was too crowded, so I thought I'd set hide one of stackViews and animate the change, as follows:
stackViewNumber2.isHidden = true
UIView.animate(withDuration: 0.33,
options: [.curveEaseOut],
animations: {
self.stackViewNumber1.layoutIfNeeded()
self.stackViewNumber2.layoutIfNeeded()
self.view.layoutIfNeeded()
},
completion: nil)
}
While I got the desired result in Simulator and on a physical device, the moment I hide one of the UIStackViews, my console fills up with AutoLayout errors.
I took a look at this post and my problem seemed pretty straightforward. I went with the approach with the least "bookkeeping" and wrapped stackViewNumber2 in a UIView. I end up with the same set of errors I originally had without wrapping stackViewNumber1 in a UIView.
I also read Apple's documentation on UIStackView and tried playing around with the arrangedSubviews() to set constraints isActive = false, as follows:
for subview in stackViewNumber2.arrangedSubviews {
for constraint in subview.constraints {
constraint.isActive = false
}
}
stackViewNumber2.updateConstraints()
// then run animation block to update view
Is there a more efficient way to hide nested UIStackViews without adding an IBOutlet for constraint, making sure it's not weak, and doing bookkeeping on whether isActive is true/false?
I think my problem has to do with nested stackViews, but I'm unable to figure out how to address it. Thank you for reading and I welcome suggestions on how to hide a UIStackView containing nested UIStackViews.
Instead of hiding I think it would be better to remove the stackView itself.
myStackView.removeArrangedSubview(playButton)
playButton.removeFromSuperview()
Hope it helps.
I started out wanting to animate hiding one view and unhiding another, but at Md. Ibrahim Hassan's suggestion, I tried removing the stackViewNumber1 from the superview and replacing it with stackViewNumber2. The end result is much cleaner with minimal bookkeeping, no dragging a bazillion #IBOutlets onto the viewController for updating constraints, etc.
The views of parentStackView are stored in the stack's arrangedSubviews() array in the order they're displayed in Main.storyboard.
In this example, the arrangedSubviews array looks like this:
[topStackView, stackViewNumber1, stackViewNumber2, stopButton]
I only need to know about parentStackView, stackViewNumber1, and stackViewNumber2, so I dragged 3 outlets to the storyboard, as follows:
// StackViews the viewController needs to know about...
#IBOutlet weak var parentStackView: UIStackView!
// note weak is removed from the following 2 stackViews
#IBOutlet var stackViewNumber1: UIStackView!
#IBOutlet var stackViewNumber2: UIStackView!
From there, if I want to remove stackViewNumber2, the following code removes it:
parentStackView.removeArrangedSubview(stackViewNumber2)
// parentStackView.arrangedSubviews() = [topStackView, stackViewNumber1, stopButton]
stackViewNumber2.removeFromSuperview()
parentStackView.insertArrangedSubview(stackViewNumber1, at: 1)
// parentStackView.arrangedSubviews() = [topStackView, stackViewNumber1, stopButton]
Now, let's say I want to take out stackViewNumber1 and replace it with stackViewNumber2:
parentStackView.removeArrangedSubview(stackViewNumber1)
// parentStackView.arrangedSubviews() = [topStackView, stopButton]
stackViewNumber2.removeFromSuperview()
parentStackView.insertArrangedSubview(stackViewNumber2, at: 1)
// parentStackView.arrangedSubviews() = [topStackView, stackViewNumber2, stopButton]
Recall stackViewNumber1 and stackViewNumber2 are not weak. The reason is I want to keep them around so I can swap them in and out. I didn't see any adverse impact on memory usage as a result of changing the 2 UIStackView outlets to weak.
End result: The slew of AutoLayout errors I encountered with my initial approach is gone without a ton of bookkeeping.
Update:
With a little more tinkering, I got animation to work, too. Here's the code:
UIView.animate(withDuration: 0.25,
delay: 0,
usingSpringWithDamping: 2.0,
initialSpringVelocity: 10.0,
options: [.curveEaseOut],
animations: {
self.view.layoutIfNeeded()
},
completion: nil)
Related
Why restrain a subview from animation? Why not simply carry out its layout changes after the animation?
That's logical but not feasible considering my view hierarchy and this particular use-case.
My view hierarchy:
MyViewController: UIViewController -> MyCustomView: UIView -> MyCustomScrollView: UIScrollView, UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource
Why is it not possible?:
1) I do this in MyViewController after various constraint changes:
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
})
2) Since MyCustomView is a subview which contains MyCustomScrollView (which in turn contains a UICollectionView as its subview), the layout update triggers CV's willDisplay delegate method under which I'm adding a bunch of labels to MyCustomView, to be precise.
Here's the function in MyCustomView that I'm calling:
func addLabel(forIndexPath indexPath: IndexPath) {
var label: UILabel!
label.frame = Util.labelFrame(forIndex: indexPath, fillWidth: false) // The frame for the label is generated here!
//Will assign text and font to the label which are unnecessary to this context
self.anotherSubView.addSubview(label) //Add the label to MyCustomView's subview
}
3) Since these changes get caught up within the animation block from point 1, I get some unnecessary, undesired animations happening. And so, MyCustomView's layout change is bound with this animation block, forcing me to look for a way to restrain this from happening
Things tried so far:
1) Tried the wrap the addSubView() from addLabel(forIndexPath:) inside a UIView.performWithoutAnimation {} block. - No luck
2) Tried the wrap the addSubView() from addLabel(forIndexPath:) inside another animation block with 0.0 seconds time as to see if this overrides the parent animation block - No luck
3) Explored UIView.setAnimationsEnabled(enabled:) but it seems like this won't cancel/pause the existing animators, and will completely disable all the animations if true (which is not what I want)
To sum this all up, my problem is:
I need to restrain the animations on MyCustomView but I need all my other desired layout changes to take place. Is this even possible? Would really appreciate a hint or a solution, TYIA!
Thanks to this answer, removing all the animations from anotherSubview's layer(inside the addLabel(forIndexPath:)) after adding the label:
self.anotherSubview.addSubview(label)
self.anotherSubview.layer.removeAllAnimations() //Doing this removes the animations queued to animate the label's frame into the view
does exactly what I want!
Before the switch to auto layout, applying animations to a view was easy - you'd just change the frame in UIView.layoutWithDuration:. When constraints come into the picture, things get more complicated. The most common method of animating a view that uses auto layout is to keep a reference to the constraint(s) you want to change, then set the constant value, but this is very difficult to design around, and can affect other views if their constraints depend on the position of the view you want to animate.
There must be a better way to do this. I'd love to be able to do something like view.translateFrom(direction: .left, distance: 30, duration: 0.3, delay: 0).
Ultimately you will want to use constraints to achieve simple animations. The overriding reason for this is -
"Choosing any other way is simply swimming against the stream"
If you don't want to 'litter' your classes with #IBOutlets for the constraints you wish to animate then you can in most cases obtain a reference to a pertinent constraint in code:
Handy extension
import UIKit
extension UIView {
func constraint(attribute: NSLayoutAttribute) -> NSLayoutConstraint? {
var constraint : NSLayoutConstraint? = .none
for potentialCenterXConstraint in self.constraints {
if potentialCenterXConstraint.firstAttribute == attribute {
constraint = potentialCenterXConstraint
break
}
}
return constraint
}
}
client code use
if let centerXConstraint = someView.constraint(attribute: .centerX) {
// Do something funky with centerXConstraint
}
Currently working on some swift program and I've a call to action where I remove a blurview from the superview and at the same time I'm animating 2 buttons.
Everything works like it should but there is one small problem. When I remove the blurview from my superview the constraints on my 2 buttons are being set to 0 on the bottom and animating from that position.
I don't want them to shift to 0. If I don't remove the blurview my animation is working perfectly. I've checked if my button constraints are related to the blurview, but that isn't the case. Because I assumed that it could only reset my constraints when they are relative to the blurview.
My storyboard looks the following:
view
|-> camera_view
|-> blur_view
|-> record_label
|-> record_button
The code that I'm executing is the following:
#IBAction func recordButton(sender: AnyObject) {
self.blurView?.removeFromSuperview()
UIButton.animateWithDuration(0.3, delay: 0.2, options: .CurveEaseOut, animations: {
var recordButtonFrame = self.recordButton.frame
var recordLabelFrame = self.recordLabel.frame
recordButtonFrame.origin.y -= recordButtonFrame.size.height
recordLabelFrame.origin.y -= recordLabelFrame.size.height
self.recordButton.frame = recordButtonFrame
self.recordLabel.frame = recordLabelFrame
}, completion: { finished in
print("Button moved")
})
}
What am I doing wrong?
Kind regards,
Wouter
Instead of removing blurView from superview you could hide it.
Replace
self.blurView?.removeFromSuperview()
with
self.blurView?.hidden = true
The problem is that you're animating frames while using constraints. You should be animating constrain changes / constraint constant value changes.
When you don't remove the view the layout isn't recalculated so your frame animation 'works'. It isn't correct and will get reorganised at some point in the future.
When you remove the view the layout is recalculated and everything moves around before your animation starts.
You don't give details of your constraints but it seems likely that you should be animating constraints before removing the view, then removing and ensuring the constraints are all sane on completion.
I am attempting to show a loading view on top of a UITableViewController when a user taps on a cell or a button in the cell. For some reason, the view does not show up, nor does it show any constraint failures. Can someone spot error in my code. I need this view to show up covering the tableview on both orientations, when shown. I thought this to be a view render issue, tried the same code in viewWillAppear. Still does not work. Hence I eliminated layout rendering issues. Same code works perfectly fine on UIViewController derived classes. Seems to have issues on UITableViewController classes alone!!!
private func initActivityView() {
let overlayView = UIView(frame: CGRectZero)
overlayView.backgroundColor = UIColor.lightGrayColor()
overlayView.translatesAutoresizingMaskIntoConstraints = false
overlayView.alpha = 0.5
self.view.addSubview(overlayView)
// add constraints
let viewDictionary = ["overlayView":overlayView]
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[overlayView]-0-|",
options: .AlignAllBaseline, metrics: nil, views: viewDictionary))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[overlayView]-0-|",
options: .AlignAllBaseline, metrics: nil, views: viewDictionary))
}
You should let the constraints engine know that it needs update. See the triggering Auto layout section of the UIView Class Reference. Also I would consider making your overlay view an instance variable or find some other way to keep a reference of it so that when the time comes you can remove it.
Do not call updateConstraints() but rather setNeedsUpdateContraints()
From the UIView Class Reference.
Discussion
When a property of your custom view changes in a way that
would impact constraints, you can call this method to indicate that
the constraints need to be updated at some point in the future. The
system will then call updateConstraints as part of its normal layout
pass. Updating constraints all at once just before they are needed
ensures that you don’t needlessly recalculate constraints when
multiple changes are made to your view in between layout passes.
If your having trouble yo can always set a breakpoint on the UIViewController's
viewWillLayoutSubViews() and viewDidLayoutSubViews() and inspect your subview frames.
It is also pretty helpful to keep update with Apple's documentation for Auto Layout as Auto Layout is constantly changing and improving.
Apparently doing it the autolayout way will not work, due to the inherent nature of UITableViewController. However a better solution for this problem will be something like this
let overlayView1 = UIView(frame: self.tableView.frame)
overlayView1.autoresizingMask = self.tableView.autoresizingMask
overlayView1.backgroundColor = UIColor.lightGrayColor()
overlayView1.alpha = 0.5
self.tableView.addSubview(overlayView1)
So I am trying to create a simple moving animation for my UIImageView.
Problem is I've had its constraints set in the storyview (3 constraints, top, left and right positions).
So I cannot just alter coordinates - my image doesn't move if I do so.
So I guess I have to alter my constraints first. But I cannot even get them in code.
println(myPicture.constraints())
This returns an empty array.
How do I animate an object that has its constraints set in the storyboard?
You can change the constant values of the constraints. I would make outlets for them. Something like this:
#IBOutlet var topConstraint: NSLayoutConstraint!
You can connect these in Storyboard just like any other outlet. It's easiest if you find the constraint in the left-side view hierarchy menu and drag there to make the connections. Then in your code:
topConstraint.constant = 50 // or whatever you want to set it to
That should do it. If you want to animate the movement, you could put it an an animation block. If the change isn't happening when you do this, try calling this afterwards:
UIView.animateWithDuration(0.2,
animations: { () -> Void in
self.topConstraint.constant = 50
self.topConstraint.layoutIfNeeded()
},
completion: nil)