最近在使用snapkit过程中遇到一个问题,在github上搜索之后发现另外一个有趣的问题 问题链接
- frameImageContainer.snp.makeConstraints({ (make) in
- make.width.equalTo(295).multipliedBy(0.2)
- make.height.equalTo(355).multipliedBy(0.2)
- make.top.equalToSuperview().offset(self.view.frame.height/8)
- make.centerX.equalToSuperview();
- })
看起来很理所当然的,明显不可以这样写,但是具体是什么原因呢,明明没有报任何错误和警告,但是.multipliedBy()方法却没有效果,那我们来看一下snapkit源码。
1.首先点进equalTo()方法,代码是这样的:
- @discardableResult
- public func equalTo(_ other: ConstraintRelatableTarget, _ file: String = #file, _ line: UInt = #line) -> ConstraintMakerEditable {
- return self.relatedTo(other, relation: .equal, file: file, line: line)
- }
再点进relatedTo()方法:
- internal func relatedTo(_ other: ConstraintRelatableTarget, relation: ConstraintRelation, file: String, line: UInt) -> ConstraintMakerEditable {
- let related: ConstraintItem
- let constant: ConstraintConstantTarget
-
- if let other = other as? ConstraintItem {
- guard other.attributes == ConstraintAttributes.none ||
- other.attributes.layoutAttributes.count <= 1 ||
- other.attributes.layoutAttributes == self.description.attributes.layoutAttributes ||
- other.attributes == .edges && self.description.attributes == .margins ||
- other.attributes == .margins && self.description.attributes == .edges else {
- fatalError("Cannot constraint to multiple non identical attributes. (\(file), \(line))");
- }
-
- related = other
- constant = 0.0
- } else if let other = other as? ConstraintView {
- related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
- constant = 0.0
- } else if let other = other as? ConstraintConstantTarget {
- related = ConstraintItem(target: nil, attributes: ConstraintAttributes.none)
- constant = other
- } else if #available(iOS 9.0, OSX 10.11, *), let other = other as? ConstraintLayoutGuide {
- related = ConstraintItem(target: other, attributes: ConstraintAttributes.none)
- constant = 0.0
- } else {
- fatalError("Invalid constraint. (\(file), \(line))")
- }
-
- let editable = ConstraintMakerEditable(self.description)
- editable.description.sourceLocation = (file, line)
- editable.description.relation = relation
- editable.description.related = related
- editable.description.constant = constant
- return editable
- }
可以看到上面红色部分,此时other可以转换为ConstraintConstantTarget类型,设置related的target为nil,attributes为none,constant设置为other,最后将这些变量赋值给description属性保存。
2.multipliedBy()方法:
- @discardableResult
- public func multipliedBy(_ amount: ConstraintMultiplierTarget) -> ConstraintMakerEditable {
- self.description.multiplier = amount
- return self
- }
可以看到,multipliedBy方法中只是简单的赋值,将amout值复制到description中保存。
3.再来看一下makeConstraints()方法:
- internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
- let maker = ConstraintMaker(item: item)
- closure(maker)
- var constraints: [Constraint] = []
- for description in maker.descriptions {
- guard let constraint = description.constraint else {
- continue
- }
- constraints.append(constraint)
- }
- for constraint in constraints {
- constraint.activateIfNeeded(updatingExisting: false)
- }
- }
首先将要添加约束的对象封装成ConstraintMaker对象,再把它作为参数调用closure,开始添加约束。closure中的每条语句会先被解析ConstraintDescription对象,添加到maker的descriptions属性中。 在第一个for循环中可以看到,每次循环从descriptions中取出一条description,判断是否改description是否有constraint属性;
constraint属性使用的懒加载,值为:
- internal lazy var constraint: Constraint? = {
- guard let relation = self.relation,
- let related = self.related,
- let sourceLocation = self.sourceLocation else {
- return nil
- }
- let from = ConstraintItem(target: self.item, attributes: self.attributes)
-
- return Constraint(
- from: from,
- to: related,
- relation: relation,
- sourceLocation: sourceLocation,
- label: self.label,
- multiplier: self.multiplier,
- constant: self.constant,
- priority: self.priority
- )
- }()
先判断relation,related,sourceLocation的值是否为nil,若为nil则返回nil,否则就根据description属性创建一个Constraint对象并返回。
Constraint的构造方法:
- internal init(from: ConstraintItem,
- to: ConstraintItem,
- relation: ConstraintRelation,
- sourceLocation: (String, UInt),
- label: String?,
- multiplier: ConstraintMultiplierTarget,
- constant: ConstraintConstantTarget,
- priority: ConstraintPriorityTarget) {
- self.from = from
- self.to = to
- self.relation = relation
- self.sourceLocation = sourceLocation
- self.label = label
- self.multiplier = multiplier
- self.constant = constant
- self.priority = priority
- self.layoutConstraints = []
- // get attributes
- let layoutFromAttributes = self.from.attributes.layoutAttributes
- let layoutToAttributes = self.to.attributes.layoutAttributes
- // get layout from
- let layoutFrom = self.from.layoutConstraintItem!
-
- // get relation
- let layoutRelation = self.relation.layoutRelation
- for layoutFromAttribute in layoutFromAttributes {
- // get layout to attribute
- let layoutToAttribute: LayoutAttribute
- #if os(iOS) || os(tvOS)
- if layoutToAttributes.count > 0 {
- if self.from.attributes == .edges && self.to.attributes == .margins {
- switch layoutFromAttribute {
- case .left:
- layoutToAttribute = .leftMargin
- case .right:
- layoutToAttribute = .rightMargin
- case .top:
- layoutToAttribute = .topMargin
- case .bottom:
- layoutToAttribute = .bottomMargin
- default:
- fatalError()
- }
- } else if self.from.attributes == .margins && self.to.attributes == .edges {
- switch layoutFromAttribute {
- case .leftMargin:
- layoutToAttribute = .left
- case .rightMargin:
- layoutToAttribute = .right
- case .topMargin:
- layoutToAttribute = .top
- case .bottomMargin:
- layoutToAttribute = .bottom
- default:
- fatalError()
- }
- } else if self.from.attributes == self.to.attributes {
- layoutToAttribute = layoutFromAttribute
- } else {
- layoutToAttribute = layoutToAttributes[0]
- }
- } else {
- if self.to.target == nil && (layoutFromAttribute == .centerX || layoutFromAttribute == .centerY) {
- layoutToAttribute = layoutFromAttribute == .centerX ? .left : .top
- } else {
- layoutToAttribute = layoutFromAttribute
- }
- }
- #else
- if self.from.attributes == self.to.attributes {
- layoutToAttribute = layoutFromAttribute
- } else if layoutToAttributes.count > 0 {
- layoutToAttribute = layoutToAttributes[0]
- } else {
- layoutToAttribute = layoutFromAttribute
- }
- #endif
-
- // get layout constant
- let layoutConstant: CGFloat = self.constant.constraintConstantTargetValueFor(layoutAttribute: layoutToAttribute)
- // get layout to
- var layoutTo: AnyObject? = self.to.target
- // use superview if possible
- if layoutTo == nil && layoutToAttribute != .width && layoutToAttribute != .height {
- layoutTo = layoutFrom.superview
- }
- // create layout constraint
- let layoutConstraint = LayoutConstraint(
- item: layoutFrom,
- attribute: layoutFromAttribute,
- relatedBy: layoutRelation,
- toItem: layoutTo,
- attribute: layoutToAttribute,
- multiplier: self.multiplier.constraintMultiplierTargetValue,
- constant: layoutConstant
- )
- // set label
- layoutConstraint.label = self.label
- // set priority
- layoutConstraint.priority = LayoutPriority(rawValue: self.priority.constraintPriorityTargetValue)
- // set constraint
- layoutConstraint.constraint = self
- // append
- self.layoutConstraints.append(layoutConstraint)
- }
- }
重点看红色部分,遍历layoutAttributes,并根据layoutAttribute的值生成一个LayoutConstraint对象添加到layoutConstraints数组中。LayoutConstraint继承自系统类NSLayoutConstraint。
4.最后再看3中的第二个for循环,使用activeIfNeeded()方法激活约束:
- internal func activateIfNeeded(updatingExisting: Bool = false) {
- guard let item = self.from.layoutConstraintItem else {
- print("WARNING: SnapKit failed to get from item from constraint. Activate will be a no-op.")
- return
- }
- let layoutConstraints = self.layoutConstraints
- if updatingExisting {
- var existingLayoutConstraints: [LayoutConstraint] = []
- for constraint in item.constraints {
- existingLayoutConstraints += constraint.layoutConstraints
- }
- for layoutConstraint in layoutConstraints {
- let existingLayoutConstraint = existingLayoutConstraints.first { $0 == layoutConstraint }
- guard let updateLayoutConstraint = existingLayoutConstraint else {
- fatalError("Updated constraint could not find existing matching constraint to update: \(layoutConstraint)")
- }
- let updateLayoutAttribute = (updateLayoutConstraint.secondAttribute == .notAnAttribute) ? updateLayoutConstraint.firstAttribute : updateLayoutConstraint.secondAttribute
- updateLayoutConstraint.constant = self.constant.constraintConstantTargetValueFor(layoutAttribute: updateLayoutAttribute)
- }
- } else {
- NSLayoutConstraint.activate(layoutConstraints)
- item.add(constraints: [self])
- }
- }
当updatingExisting为false时,进入else语句,使用的系统类NSLayoutConstraint的方法激活约束:
- /* Convenience method that activates each constraint in the contained array, in the same manner as setting active=YES. This is often more efficient than activating each constraint individually. */
- @available(iOS 8.0, *)
- open class func activate(_ constraints: [NSLayoutConstraint])
并将设置过的约束添加到item的constraintSet这个私有属性中:
- internal var constraints: [Constraint] {
- return self.constraintsSet.allObjects as! [Constraint]
- }
-
- internal func add(constraints: [Constraint]) {
- let constraintsSet = self.constraintsSet
- for constraint in constraints {
- constraintsSet.add(constraint)
- }
- }
-
- internal func remove(constraints: [Constraint]) {
- let constraintsSet = self.constraintsSet
- for constraint in constraints {
- constraintsSet.remove(constraint)
- }
- }
-
- private var constraintsSet: NSMutableSet {
- let constraintsSet: NSMutableSet
-
- if let existing = objc_getAssociatedObject(self, &constraintsKey) as? NSMutableSet {
- constraintsSet = existing
- } else {
- constraintsSet = NSMutableSet()
- objc_setAssociatedObject(self, &constraintsKey, constraintsSet, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
- }
- return constraintsSet
-
- }
5.通过这个过程不难发现,使用make.width.equalTo(295).multipliedBy(0.2) 这种方式不能得到想要的结果。在3中Constraint的构造方法的红色部分,其实构造LayoutConstraint对象时调用的NSLayoutConstraint的便利构造器方法:
- /* Create constraints explicitly. Constraints are of the form "view1.attr1 = view2.attr2 * multiplier + constant"
- If your equation does not have a second view and attribute, use nil and NSLayoutAttributeNotAnAttribute.
- */
- public convenience init(item view1: Any, attribute attr1: NSLayoutConstraint.Attribute, relatedBy relation: NSLayoutConstraint.Relation, toItem view2: Any?, attribute attr2: NSLayoutConstraint.Attribute, multiplier: CGFloat, constant c: CGFloat)
注意上面注释 view1.attr1 = view2.attr2 * multiplier + constant 如果只设置为数字,则相当于view2为nil,所以view1的属性值只能等于constant的值,不会乘以multiplier。
6.终于写完了,哈哈。 demo工程地址,对应的方法已打断点,可以跟着代码一步步调试,有助于理解。
疑问:在4中最后一部分红色字体的内容,私有属性constraintsSet为啥不直接使用,还要使用runtime给当前对象绑定一个同名的属性,每次使用时获取绑定的属性的值,不懂,希望知道的同学不吝赐教。