最近在使用snapkit過程中遇到一個問題,在github上搜索之後發現另外一個有趣的問題 看起來很理所當然的,明顯不可以這樣寫,但是具體是什麼原因呢,明明沒有報任何錯誤和警告,但是.multipliedBy()方法卻沒有效果,那我們來看一下snapkit源碼。 1.首先點進equalTo()方法, ...
最近在使用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給當前對象綁定一個同名的屬性,每次使用時獲取綁定的屬性的值,不懂,希望知道的同學不吝賜教。