I have a button placed in the center using centerXAnchor of superview, but now I have to change the position of the button from centerX to align leading from code. However, it's not moving to the left. Instead, it gets full width button.
buttonView!.translatesAutoresizingMaskIntoConstraints = false
buttonView!.removeConstraints(buttonView!.constraints)
NSLayoutConstraint.activate([
buttonView!.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 12),
buttonView!.bottomAnchor.constraint(equalTo: mainView.bottomAnchor, constant: 20),
])
Removing/add constraints doesn't cause them to be (re)applied.
Call .setNeedsUpdateConstraints() on your view. The system will then call updateConstraints as part of its next layout pass. For complex constraint scenarios you may need your own implementation of updateConstraints, but for most cases, and definitely yours, this won't be needed (and should generally be avoided unless there is a specific reason to use it - see the docs)
first remove all Constraint
extension UIView {
public func removeAllConstraints() {
var _superview = self.superview
while let superview = _superview {
for constraint in superview.constraints {
if let first = constraint.firstItem as? UIView, first == self {
superview.removeConstraint(constraint)
}
if let second = constraint.secondItem as? UIView, second == self {
superview.removeConstraint(constraint)
}
}
_superview = superview.superview
}
self.removeConstraints(self.constraints)
self.translatesAutoresizingMaskIntoConstraints = true
self.setNeedsUpdateConstraints()
}
}
to use it
DispatchQueue.main.async { [weak self] in
self?.buttonView.removeAllConstraints()
// then you can add your constraint as you like
}
ref
There are various ways to do this, depending on exactly what you need to accomplish.
First, you said you're laying out your views in Storyboard, so...
If we're talking about one (or a few) specific views, we can create #IBOutlet vars for the constraints you want to change.
In Storyboard:
give your buttonView a centerX constraint, with Priority: Required (1000)
give your buttonView your desired leading constraint, with Priority: Low (250)
Connect them to outlets:
#IBOutlet var buttonCenterConstraint: NSLayoutConstraint!
#IBOutlet var buttonLeadingConstraint: NSLayoutConstraint!
Then, to switch from centered to leading:
buttonCenterConstraint.priority = .defaultLow
buttonLeadingConstraint.priority = .required
and you can "toggle" it back to centered with:
buttonLeadingConstraint.priority = .defaultLow
buttonCenterConstraint.priority = .required
Perhaps do the same thing with centerY and bottom constraints.
If you want a little more "flexibility," you could do something like this:
in Storyboard, set only the centerX constraint
Then, to change that to a leading constraint:
// find the centerX constraint and de-activate it
if let cxConstraint = mainView.constraints.first(where: { ($0.firstAttribute == .centerX && $0.firstItem === buttonView) }) {
cxConstraint.isActive = false
} else if let cxConstraint = mainView.constraints.first(where: { ($0.firstAttribute == .centerX && $0.secondItem === buttonView) }) {
cxConstraint.isActive = false
}
// add a new leading constraint
buttonView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor, constant: 12.0).isActive = true
You could also use an extension as suggested by someone else to "remove all constraints" ... but you risk removing constraints that you do not want changed.
Related
I use following code (AutoLayout, SnapKit) to set up scrollView:
public lazy var contentView = UIView()
public lazy var scrollView = UIScrollView()
func addScrollView(insets: UIEdgeInsets, safeAreaRelatedSides: [Side]) {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
scrollView.snp.makeConstraints { make in
make.top.equalTo(safeAreaRelatedSides.contains(where: { $0 == .top }) ? view.safeAreaLayoutGuide.snp.top : view.snp.top)
make.left.equalTo(safeAreaRelatedSides.contains(where: { $0 == .left }) ? view.safeAreaLayoutGuide.snp.left : view.snp.left)
make.right.equalTo(safeAreaRelatedSides.contains(where: { $0 == .right }) ? view.safeAreaLayoutGuide.snp.right : view.snp.right)
make.bottom.equalTo(safeAreaRelatedSides.contains(where: { $0 == .bottom }) ? view.safeAreaLayoutGuide.snp.bottom : view.snp.bottom)
}
contentView.snp.makeConstraints { make in
make.top.equalToSuperview().inset(insets.top)
make.left.equalToSuperview().inset(insets.left)
make.right.equalToSuperview().inset(insets.right)
make.bottom.equalToSuperview().inset(insets.bottom).priority(250)
make.centerX.equalToSuperview()
make.centerY.equalToSuperview().priority(250)
}
scrollView.contentInsetAdjustmentBehavior = .never
}
It works great. But When I want scroll view's bottom constraint to depend on superview and not its safe area (to let excessive content height be visible below safe area), I call:
addScrollView(insets: someValue, safeAreaRelatedSides: [.top, .left, .right])
scrollView.contentInset.bottom = UIApplication.shared.safeAreaInsets.bottom
In result, contentView layouts its height as:
expectedContentViewHeight + scrollView.contentInset.bottom
A view with lowest contenHuggingPriority gets stretched by scrollView.contentInset.bottom more than it should. And now scrollView.contentSize.height exceeds scrollView.bounds.height with nothing forcing that.
This only happens when contentView height is less than screen height.
First screenshot represents initial UI. Second one - after scrolling to the very bottom
What can I do to prevent contentView height from this excessive growing?
Posting a question for the first time here.
So I have been trying to make an animation of an UIimageView. I did that so far. So the image moves from the middle of the screen to the top. I want to be able to make that animation with constraints. But while trying to add some constraints, I receive this error "Unable to activate constraint with anchors error".
here is the code which I try to add some constraints to banditLogo imageview.
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(banditLogo)
view.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
chooseLabel.alpha = 0
signInButtonOutlet.alpha = 0
self.banditLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 304).isActive = true
self.banditLogo.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
self.banditLogo.widthAnchor.constraint(equalToConstant: 224).isActive = true
self.banditLogo.heightAnchor.constraint(equalToConstant: 289).isActive = true
}
and here is the func that makes the animation.
this func is being called in viewDidAppear and animatedImage variable of the function is referred to banditLogo UIimageView.
so when the view screen loads up, the image moves to top of the view.
func logoAnimate(animatedImage: UIImageView!, animatedLabel: UILabel!) {
UIView.animate(withDuration: 1.5, delay: 1, options: [.allowAnimatedContent]) {
animatedImage.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 5).isActive = true
animatedImage.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
} completion: { (true) in
UIView.animate(withDuration: 0.25) {
animatedLabel.alpha = 1
}
}
}
You may find it easier to create a class-level property to hold the image view's top constraint, then change that constraint's .constant value when you want to move it.
Here's a quick example - tapping anywhere on the view will animate the image view up or down:
class AnimLogoViewController: UIViewController {
let banditLogo = UIImageView()
// we'll change this constraint's .constant to change the image view's position
var logoTopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
if let img = UIImage(systemName: "person.fill") {
banditLogo.image = img
}
view.addSubview(banditLogo)
// I assume this was a typo... you want to set it on the image view, not the controller's view
//view.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
banditLogo.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
// create the image view's top constraint
logoTopConstraint = banditLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 304)
// activate it
logoTopConstraint.isActive = true
// non-changing constraints
self.banditLogo.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
self.banditLogo.widthAnchor.constraint(equalToConstant: 224).isActive = true
self.banditLogo.heightAnchor.constraint(equalToConstant: 289).isActive = true
// animate the logo when you tap the view
let t = UITapGestureRecognizer(target: self, action: #selector(self.didTap(_:)))
view.addGestureRecognizer(t)
}
#objc func didTap(_ g: UITapGestureRecognizer) -> Void {
// if the logo image view is at the top, animate it down
// else, animate it up
if logoTopConstraint.constant == 5.0 {
logoTopConstraint.constant = 304.0
} else {
logoTopConstraint.constant = 5.0
}
UIView.animate(withDuration: 1.5, animations: {
self.view.layoutIfNeeded()
})
}
}
I animate views that have constraints by changing constraints, not setting them. Leave the constraints that are static "as is" - that is, use isActive = true. But those you wish to change? Put them in two arrays and activate/deactivte them. Complete the animation like you are by using UIView.animate.
For instance, let's say you wish to move banditLogo from top 304 to top 5, which appears to me to be what you trying to do. Leave all other constraints as is - left (which your code doesn't seem to change), height, and width. Now, create two arrays:
var start = [NSLayoutConstraint]()
var finish = [NSLayoutConstraint]()
Add in the constraints that change. Note that I'm not setting them as active:
start.append(banditLogo.topAnchor.constraint(equalTo: safeAreaView.topAnchor, constant: 305))
finish.append(banditLogo.topAnchor.constraint(equalTo: safeAreaView.topAnchor, constant: 5))
Initialize things in viewDidLoad or any other view controller method as needed:
NSLayoutConstraint.activate(start)
Finally, when you wish to do the animation, deactivate/activate and tell the view to show the animation:
NSLayoutConstraint.deactivate(start)
NSLayoutConstraint.activate(finish)
UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() }
Last piece of critique, made with no intent of being offending.
Something in your code posted feels messy to me. Creating a function to move a single view should directly address the view IMHO, not pass the view into it. Maybe you are trying to move several views this way - in which case this is good code - but nothing in your question suggests it. It's okay to do the animation in a function - that way you can call it when needed. I do this all the time for something like this - sliding a tool overlay in and out. But if you are doing this to a single view, just address it directly. The code is more readable to other coders.
Also, my preference for the start is in viewDidLoad unless the VC is part of a navigation stack. But in that case, don't just use viewDidAppear, set things back to start in viewDidDisappear.
EDIT: looking at the comments, I assumed that yes you have already used translatesAutoresizingMaskIntoConstraints = false properly on every view needed.
I have these constraints in place for my UItableView (from storyboard):
musicHomeTableView.translatesAutoresizingMaskIntoConstraints = false
musicHomeTableView.topAnchor.constraint(equalTo: view.topAnchor ).isActive = true
musicHomeTableView.bottomAnchor.constraint(equalTo: mainTabBar.topAnchor).isActive = true
musicHomeTableView.widthAnchor.constraint(equalToConstant: view.frame.width).isActive = true
The above code is called from viewDidLoad and it works fine. Yet when I try to execute this code later:
musicHomeTableView.bottomAnchor.constraint(equalTo: playerView.topAnchor).isActive = true
Nothing happens. mainTabBar and playerView are both programmatically setUp views and subviews of view.
I've tried view.layoutSubviews() and view.layOutIfNeeded()
musicHomeTableView already has a bottomAnchor constraint. You need to remove this constraint before adding another one to the same anchor. Setting another constraint to the same anchor does not remove the old constraint.
class ViewController {
var musicHomeTableViewBottomAnchorConstraint: NSLayoutConstraint?
func viewDidLoad() {
musicHomeTableViewBottomAnchorConstraint = musicHomeTableView.bottomAnchor.constraint(equalTo: view.topAnchor)
musicHomeTableViewBottomAnchorConstraint!.isActive = true
}
func addNewConstraint() {
if let constraint = musicHomeTableViewBottomAnchorConstraint {
constraint.isActive = false
}
// Add new constraint here
}
}
There are other ways to do this, but this is one.
I was reading about auto layout rendering pipelines i mean how auto layout work under the hood. There are some methods which get called at different stages of autoLayout rendering like
layoutIfNeeded()
layoutSubviews()
updateConstraints()
updateConstraintsIfNeeded()
but i don't know which method is called when and what is the significance of that method and if i want to use auto layout then in which order i can use that methods and how can i control the autoLayout rendering pipeline
Usually you don't need to care about the autolayout method chain. You just need to create the constraints for the views to define their sizes and positions. You can add/remove, activate/deactivate constraints anytime in lifecycle of the view, but you want to always have a set of satisfiable (non-conflicting), yet complete set of constraints.
Take an example. You can tell the autolayout that button A should be 50 points wide, 20 points high, with its left top corner positioned at point (0,0) in the viewController's view. Now, this is non-conflicting, yet complete set of constraints for the button A. But lets say you want to expand that button, when the user taps it. So in the tap handler you will add one new constraint saying that the button should be 100 points wide - now you have unsatisfiable constraints - there is a constraint say it should be 50 points wide, and another one saying it shoul be 100 points wide. Therefore, to prevent conflict, before activating the new constraint, you have to deactivate the old one. Incomplete constraints is an opposite case, lets say you deactivate the old width constraint, but never activate the new one. Then autolayout can calculate position (because there are constraints defining it), and height, but not width, which usually ends in undefined behavior (now in case of a UIButton that's not true, because it has intrinsic size, which implicitly defines its width and height, but I hope you get the point).
So when you create those constraints is up to you (in my example you were manipulating them when the user tapped the button). Usually you start in initializer in case of a UIView subclass or in loadView in UIViewController subclass and there you can define and activate the default set of constraints. And then you can use handlers to react to user activity. My recommendation is prepare all the constraints in loadView, keep them in properties, and activate/deactivate them when necessary.
But there are of course some limitation as when and how not to create new constraints - for a more detailed discussion of those cases I really recommend looking at Advanced Autolayout Toolbox by objc.io.
EDIT
See following example of a simple custom SongView that uses autolayout for layout and supports also some dynamic changes in constraints by activating/deactivating them. You can just simply copy paste the whole code into a playground and test it out there, or include it in a project.
Notice there that I don't call any of the autolayout lifecycle methods, except of setNeedsLayout and layoutIfNeeded. setNeedsLayout sets a flag telling the autolayout that constraints have been changed, and layoutIfNeeded then tells it to recalculate frames. Normally, that would happen automatically, but to animate the constraints changes we need to tell it explicitly - see the setExpanded method in SongView. For more detailed explanation of using autolayout in animations, see my different answer.
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
let songView = SongView()
let button = UIButton()
override func loadView() {
super.loadView()
view.backgroundColor = .white
self.view.addSubview(button)
self.view.addSubview(songView)
button.setTitle("Expand/Collapse", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(expandCollapse), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
songView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// button has intrinsic size, no need to define constraints for size, position is enough
button.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -50),
button.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
// songView has defined its height (see SongView class), but not width, therefore we need more constraints
songView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
songView.leftAnchor.constraint(equalTo: self.view.leftAnchor),
songView.rightAnchor.constraint(equalTo: self.view.rightAnchor),
])
}
#objc func expandCollapse() {
if songView.isExpanded {
songView.setExpanded(to: false, animated: true)
} else {
songView.setExpanded(to: true, animated: true)
}
}
}
class SongView: UIView {
private let numberLabel: UILabel = UILabel()
private let nameLabel: UILabel = UILabel()
private var expandedConstraints: [NSLayoutConstraint] = []
private var collapsedConstraints: [NSLayoutConstraint] = []
// this can be triggered by some event
private(set) var isExpanded: Bool = false
func setExpanded(to expanded: Bool, animated: Bool) {
self.isExpanded = expanded
if animated {
if expanded {
// setup expanded state
NSLayoutConstraint.deactivate(collapsedConstraints)
NSLayoutConstraint.activate(expandedConstraints)
} else {
// setup collapsed
NSLayoutConstraint.deactivate(expandedConstraints)
NSLayoutConstraint.activate(collapsedConstraints)
}
self.setNeedsLayout()
UIView.animate(withDuration: 0.2, animations: {
self.layoutIfNeeded()
})
} else {
// non animated version (no need to explicitly call setNeedsLayout nor layoutIfNeeded)
if expanded {
// setup expanded state
NSLayoutConstraint.deactivate(collapsedConstraints)
NSLayoutConstraint.activate(expandedConstraints)
} else {
// setup collapsed
NSLayoutConstraint.deactivate(expandedConstraints)
NSLayoutConstraint.activate(collapsedConstraints)
}
}
}
var data: (String, String)? {
didSet {
numberLabel.text = data?.0
nameLabel.text = data?.1
}
}
init() {
super.init(frame: CGRect.zero)
setupInitialHierarchy()
setupInitialAttributes()
setupInitialLayout()
}
fileprivate func setupInitialHierarchy() {
self.addSubview(numberLabel)
self.addSubview(nameLabel)
}
fileprivate func setupInitialAttributes() {
numberLabel.font = UIFont.boldSystemFont(ofSize: UIFont.preferredFont(forTextStyle: UIFontTextStyle.body).pointSize)
numberLabel.textColor = UIColor.darkGray
numberLabel.text = "0"
numberLabel.textAlignment = .right
nameLabel.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.body)
nameLabel.text = "NONE"
nameLabel.textAlignment = .left
self.backgroundColor = UIColor.lightGray
}
fileprivate func setupInitialLayout() {
self.translatesAutoresizingMaskIntoConstraints = false
numberLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.translatesAutoresizingMaskIntoConstraints = false
// just randomly selected different layouts for collapsed and expanded states
expandedConstraints = [
numberLabel.widthAnchor.constraint(equalToConstant: 35),
self.heightAnchor.constraint(equalToConstant: 80),
]
collapsedConstraints = [
numberLabel.widthAnchor.constraint(equalToConstant: 50),
self.heightAnchor.constraint(equalToConstant: 40),
]
// activating collapsed as default layout
NSLayoutConstraint.activate(collapsedConstraints)
NSLayoutConstraint.activate([
numberLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 4),
numberLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -4),
numberLabel.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 4),
nameLabel.centerYAnchor.constraint(equalTo: numberLabel.centerYAnchor),
nameLabel.leftAnchor.constraint(equalTo: numberLabel.rightAnchor, constant: 8),
nameLabel.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -4)
])
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
PlaygroundPage.current.liveView = ViewController()
it has a replacementView as UIView in a storyboard with
#IBOutlet var replacementView: UIView!
connected.
I want to replace the replacementView with
replacementView = SecondViewController().view
But it doesn't work. What is the problem?
replacementView is just reference. You have to change object in view stack.
You should keep reference to replacementView's parent. Next remove replacementView from parentView and add SecondViewController().view to parentView.
I suggest you to try add your SecondViewController().view as replacementView and add fill constraints.
You should also remember about retain SecondViewController, otherwise it could be dealocated before it will appear. Read about addChildViewController(childController: UIViewController) UIViewController method.
Essentially you need to add constraints to the view you are adding and then remove the old view. Assuming the items are in order in your parent view, here's some code that would do that for you.
func replaceView(oldView:UIView, newView: UIView, spacingBetweenViews: CGFloat) {
//loop through, find the view, and add a constraint between the next and previous items.
let parentView = oldView.superview ?? UIView()
for (index, subview) in parentView.subviews.enumerated() {
let PREVIOUS_VIEW = index - 1
let NEXT_VIEW = index + 1
if subview == oldView {
if index == 0 {
//if the first view
let constraints = [
parentView.topAnchor.constraint(
equalTo: newView.topAnchor,
constant: spacingBetweenViews),
newView.bottomAnchor.constraint(
equalTo: parentView.subviews[NEXT_VIEW].topAnchor,
constant: spacingBetweenViews)
]
NSLayoutConstraint.activate(constraints)
} else if index == parentView.subviews.count - 1 {
// if the last view
let constraints = [
parentView.subviews[PREVIOUS_VIEW].bottomAnchor.constraint(
equalTo: newView.topAnchor,
constant: spacingBetweenViews),
newView.bottomAnchor.constraint(
equalTo: parentView.bottomAnchor,
constant: spacingBetweenViews)
]
NSLayoutConstraint.activate(constraints)
} else {
let constraints = [
parentView.subviews[PREVIOUS_VIEW].bottomAnchor.constraint(
equalTo: newView.topAnchor,
constant: spacingBetweenViews),
newView.bottomAnchor.constraint(
equalTo: parentView.subviews[NEXT_VIEW].topAnchor,
constant: spacingBetweenViews)
]
NSLayoutConstraint.activate(constraints)
}
parentView.subviews[index].removeFromSuperview()
}
}
}