iOS SnapKit源碼解析(一)makeConstraints的過程

寫在前面

經(jīng)過激烈的思想斗爭烫幕,筆者從Android開發(fā)轉(zhuǎn)到了iOS開發(fā),發(fā)現(xiàn)了兩者很多的共同之處,這里就不在贅述庐镐;不過最大的不適應(yīng)體現(xiàn)在UI方面,Android的布局編寫和預(yù)覽更舒適变逃。

萬般無奈之下必逆,接觸到了SnapKit,一個用Swift編寫的AutoLayout框架揽乱,極大程度上簡化了純布局的代碼名眉。

分析源碼

本文只探究makeConstraints的過程,也就是停留在閉包之外凰棉。

ConstraintView

SnapKit的最基本用法:

view.snp.makeConstraints { (make) in

}

首先view.snp很容易讓人想到是使用了擴(kuò)展损拢,但并不是直接對UIView的擴(kuò)展,而是要引入一個新的概念ConstraintView撒犀,具體情況在ConstraintView.swift中體現(xiàn):

#if os(iOS) || os(tvOS)
    import UIKit
#else
    import AppKit
#endif

#if os(iOS) || os(tvOS)
    public typealias ConstraintView = UIView
#else
    public typealias ConstraintView = NSView
#endif

這就是該文件所有的代碼了探橱,可以看到申屹,通過判斷當(dāng)前系統(tǒng)做了兩件事:

  1. 包的導(dǎo)入:如果當(dāng)前系統(tǒng)是iOS或者tvOS,那么導(dǎo)入UIKit隧膏,否則導(dǎo)入AppKit
  2. 類的重命名:如果當(dāng)前系統(tǒng)是iOS或者tvOS哗讥,那么將UIView重命名為ConstraintView,否則將NSView重命名為ConstraintView胞枕。其中typealias用于為已存在的類重新命名杆煞,提高代碼的可讀性。

總而言之腐泻,ConstraintView是為了適配多平臺而定義的UIViewNSView的別稱决乎。

extension ConstraintView

緊接上文,view.snp是對ConstraintView的擴(kuò)展派桩,在ConstraintView+Extensions.swift中返回:

#if os(iOS) || os(tvOS)
    import UIKit
#else
    import AppKit
#endif

public extension ConstraintView {
    // 此處略去很多廢棄的方法
    
    public var snp: ConstraintViewDSL {
        return ConstraintViewDSL(view: self)
    }
    
}

此處省略了該文件中很多被廢棄的方法构诚,只看最關(guān)鍵的變量snp,此處返回了一個新的對象ConstraintViewDSL铆惑,并以自己范嘱,一個ConstraintView作為參數(shù)。

注意:在SnapKit中员魏,幾乎所有文件開頭都有關(guān)于導(dǎo)入UIKit還是AppKit的判斷丑蛤,之后就不再展示這段重復(fù)的代碼。

ConstraintViewDSL

接下來jump到ConstraintViewDSL.swift文件中撕阎,這里只展示它的一個最關(guān)鍵方法:

public struct ConstraintViewDSL: ConstraintAttributesDSL {

    public func makeConstraints(_ closure: (_ make: ConstraintMaker) -> Void) {
        ConstraintMaker.makeConstraints(item: self.view, closure: closure)
    }
    internal let view: ConstraintView
    
    internal init(view: ConstraintView) {
        self.view = view
    }
}

首先可以看到ConstraintViewDSL是一個結(jié)構(gòu)體受裹,實現(xiàn)了ConstraintAttributesDSL接口,構(gòu)造函數(shù)也非常簡單虏束,只接收一個ConstraintView并保存起來棉饶;另外,view.snp.makeConstraints也只是把保存的ConstraintView镇匀,連同傳遞進(jìn)來的閉包一起交給ConstraintMaker處理砰盐。

除了makeConstraints方法,還有remakeConstraints坑律、updateConstraintsremoveConstraints等方法囊骤,因為都是交給ConstraintMaker處理晃择,所以不再贅述。

ConstraintMaker

ConstraintMaker.swift文件中:

public class ConstraintMaker {

    private let item: LayoutConstraintItem
    private var descriptions = [ConstraintDescription]()
    
    internal init(item: LayoutConstraintItem) {
        self.item = item
        self.item.prepare()
    }

    internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
        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)
        }
        return constraints
    }
    
    internal static func makeConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) {
        let constraints = prepareConstraints(item: item, closure: closure)
        for constraint in constraints {
            constraint.activateIfNeeded(updatingExisting: false)
        }
    }

}

ConstraintMaker是一個也物,從上面展示的代碼可以知道創(chuàng)建約束的基本流程:首先makeConstraints調(diào)用prepareConstraints宫屠,構(gòu)造一個maker,然后由閉包調(diào)用這個maker滑蚯,遍歷makerdescriptions浪蹂,將獲取的約束添加到一個約束數(shù)組constraints中抵栈,然后prepareConstraints執(zhí)行完畢并將約束返回這個constraintsmakeConstraints繼續(xù)執(zhí)行坤次,獲取這些約束古劲,然后激活。

構(gòu)造maker時缰猴,傳入構(gòu)造函數(shù)的item應(yīng)為保存在ConstraintViewDSL中的ConstraintView产艾,但在init聲明中變成了LayoutConstraintItem

LayoutConstraintItem

LayoutConstraintItem.swift

public protocol LayoutConstraintItem: class {
}

extension ConstraintView : LayoutConstraintItem {
}

可以看到這是一個協(xié)議滑绒,并且ConstraintView實現(xiàn)了它闷堡,協(xié)議中也實現(xiàn)了一些方法,其中就包括prepare

extension LayoutConstraintItem {
    internal func prepare() {
        if let view = self as? ConstraintView {
            view.translatesAutoresizingMaskIntoConstraints = false
        }
    }
}

prepare方法禁用了從AutoresizingMaskConstraints的自動轉(zhuǎn)換疑故,即translatesAutoresizingMaskIntoConstraints可以把 frame 杠览,bouds,center 方式布局的視圖自動轉(zhuǎn)化為約束形式纵势,轉(zhuǎn)化的結(jié)果就是自動添加需要的約束踱阿;而此時我們需要自己添加約束,必然會產(chǎn)生沖突吨悍,所以直接指定這個視圖不去使用約束布局扫茅。

中途休息一下

到目前為止,我們知道了調(diào)用view.snp.makeConstraints時育瓜,這個view經(jīng)過一系列轉(zhuǎn)運葫隙,最終禁用了自己的約束布局,而這個過程僅僅是prepareConstraints方法的第一行躏仇,也就是只調(diào)用了ConstraintMaker的構(gòu)造函數(shù)恋脚,接下來繼續(xù)分析prepareConstraints

    internal static func prepareConstraints(item: LayoutConstraintItem, closure: (_ make: ConstraintMaker) -> Void) -> [Constraint] {
        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)
        }
        return constraints
    }

構(gòu)造maker之后焰手,先是執(zhí)行了閉包的內(nèi)容(不在本文討論范圍內(nèi))糟描,緊接著創(chuàng)建了一個包含Constraint的數(shù)組constraints;然后遍歷包含了ConstraintDescription類型的descriptions數(shù)組(該數(shù)組是maker成員變量书妻,具體可以往上翻翻)船响,并試圖將每個description中包含的constraint添加到constraints數(shù)組中,最后返回該數(shù)組躲履。

ConstraintDescription

ConstraintDescription.swift

public class ConstraintDescription {
    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
        )
    }()
}

此處略去了很多成員變量见间,簡單來說,ConstraintDescription內(nèi)部持有一個Constraint變量工猜,需要時可以利用自己的成員變量構(gòu)造出一個Constraint并返回米诉。

Constraint

Constraint.swift中,關(guān)鍵代碼在構(gòu)造函數(shù)篷帅,略去成員變量和方法史侣,以及構(gòu)造函數(shù)中關(guān)于多平臺的適配之后拴泌,內(nèi)容精簡如下:

internal init(...) {
    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: NSLayoutAttribute
        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
            }
        }
        // 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 = self.priority.constraintPriorityTargetValue
        
        // set constraint
        layoutConstraint.constraint = self
        
        // append
        self.layoutConstraints.append(layoutConstraint)
    }
}

首先創(chuàng)建layoutConstraints來保存最后生成的所有LayoutConstraint(繼承自NSLayoutConstraint),然后獲取該約束的起始對象的約束屬性layoutFromAttributes和目標(biāo)對象的約束屬性layoutToAttributes惊橱。接下來的主要邏輯就在循環(huán)體內(nèi)蚪腐,通過遍歷起始對象的約束屬性,然后獲取目標(biāo)對象的約束屬性李皇,最終創(chuàng)建一條新的約束削茁。

至此,我們可以認(rèn)為prepareConstraints執(zhí)行完畢掉房,makeConstraints已經(jīng)獲取到了所有需要的約束茧跋,接下來要執(zhí)行最后一步:激活約束

activateIfNeeded

這是Constraint.swift中的一個方法:

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

這里首先獲取了起始目標(biāo)item,類型為LayoutConstraintItem卓囚,有變量constraintsSet來保存所有的約束瘾杭;

然后獲取了自己的layoutConstraints數(shù)組,Constraint不單指一個約束哪亿,而是layoutConstraints中所有約束的集合粥烁,或者說是snp.makeConstraints過程中的一個集合;

最后通過NSLayoutConstraint.activate激活了整個layoutConstraints數(shù)組中的約束蝇棉,并且將這些約束添加到了起始目標(biāo)的約束集合中保存起來讨阻。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市篡殷,隨后出現(xiàn)的幾起案子钝吮,更是在濱河造成了極大的恐慌,老刑警劉巖板辽,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奇瘦,死亡現(xiàn)場離奇詭異,居然都是意外死亡劲弦,警方通過查閱死者的電腦和手機耳标,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來邑跪,“玉大人次坡,你說我怎么就攤上這事』” “怎么了砸琅?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長夜赵。 經(jīng)常有香客問我,道長乡革,這世上最難降的妖魔是什么寇僧? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任摊腋,我火速辦了婚禮,結(jié)果婚禮上嘁傀,老公的妹妹穿的比我還像新娘兴蒸。我一直安慰自己,他們只是感情好细办,可當(dāng)我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布橙凳。 她就那樣靜靜地躺著,像睡著了一般笑撞。 火紅的嫁衣襯著肌膚如雪岛啸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天茴肥,我揣著相機與錄音坚踩,去河邊找鬼。 笑死瓤狐,一個胖子當(dāng)著我的面吹牛瞬铸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播础锐,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼嗓节,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了皆警?” 一聲冷哼從身側(cè)響起拦宣,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耀怜,沒想到半個月后恢着,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡财破,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年掰派,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片左痢。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡靡羡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出俊性,到底是詐尸還是另有隱情略步,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布定页,位于F島的核電站趟薄,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏典徊。R本人自食惡果不足惜杭煎,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一恩够、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧羡铲,春花似錦蜂桶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至雷恃,卻和暖如春疆股,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背褂萧。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工押桃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人导犹。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓唱凯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谎痢。 傳聞我的和親對象是個殘疾皇子磕昼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,107評論 2 356