简体   繁体   中英

How to "find" your own constraint?

Say I have a UIView,

 class CleverView: UIView

In the custom class, I want to do this:

func changeWidth() {

  let c = ... find my own layout constraint, for "width"
  c.constant = 70 * Gameinfo.ImportanceOfEnemyFactor
}

Similarly I wanna be able to "find" like that, the constraint (or I guess, all constraints, there could be more than one) attached to one of the four edges.

So, to look through all the constraints attached to me, and find any width/height ones, or indeed any relevant to a given (say, "left") edge.

Any ideas?

It's perhaps worth noting this question


Please, note that (obviously) I am asking how to do this dynamically/programmatically.

(Yes, you can say "link to the constraint" or "use an ID" - the whole point of the QA is how to find them on the fly and work dynamically.)

If you are new to constraints, note that .constraints just gives you the ends stored "there".

There are really two cases:

  1. Constraints regarding a view's size or relations to descendant views are saved in itself
  2. Constraints between two views are saved in the views' lowest common ancestor

To repeat. For constraints which are between two views. iOS does, in fact, always store them in the lowest common ancestor. Thus, a constraint of a view can always be found by searching all ancestors of the view.

Thus, we need to check the view itself and all its superviews for constraints. One approach could be:

extension UIView {

    // retrieves all constraints that mention the view
    func getAllConstraints() -> [NSLayoutConstraint] {

        // array will contain self and all superviews
        var views = [self]

        // get all superviews
        var view = self
        while let superview = view.superview {
            views.append(superview)
            view = superview
        }

        // transform views to constraints and filter only those
        // constraints that include the view itself
        return views.flatMap({ $0.constraints }).filter { constraint in
            return constraint.firstItem as? UIView == self ||
                constraint.secondItem as? UIView == self
        }
    }
}

You can apply all kinds of filters after getting all constraints about a view, and I guess that's the most difficult part. Some examples:

extension UIView {

    // Example 1: Get all width constraints involving this view
    // We could have multiple constraints involving width, e.g.:
    // - two different width constraints with the exact same value
    // - this view's width equal to another view's width
    // - another view's height equal to this view's width (this view mentioned 2nd)
    func getWidthConstraints() -> [NSLayoutConstraint] {
        return getAllConstraints().filter( {
            ($0.firstAttribute == .width && $0.firstItem as? UIView == self) ||
            ($0.secondAttribute == .width && $0.secondItem as? UIView == self)
        } )
    }

    // Example 2: Change width constraint(s) of this view to a specific value
    // Make sure that we are looking at an equality constraint (not inequality)
    // and that the constraint is not against another view
    func changeWidth(to value: CGFloat) {

        getAllConstraints().filter( {
            $0.firstAttribute == .width &&
                $0.relation == .equal &&
                $0.secondAttribute == .notAnAttribute
        } ).forEach( {$0.constant = value })
    }

    // Example 3: Change leading constraints only where this view is
    // mentioned first. We could also filter leadingMargin, left, or leftMargin
    func changeLeading(to value: CGFloat) {
        getAllConstraints().filter( {
            $0.firstAttribute == .leading &&
                $0.firstItem as? UIView == self
        }).forEach({$0.constant = value})
    }
}

// edit: Enhanced examples and clarified their explanations in comments

I guess you can work with constraints property of UIView . constraints basically returns an array of constraint directly assigned to UIView. It will not be able to get you the constraints held by superview such as leading, trailing, top or bottom but width and height constraints are held by View itself. For superview's constraints, you can loop through superview's constraints. Lets say the clever view has these constraints:

在此处输入图片说明

class CleverView: UIView {

    func printSuperViewConstriantsCount() {
        var c = 0
        self.superview?.constraints.forEach({ (constraint) in
            guard constraint.secondItem is CleverView || constraint.firstItem is CleverView else {
                return
            }
            c += 1
            print(constraint.firstAttribute.toString())
        })
        print("superview constraints:\(c)")
    }

    func printSelfConstriantsCount() {
        self.constraints.forEach { (constraint) in
            return print(constraint.firstAttribute.toString())
        }
        print("self constraints:\(self.constraints.count)")
    }
}

Output :

top
leading
trailing
superview constraints:3
height
self constraints:1

Basically, you can look at NSLayoutConstraint class to get the info out about a particular constraint.

To print the name of constraints, we can use this extension

extension NSLayoutAttribute {
    func toString() -> String {
        switch self {
        case .left:
            return "left"
        case .right:
            return "right"
        case .top:
            return "top"
        case .bottom:
            return "bottom"
        case .leading:
            return "leading"
        case .trailing:
            return "trailing"
        case .width:
            return "width"
        case .height:
            return "height"
        case .centerX:
            return "centerX"
        case .centerY:
            return "centerY"
        case .lastBaseline:
            return "lastBaseline"
        case .firstBaseline:
            return "firstBaseline"
        case .leftMargin:
            return "leftMargin"
        case .rightMargin:
            return "rightMargin"
        case .topMargin:
            return "topMargin"
        case .bottomMargin:
            return "bottomMargin"
        case .leadingMargin:
            return "leadingMargin"
        case .trailingMargin:
            return "trailingMargin"
        case .centerXWithinMargins:
            return "centerXWithinMargins"
        case .centerYWithinMargins:
            return "centerYWithinMargins"
        case .notAnAttribute:
            return "notAnAttribute"
        }
    }
}

stakri 's answer is OK, but we can do better by using sequence(first:next:) :

extension UIView {
    var allConstraints: [NSLayoutConstraint] {
        sequence(first: self, next: \.superview)
            .flatMap(\.constraints)
            .lazy
            .filter { constraint in
                constraint.firstItem as? UIView == self || constraint.secondItem as? UIView == self
            }
    }
}

Then, if we check both implementations by swift-benchmark by Google we can see that Sequence implementation is much faster (almost +50k iterations for the ±same time).

running Find All Constraints: Stakri... done! (1778.86 ms)
running Find All Constraints: Sequence... done! (1875.20 ms)

name                          time        std        iterations
---------------------------------------------------------------
Find All Constraints.Stakri   3756.000 ns ±  96.67 %     291183
Find All Constraints.Sequence 3727.000 ns ± 117.42 %     342261

Might save someone some typing.......

Based on stakri's bounty-winning answer, here is exactly how to get

all constraints of the type "fractional width of another view"

all constraints of the type "fixed point width"

all constraints of the type "your x position"

So ..

 fileprivate extension UIView {
    func widthAsPointsConstraints()->[NSLayoutConstraint] {}
    func widthAsFractionOfAnotherViewConstraints()->[NSLayoutConstraint] {}
    func xPositionConstraints()->[NSLayoutConstraint]
}

Full code below. Of course, you can do "height" the same way.

So, use them like this...

let cc = someView.widthAsFractionOfAnotherViewConstraints()
for c in cc {
   c.changeToNewConstraintWith(multiplier: 0.25)
}

or

let cc = someView.widthAsPointsConstraints()
for c in cc {
    c.constant = 150.0
}

Also, at the bottom I pasted in a simple demo code, example output...

在此处输入图片说明

Here's the code. V2 ...

fileprivate extension UIView { // experimental
    
    func allConstraints()->[NSLayoutConstraint] {
        
        var views = [self]
        var view = self
        while let superview = view.superview {
            
            views.append(superview)
            view = superview
        }
        
        return views.flatMap({ $0.constraints }).filter { constraint in
            return constraint.firstItem as? UIView == self ||
                constraint.secondItem as? UIView == self
        }
    }
    
     func widthAsPointsConstraints()->[NSLayoutConstraint] {

        return self.allConstraints()
         .filter({
            ( $0.firstItem as? UIView == self && $0.secondItem == nil )
         })
         .filter({
            $0.firstAttribute == .width && $0.secondAttribute == .notAnAttribute
         })
    }
    
    func widthAsFractionOfAnotherViewConstraints()->[NSLayoutConstraint] {
        
        func _bothviews(_ c: NSLayoutConstraint)->Bool {
            if c.firstItem == nil { return false }
            if c.secondItem == nil { return false }
            if !c.firstItem!.isKind(of: UIView.self) { return false }
            if !c.secondItem!.isKind(of: UIView.self) { return false }
            return true
        }
        
        func _ab(_ c: NSLayoutConstraint)->Bool {
            return _bothviews(c)
                && c.firstItem as? UIView == self
                && c.secondItem as? UIView != self
                && c.firstAttribute == .width
        }
        
        func _ba(_ c: NSLayoutConstraint)->Bool {
            return _bothviews(c)
                && c.firstItem as? UIView != self
                && c.secondItem as? UIView == self
                && c.secondAttribute == .width
        }
        
        // note that .relation could be anything: and we don't mind that
        
        return self.allConstraints()
            .filter({ _ab($0) || _ba($0) })
    }

     func xPositionConstraints()->[NSLayoutConstraint] {

         return self.allConstraints()
            .filter({
         return $0.firstAttribute == .centerX || $0.secondAttribute == .centerX
         })
    }
}

extension NSLayoutConstraint {
    
    // typical routine to "change" multiplier fraction...
    
    @discardableResult
    func changeToNewConstraintWith(multiplier:CGFloat) -> NSLayoutConstraint {

        //NSLayoutConstraint.deactivate([self])
        self.isActive = false
        
        let nc = NSLayoutConstraint(
            item: firstItem as Any,
            attribute: firstAttribute,
            relatedBy: relation,
            toItem: secondItem,
            attribute: secondAttribute,
            multiplier: multiplier,
            constant: constant)

        nc.priority = priority
        nc.shouldBeArchived = self.shouldBeArchived
        nc.identifier = self.identifier

        //NSLayoutConstraint.activate([nc])
        nc.isActive = true
        return nc
    }
}

Just an example demo...

override func viewDidAppear(_ animated: Bool) {
    
    super.viewDidAppear(animated)
    
    _teste()
    
    delay(5) {
        print("changing any 'fraction fo another view' style widths ...\n\n")
        let cc = self.animeHolder.widthAsFractionOfAnotherViewConstraints()
        for c in cc {
            c.changeToNewConstraintWith(multiplier: 0.25)
        }
        self._teste()
    }
    
    delay(10) {
        print("changing any 'points' style widths ...\n\n")
        let cc = self.animeHolder.widthAsPointsConstraints()
        for c in cc {
            c.constant = 150.0
        }
        self._teste()
    }
}

func _teste() {
    
    print("\n---- allConstraints")
    for c in animeHolder.allConstraints() {
        print("\n \(c)")
    }
    print("\n---- widthAsPointsConstraints")
    for c in animeHolder.widthAsPointsConstraints() {
        print("\n \(c)\n \(c.multiplier) \(c.constant)")
    }
    print("\n---- widthAsFractionOfAnotherViewConstraints")
    for c in animeHolder.widthAsFractionOfAnotherViewConstraints() {
        print("\n \(c)\n \(c.multiplier) \(c.constant)")
    }
    print("\n----\n")
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM