Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Subclassing NSLayoutConstraint, bizarre behavior of constants

On whatever project you're currently working on, simply

  1. On any screen drop in a UIView

  2. Just add a width constraint (666 or whatever is fine),

  3. Change the custom class of the constraint, to Constrainty

  4. Run the app,

enter image description here

How can this possibly be?

Does it actually call for the value of the constant, "before the class is initialized," or some such?

How can it happen, and how to solve?

At first, I had the two variables as @IBInspectable and I was surprised that didn't work at all. But then I changed them to ordinary let constants - and that doesn't work!

class Constrainty: NSLayoutConstraint {

    let percentageWidth: CGFloat = 77 // nothing up my sleeve
    let limitWidth: CGFloat = 350

    override var constant: CGFloat {

        get { return teste() }
        set { super.constant = newValue }
    }

    func teste()->CGFloat {

        print("\n\n HERE WE GO \(percentageWidth) \(limitWidth) \n\n")

        if let sw = (firstItem as? UIView)?.superview?.bounds.width {

            let w = sw * ( percentageWidth / 100.0 )
            let r = w > limitWidth ? limitWidth : w
            print("result is \(r) \n\n")
            return r
        }

        return 50
    }

}
like image 779
Fattie Avatar asked Jan 04 '18 18:01

Fattie


2 Answers

I don't think it's a good idea to subclass NSLayoutConstraint. I don't think it was designed to be subclassed outside of Apple.

Anyway, the problem is that NSLayoutConstraint conforms to the NSCoding protocol, but doesn't declare that it conforms to NSCoding in the header files. Because of this, Swift doesn't know that NSLayoutConstraint can be initialized by -[NSLayoutConstraint initWithCoder:], so it doesn't generate an override of initWithCoder: that initializes the instance variables you add in your subclass.

Here's how to fix it.

First, if your project doesn't have a bridging header, add one. The easiest way to add one is to create a new Objective-C class in your project, accept Xcode's offer to create the bridging header, then delete the .h and .m files it created for the class (but keep the bridging header).

Then, in the bridging header, declare that NSLayoutConstraint conforms to NSCoding:

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

@import UIKit;

@interface NSLayoutConstraint (MyProject) <NSCoding>
@end

Finally, in your Constrainty class, override init(coder:) like this:

required init(coder decoder: NSCoder) {
    super.init(coder: decoder)!
}

Et voila:

 HERE WE GO 77.0 350.0 


result is 246.4 
like image 183
rob mayoff Avatar answered Nov 15 '22 22:11

rob mayoff


My wild guess it's all because of a class named UIClassSwapper. It's a private class that handles all the UI objects initialization from the Interface Builder files. I would suggest to replace your let constants with computed properties.

//...
var percentageWidht: CGFloat { // nothing up my sleeve
    return 77.0
}

var limitWidth: CGFloat {
    return 110.0
}
//...

UPD

Swift default property values(properties with values in their declaration) are being set before the initializer call. E.G. if you have a class MyClass with a property let someVar: CGFloat = 12.0 and it's bridged to Objective-C, when you allocate memory for your object and do not call an initializer MyClass *obj = [MyClass alloc] your variable will have a default value of 0.0 and will stay so unless you’ll call an initializer like [obj init]. So my second wild guess is that because NSLayoutConstraint class is written in Objective-C and it's initWithCoder: initializer isn't declared in it's header(it's private), the ObjC-Swift bridging mechanism doesn't recognize it's call as an initializer call(it thinks it is just a simple instance method), so your Swift properties with default values aren't being initialized at all.

like image 39
Vahan Babayan Avatar answered Nov 15 '22 23:11

Vahan Babayan