移除所有影响 UIView 的约束

我有一个 UIView,它通过几个约束放置在屏幕上。一些约束属于 superview,其他约束属于其他祖先(例如,可能是 UIViewController 的 view 属性)。

我想移除所有这些旧的约束,并使用新的约束将其放置在新的地方。

如何在不为每个约束创建 IBOutlet 并且必须记住哪个视图拥有该约束的情况下完成此操作?

为了详细说明,幼稚的方法是为每个约束创建一组 IBOutlet,然后调用以下代码:

[viewA removeConstraint:self.myViewsLeftConstraint];
[viewB removeConstraint:self.myViewsTopConstraint];
[viewB removeConstraint:self.myViewsBottomConstraint];
[self.view removeConstraint:self.myViewsRightConstraint];

这段代码的问题是,即使在最简单的情况下,我也需要创建2个 IBOutlet。对于复杂的布局,这可以很容易地达到4或8个所需的 IBOutlet。此外,我还需要确保在正确的视图上调用消除约束的调用。例如,假设 myViewsLeftConstraintviewA所有。如果我不小心调用 [self.view removeConstraint:self.myViewsLeftConstraint],什么也不会发生。

注意: 方法 约束影响轴的布局看起来很有前途,但是仅用于调试目的。


更新: 我收到的许多答案都与 self.constraintsself.superview.constraints或其他变体有关。这些解决方案不会起作用,因为这些方法只返回视图的约束 拥有,而不返回视图的约束 感人至深

为了澄清这些解决方案的问题,请考虑以下视图层次结构:

  • 祖父
    • 父亲
        • 儿子
        • 女儿
      • 哥哥
    • 叔叔

现在假设我们创建了以下约束,并总是将它们附加到它们最近的共同祖先上:

  • C0: Me: 和 Son (我拥有)一样的上衣
  • C1: Me: width = 100(由 Me 拥有)
  • C2: 我: 和哥哥一样高(由父亲拥有)
  • C3: 我: 和叔叔一样的上衣(爷爷的)
  • C4: 我: 和祖父一样的左边(由祖父拥有)
  • C5: 兄弟: 与父亲一样左(父亲拥有)
  • 叔叔: 和祖父一样的左边(由祖父拥有)
  • 儿子: 和女儿一样的左边(由我拥有)

现在假设我们想移除所有影响 Me的约束。任何适当的解决方案都应该移除 [C0,C1,C2,C3,C4]而不是其他任何东西。

如果我使用 self.constraints(其中 self 是 Me) ,我将得到 [C0,C1,C7],因为这些是 Me 拥有的唯一约束。显然,这将不足以删除这,因为它缺少 [C2,C3,C4]。此外,它正在不必要地删除 C7

如果我使用 self.superview.constraints(其中自我是我) ,我将得到 [C2,C5],因为这些是父亲所拥有的约束。显然,我们不能删除所有这些,因为 C5是完全无关的 Me

如果我使用 grandfather.constraints,我将得到 [C3,C4,C6]。同样,我们不能删除所有这些,因为 C6应该保持完整。

强制方法是遍历每个视图的祖先(包括它本身) ,并查看 firstItemsecondItem是否是视图本身; 如果是,删除该约束。这将导致一个正确的解决方案,返回 [C0,C1,C2,C3,C4],并且只返回那些约束。

然而,我希望有一个比必须遍历整个祖先列表更优雅的解决方案。

125727 次浏览

The only solution I have found so far is to remove the view from its superview:

[view removeFromSuperview]

This looks like it removes all constraints affecting its layout and is ready to be added to a superview and have new constraints attached. However, it will incorrectly remove any subviews from the hierarchy as well, and get rid of [C7] incorrectly.

You can remove all constraints in a view by doing this:

self.removeConstraints(self.constraints)

EDIT: To remove the constraints of all subviews, use the following extension in Swift:

extension UIView {
func clearConstraints() {
for subview in self.subviews {
subview.clearConstraints()
}
self.removeConstraints(self.constraints)
}
}

You could use something like this:

[viewA.superview.constraints enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSLayoutConstraint *constraint = (NSLayoutConstraint *)obj;
if (constraint.firstItem == viewA || constraint.secondItem == viewA) {
[viewA.superview removeConstraint:constraint];
}
}];


[viewA removeConstraints:viewA.constraints];

Basically, this is enumerates over all the constraints on the superview of viewA and removes all of the constraints that are related to viewA.

Then, the second part removes the constraints from viewA using the array of viewA's constraints.

I use the following method to remove all constraints from a view:

.h file:

+ (void)RemoveContraintsFromView:(UIView*)view
removeParentConstraints:(bool)parent
removeChildConstraints:(bool)child;

.m file:

+ (void)RemoveContraintsFromView:(UIView *)view
removeParentConstraints:(bool)parent
removeChildConstraints:(bool)child
{
if (parent) {
// Remove constraints between view and its parent.
UIView *superview = view.superview;
[view removeFromSuperview];
[superview addSubview:view];
}


if (child) {
// Remove constraints between view and its children.
[view removeConstraints:[view constraints]];
}
}

You can also read this post on my blog to better understand how it works behind the hood.

If you need more granular control, I'd strongly advise switching to Masonry, a powerful framework class you could use whenever you need to properly handle constraints programmatically.

In Swift:

import UIKit


extension UIView {


/**
Removes all constrains for this view
*/
func removeConstraints() {


let constraints = self.superview?.constraints.filter{
$0.firstItem as? UIView == self || $0.secondItem as? UIView == self
} ?? []


self.superview?.removeConstraints(constraints)
self.removeConstraints(self.constraints)
}
}

This approach worked for me:

@interface UIView (RemoveConstraints)


- (void)removeAllConstraints;


@end




@implementation UIView (RemoveConstraints)


- (void)removeAllConstraints
{
UIView *superview = self.superview;
while (superview != nil) {
for (NSLayoutConstraint *c in superview.constraints) {
if (c.firstItem == self || c.secondItem == self) {
[superview removeConstraint:c];
}
}
superview = superview.superview;
}


[self removeConstraints:self.constraints];
self.translatesAutoresizingMaskIntoConstraints = YES;
}


@end

After it's done executing your view remains where it was because it creates autoresizing constraints. When I don't do this the view usually disappears. Additionally, it doesn't just remove constraints from superview but traversing all the way up as there may be constraints affecting it in ancestor views.


Swift 4 Version

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

A Swift solution:

extension UIView {
func removeAllConstraints() {
var view: UIView? = self
while let currentView = view {
currentView.removeConstraints(currentView.constraints.filter {
return $0.firstItem as? UIView == self || $0.secondItem as? UIView == self
})
view = view?.superview
}
}
}

It's important to go through all the parents, since the constraints between two elements are holds by the common ancestors, so just clearing the superview as detailed in this answer is not good enough, and you might end up having bad surprise later on.

There are two ways of on how to achieve that according to Apple Developer Documentation

1. NSLayoutConstraint.deactivateConstraints

This is a convenience method that provides an easy way to deactivate a set of constraints with one call. The effect of this method is the same as setting the isActive property of each constraint to false. Typically, using this method is more efficient than deactivating each constraint individually.

// Declaration
class func deactivate(_ constraints: [NSLayoutConstraint])


// Usage
NSLayoutConstraint.deactivate(yourView.constraints)

2. UIView.removeConstraints (Deprecated for >= iOS 8.0)

When developing for iOS 8.0 or later, use the NSLayoutConstraint class’s deactivateConstraints: method instead of calling the removeConstraints: method directly. The deactivateConstraints: method automatically removes the constraints from the correct views.

// Declaration
func removeConstraints(_ constraints: [NSLayoutConstraint])`


// Usage
yourView.removeConstraints(yourView.constraints)

Tips

Using Storyboards or XIBs can be such a pain at configuring the constraints as mentioned on your scenario, you have to create IBOutlets for each ones you want to remove. Even so, most of the time Interface Builder creates more trouble than it solves.

Therefore when having very dynamic content and different states of the view, I would suggest:

  1. Creating your views programmatically
  2. Layout them and using NSLayoutAnchor
  3. Append each constraint that might get removed later to an array
  4. Clear them every time before applying the new state

Simple Code

private var customConstraints = [NSLayoutConstraint]()


private func activate(constraints: [NSLayoutConstraint]) {
customConstraints.append(contentsOf: constraints)
customConstraints.forEach { $0.isActive = true }
}


private func clearConstraints() {
customConstraints.forEach { $0.isActive = false }
customConstraints.removeAll()
}


private func updateViewState() {
clearConstraints()


let constraints = [
view.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
view.topAnchor.constraint(equalTo: parentView.topAnchor),
view.bottomAnchor.constraint(equalTo: parentView.bottomAnchor)
]


activate(constraints: constraints)


view.layoutIfNeeded()
}

References

  1. NSLayoutConstraint
  2. UIView

(As of July 31, 2017)

SWIFT 3

self.yourCustomView.removeFromSuperview()
self.yourCustomViewParentView.addSubview(self.yourCustomView)

Objective C

[self.yourCustomView removeFromSuperview];
[self.yourCustomViewParentView addSubview:self.yourCustomView];

This is the easiest way to quickly remove all constraints that exist on a UIView. Just be sure to add the UIView back with it's new constraints or new frame afterwards =)

Details

  • Xcode 10.2.1 (10E1001), Swift 5

Solution

import UIKit


extension UIView {


func removeConstraints() { removeConstraints(constraints) }
func deactivateAllConstraints() { NSLayoutConstraint.deactivate(getAllConstraints()) }
func getAllSubviews() -> [UIView] { return UIView.getAllSubviews(view: self) }


func getAllConstraints() -> [NSLayoutConstraint] {
var subviewsConstraints = getAllSubviews().flatMap { $0.constraints }
if let superview = self.superview {
subviewsConstraints += superview.constraints.compactMap { (constraint) -> NSLayoutConstraint? in
if let view = constraint.firstItem as? UIView, view == self { return constraint }
return nil
}
}
return subviewsConstraints + constraints
}


class func getAllSubviews(view: UIView) -> [UIView] {
return view.subviews.flatMap { [$0] + getAllSubviews(view: $0) }
}
}

Usage

print("constraints: \(view.getAllConstraints().count), subviews: \(view.getAllSubviews().count)")
view.deactivateAllConstraints()

This is the way to disable all constraints from a specific view

 NSLayoutConstraint.deactivate(myView.constraints)

Based on previous answers (swift 4)

You can use immediateConstraints when you don't want to crawl entire hierarchies.

extension UIView {
/**
* Deactivates immediate constraints that target this view (self + superview)
*/
func deactivateImmediateConstraints(){
NSLayoutConstraint.deactivate(self.immediateConstraints)
}
/**
* Deactivates all constrains that target this view
*/
func deactiveAllConstraints(){
NSLayoutConstraint.deactivate(self.allConstraints)
}
/**
* Gets self.constraints + superview?.constraints for this particular view
*/
var immediateConstraints:[NSLayoutConstraint]{
let constraints = self.superview?.constraints.filter{
$0.firstItem as? UIView === self || $0.secondItem as? UIView === self
} ?? []
return self.constraints + constraints
}
/**
* Crawls up superview hierarchy and gets all constraints that affect this view
*/
var allConstraints:[NSLayoutConstraint] {
var view: UIView? = self
var constraints:[NSLayoutConstraint] = []
while let currentView = view {
constraints += currentView.constraints.filter {
return $0.firstItem as? UIView === self || $0.secondItem as? UIView === self
}
view = view?.superview
}
return constraints
}
}

With objectiveC

[self.superview.constraints enumerateObjectsUsingBlock:^(__kindof NSLayoutConstraint * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLayoutConstraint *constraint = (NSLayoutConstraint *)obj;
if (constraint.firstItem == self || constraint.secondItem == self) {
[self.superview removeConstraint:constraint];
}
}];
[self removeConstraints:self.constraints];
}

The easier and efficient approach is to remove the view from superView and re add as subview again. this causes all the subview constraints get removed automagically.😉

Using a Reusable Sequence

I decided to approach this in a more 'reusable' way. Since finding all constraints affecting a view is the basis for all of the above, I decided to implement a custom sequence that returns them all for me, along with the owning views.

First thing to do is define an extension on Arrays of NSLayoutConstraint that returns all elements affecting a specific view.

public extension Array where Element == NSLayoutConstraint {


func affectingView(_ targetView:UIView) -> [NSLayoutConstraint] {


return self.filter{


if let firstView = $0.firstItem as? UIView,
firstView == targetView {
return true
}


if let secondView = $0.secondItem as? UIView,
secondView == targetView {
return true
}


return false
}
}
}

We then use that extension in a custom sequence that returns all constraints affecting that view, along with the views that actually own them (which can be anywhere up the view hierarchy)

public struct AllConstraintsSequence : Sequence {


public init(view:UIView){
self.view = view
}


public let view:UIView


public func makeIterator() -> Iterator {
return Iterator(view:view)
}


public struct Iterator : IteratorProtocol {


public typealias Element = (constraint:NSLayoutConstraint, owningView:UIView)


init(view:UIView){
targetView  = view
currentView = view
currentViewConstraintsAffectingTargetView = currentView.constraints.affectingView(targetView)
}


private let targetView  : UIView
private var currentView : UIView
private var currentViewConstraintsAffectingTargetView:[NSLayoutConstraint] = []
private var nextConstraintIndex = 0


mutating public func next() -> Element? {


while(true){


if nextConstraintIndex < currentViewConstraintsAffectingTargetView.count {
defer{nextConstraintIndex += 1}
return (currentViewConstraintsAffectingTargetView[nextConstraintIndex], currentView)
}


nextConstraintIndex = 0


guard let superview = currentView.superview else { return nil }


self.currentView = superview
self.currentViewConstraintsAffectingTargetView = currentView.constraints.affectingView(targetView)
}
}
}
}

Finally we declare an extension on UIView to expose all the constraints affecting it in a simple property that you can access with a simple for-each syntax.

extension UIView {


var constraintsAffectingView:AllConstraintsSequence {
return AllConstraintsSequence(view:self)
}
}

Now we can iterate all constraints affecting a view and do what we want with them...

List their identifiers...

for (constraint, _) in someView.constraintsAffectingView{
print(constraint.identifier ?? "No identifier")
}

Deactivate them...

for (constraint, _) in someView.constraintsAffectingView{
constraint.isActive = false
}

Or remove them entirely...

for (constraint, owningView) in someView.constraintsAffectingView{
owningView.removeConstraints([constraint])
}

Enjoy!

Swift

Following UIView Extension will remove all Edge constraints of a view:

extension UIView {
func removeAllConstraints() {
if let _superview = self.superview {
self.removeFromSuperview()
_superview.addSubview(self)
}
}
}