歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~javascript
本文由 鵝廠新鮮事兒發表於 雲+社區專欄
做者:oceanlong | 騰訊 移動客戶端開發工程師前端
UI佈局是整個前端體系裏不可或缺的一環。代碼的佈局是設計語言與用戶視覺感覺溝通的橋樑,不論它看起來多麼簡單或是瑣碎,但不得不認可,絕大部分軟件開發的問題,都是界面問題。那麼,如何高效的完成UI開發,也是軟件行業一直在克服的問題。java
因此,軟件界面開發的核心點便是:如何減小UI設計稿的建模難度和減小建模轉化到代碼的實現難度git
最初iOS提供了平面直角座標系的方式,來解決佈局問題,即所謂的手動佈局。平面直角座標系確實是一套完備在理論,這在數學上已經驗證過了,只要咱們的屏幕仍是平面,它就確定是有效的。但有效不必定高效,咱們在平常的生活中,不多會用平面直角座標系來向人描述位置關係。更多的是依靠相對位置。github
所幸,iOS爲咱們提供自動佈局的方法,來解決這一困境。app
其實說到本質,它和手動佈局是同樣的。對一個控件放在哪裏,咱們依然只關心它的(x, y, width, height)
。但手動佈局的方式是,一次性計算出這四個值,而後設置進去,完成佈局。但當父控件或屏幕發生變化時,子控件的計算就要從新來過,很是麻煩。框架
所以,在自動佈局中,咱們再也不關心(x, y, width, height)
的具體值,咱們只關心(x, y, width, height)
四個量對應的約束。機器學習
那麼何爲約束呢?ide
obj1.property1 =(obj2.property2 * multiplier)+ constant value
子控件的某一個量必定與另外一個控件的某一個量呈線性關係,這就是約束。函數
那麼,給(x, y, width, height)
四個量,分別給一個約束,就能夠肯定一個控件的最終位置。
//建立左邊約束 NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc];
這一段代碼便是:控件(blueView)的 x = rootView的x * 1.0 + 20這裏必定要注意,這樣的一條約束,涉及了子控件和父控件,因此這條約束必定要添加到父控件中。
//關閉Autoresizing blueView.translatesAutoresizingMaskIntoConstraints = NO; //建立左邊約束 NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc]; //建立右邊約束 NSLayoutConstraint *rightLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeRight multiplier:1.0 constant:-20]; [self.view addConstraint:rightLc]; //建立底部約束 NSLayoutConstraint *bottomLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-20]; [self.view addConstraint:bottomLc]; //建立高度約束 NSLayoutConstraint *heightLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:50]; [blueView addConstraint: heightLc];
咱們注意到,自動佈局其實工做分兩步:
到這裏,咱們只是解決了如何減小UI設計稿的建模難度的問題,顯然,減小建模轉化到代碼的實現難度這個效果沒能達成。關於如何解決減小建模轉化到代碼的實現難度的問題,
上面的代碼,咱們能夠看到,雖然自動佈局已經比手動佈局優雅很多了,但它依然行數較多。每條約束大約都須要三行代碼,面對複雜的頁面,這樣開發出來,會很難閱讀。
Masonry則爲咱們解決了這個問題。
咱們選擇使用Cocoapods的方式。引入比較簡單:
2.編輯Podfile
其中,'IosOcDemo'就是咱們工程的名字,根據須要,咱們自行替換。
3.添加依賴
完成後,執行指令pod install
。CocoaPods就會爲咱們自動下載並添加依賴。
這樣的一個代碼,用手動佈局,咱們大體的代碼應該是這樣:
-(void)initBottomView { self.bottomBarView = [[UIView alloc]initWithFrame:CGRectZero]; self.bottomButtons = [[NSMutableArray alloc]init]; _bottomBarView.backgroundColor = [UIColor yellowColor]; [self addSubview:_bottomBarView]; for(int i = 0 ; i < 3 ; i++) { UIButton *button = [[UIButton alloc]initWithFrame:CGRectZero]; button.backgroundColor = [UIColor redColor]; [_bottomButtons addObject:button]; [self addSubview:button]; } } -(void)layoutBottomView { _bottomBarView.frame = CGRectMake(20, _viewHeight - 200, _viewWidth - 40, 200); for (int i = 0 ; i < 3; i++) { UIButton *button = _bottomButtons[i]; CGFloat x = i * (_viewWidth - 40 - 20 * 4) / 3 + 20*(i+1) + 20; CGFloat y = _viewHeight - 200; CGFloat width = (_viewWidth - 40 - 20 * 4) / 3; CGFloat height = 200; button.frame = CGRectMake(x, y, width, height); } }
咱們來看一下,在Masonry的幫助下,咱們能夠把剛剛的代碼寫成什麼樣的:
-(void)initBottomView { _bottomBarView = [[UIView alloc]initWithFrame:CGRectZero]; _bottomBarView.backgroundColor = [UIColor yellowColor]; _bottomBarView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_bottomBarView]; [_bottomBarView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self); }]; _bottomButtons = [[NSMutableArray alloc]init]; for(int i = 0 ; i < 3 ; i++) { UIButton *button = [[UIButton alloc]initWithFrame: CGRectZero]; button.backgroundColor = [UIColor redColor]; button.translatesAutoresizingMaskIntoConstraints = NO; [_bottomButtons addObject:button]; [_bottomBarView addSubview:button]; [button mas_makeConstraints:^(MASConstraintMaker *make) { if (i == 0) { make.left.mas_equalTo(20); }else{ UIButton *previousButton = _bottomButtons[i-1]; make.left.equalTo(previousButton.mas_right).with.offset(20); } make.top.mas_equalTo(_bottomBarView.mas_top); make.width.equalTo(_bottomBarView.mas_width).with.multipliedBy(1.0f/3).offset(-20*4/3); make.height.equalTo(_bottomBarView.mas_height); }]; } }
咱們能夠看到在Masonry的封裝下,代碼變得很是簡練易讀,須要行數略有增長,可是計算過程減小了,咱們能更加關注於多個UIView間的位置關係,這與當前的UI設計語言是契合的。因此Masonry可否讓咱們更直觀地表達UI。
Masonry的封裝頗有魅力,那麼,咱們能夠簡單地來看一下,它是如何封裝的。咱們再仔細看一下Masonry的API會發現,咱們是直接在UIView上進行調用的。也就是說,Masonry對UIView進行了擴展。
在View+MASUtilities.h中:
#if TARGET_OS_IPHONE || TARGET_OS_TV #import <UIKit/UIKit.h> #define MAS_VIEW UIView #define MAS_VIEW_CONTROLLER UIViewController #define MASEdgeInsets UIEdgeInsets
而後在View+MASAdditions.h中,咱們看到了Masonry的擴展:
#import "MASUtilities.h" #import "MASConstraintMaker.h" #import "MASViewAttribute.h" /** * Provides constraint maker block * and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs */ @interface MAS_VIEW (MASAdditions) /** * following properties return a new MASViewAttribute with current view and appropriate NSLayoutAttribute */ @property (nonatomic, strong, readonly) MASViewAttribute *mas_left; @property (nonatomic, strong, readonly) MASViewAttribute *mas_top; @property (nonatomic, strong, readonly) MASViewAttribute *mas_right; @property (nonatomic, strong, readonly) MASViewAttribute *mas_bottom; @property (nonatomic, strong, readonly) MASViewAttribute *mas_leading; @property (nonatomic, strong, readonly) MASViewAttribute *mas_trailing; @property (nonatomic, strong, readonly) MASViewAttribute *mas_width; @property (nonatomic, strong, readonly) MASViewAttribute *mas_height; @property (nonatomic, strong, readonly) MASViewAttribute *mas_centerX; @property (nonatomic, strong, readonly) MASViewAttribute *mas_centerY; @property (nonatomic, strong, readonly) MASViewAttribute *mas_baseline; @property (nonatomic, strong, readonly) MASViewAttribute *(^mas_attribute)(NSLayoutAttribute attr); ... /** * Creates a MASConstraintMaker with the callee view. * Any constraints defined are added to the view or the appropriate superview once the block has finished executing * * @param block scope within which you can build up the constraints which you wish to apply to the view. * * @return Array of created MASConstraints */ - (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
一些,適配的代碼,我省略了,先看核心代碼。在剛剛的例子中,咱們正是調用的mas_makeConstraints
方法。
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; }
mas_makeConstraints
方法比較簡單,只是封裝了MASConstraintMaker
初始化,設置約束和安裝。這裏的block就是咱們剛剛在外層設置的約束的函數指針。也就是這一串:
^(MASConstraintMaker *make) { make.left.equalTo(self.view).with.offset(10); make.right.equalTo(self.view).with.offset(-10); make.height.mas_equalTo(50); make.bottom.equalTo(self.view).with.offset(-10); }
因爲約束條件的設置比較複雜,咱們先來看看初始化和安裝。
- (id)initWithView:(MAS_VIEW *)view { self = [super init]; if (!self) return nil; self.view = view; self.constraints = NSMutableArray.new; return self; }
初始化的代碼比較簡單,將傳入的view放入MASConstraintMaker
成員,而後建立MASConstraintMaker
的約束容器(NSMutableArray)。
- (NSArray *)install { if (self.removeExisting) { NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view]; for (MASConstraint *constraint in installedConstraints) { [constraint uninstall]; } } NSArray *constraints = self.constraints.copy; for (MASConstraint *constraint in constraints) { constraint.updateExisting = self.updateExisting; [constraint install]; } [self.constraints removeAllObjects]; return constraints; }
安裝的代碼分爲三塊:
uninstall
copy
已有的約束,遍歷,並逐一install
remove
掉全部約束,並將已添加的constraints
返回。install
的方法,仍是繼續封裝到了Constraint
中,咱們繼續跟進閱讀:
咱們會發現Constraint
只是一個接口,Masonry中對於Constraint
接口有兩個實現,分別是:MASViewConstraint
和MASCompositeConstraint
。這兩個類,分別是單個約束和約束集合。在上面的例子中,咱們只是對單個UIView
進行約束,因此咱們先看MASViewConstraint
的代碼。如下代碼MASViewConstraint
進行了必定程度的簡化,省略了一些擴展屬性,只展現咱們的例子中,會執行的代碼:
- (void)install { if (self.hasBeenInstalled) { return; } ... MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item; NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute; MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item; NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute; // alignment attributes must have a secondViewAttribute // therefore we assume that is refering to superview // eg make.left.equalTo(@10) if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) { secondLayoutItem = self.firstViewAttribute.view.superview; secondLayoutAttribute = firstLayoutAttribute; } MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:firstLayoutItem attribute:firstLayoutAttribute relatedBy:self.layoutRelation toItem:secondLayoutItem attribute:secondLayoutAttribute multiplier:self.layoutMultiplier constant:self.layoutConstant]; layoutConstraint.priority = self.layoutPriority; layoutConstraint.mas_key = self.mas_key; if (self.secondViewAttribute.view) { MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view]; NSAssert(closestCommonSuperview, @"couldn't find a common superview for %@ and %@", self.firstViewAttribute.view, self.secondViewAttribute.view); self.installedView = closestCommonSuperview; } else if (self.firstViewAttribute.isSizeAttribute) { self.installedView = self.firstViewAttribute.view; } else { self.installedView = self.firstViewAttribute.view.superview; } MASLayoutConstraint *existingConstraint = nil; ... else { [self.installedView addConstraint:layoutConstraint]; self.layoutConstraint = layoutConstraint; [firstLayoutItem.mas_installedConstraints addObject:self]; } }
自動佈局是一種相對佈局,因此,絕大部分狀況下,須要兩個UIView
(約束方與參照方)。在上面的方法中:
firstLayoutItem
是約束方,secondLayoutItem
是參照方firstLayoutAttribute
是約束方的屬性,secondLayoutAttribute
是參照方的屬性。MASLayoutConstraint
就是NSLayoutConstraint
的子類,只是添加了mas_key屬性。到這裏,咱們就與系統提供的API對應上了。NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc];
再看看咱們以前用系統API完成的例子,是否是格外熟悉?
那麼接下來,咱們就是要閱讀
make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self);
是如何變成firstLayoutItem
, secondLayoutItem
, firstLayoutAttribute
, secondLayoutAttribute
和layoutRelation
的。
回到前面的:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; }
咱們接下來,就要看block的實現:
block
實際上是一個函數指針。此處真正調用的方法是:
make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self);
咱們挑選其中一個,來看看源碼實現:
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute]; } - (MASConstraint *)left { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]; } - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute]; MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute]; if ([constraint isKindOfClass:MASViewConstraint.class]) { //replace with composite constraint NSArray *children = @[constraint, newConstraint]; MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self; [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } if (!constraint) { newConstraint.delegate = self; [self.constraints addObject:newConstraint]; } return newConstraint; }
在對單個view
添加約束時,constraint
爲nil。咱們直接生成了一個新約束newConstraint
。它的firstViewAttribute
就是咱們傳入的NSLayoutAttributeLeft
- (MASConstraint * (^)(id))equalTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationEqual); }; } - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { return ^id(id attribute, NSLayoutRelation relation) { if ([attribute isKindOfClass:NSArray.class]) { NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation"); NSMutableArray *children = NSMutableArray.new; for (id attr in attribute) { MASViewConstraint *viewConstraint = [self copy]; viewConstraint.layoutRelation = relation; viewConstraint.secondViewAttribute = attr; [children addObject:viewConstraint]; } MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self.delegate; [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } else { NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation"); self.layoutRelation = relation; self.secondViewAttribute = attribute; return self; } }; }
此處,咱們依然先看attribute不是NSArray
的狀況。這裏在單個屬性的約束中,就比較簡單了,將relation
和attribue
傳入MASConstraint
對應的成員。
在上面介紹install
方法時,咱們就曾提到過:
MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:firstLayoutItem attribute:firstLayoutAttribute relatedBy:self.layoutRelation toItem:secondLayoutItem attribute:secondLayoutAttribute multiplier:self.layoutMultiplier constant:self.layoutConstant];
firstLayoutItem
和secondLayoutItem
在install
方法中已收集完成,此時,通過left
和equalTo
咱們又收集到了:firstViewAttribute
、secondViewAttribute
和layoutRelation
勝利即在眼前。
- (MASConstraint * (^)(CGFloat))offset { return ^id(CGFloat offset){ self.offset = offset; return self; }; } - (void)setOffset:(CGFloat)offset { self.layoutConstant = offset; }
經過OC的set語法,Masonry
將offset傳入layoutConstant。
至此,layoutConstraint
就完成了所有的元素收集,可使用添加約束的方式,只須要解決最後一個問題,約束添加到哪裏呢?咱們彷佛在調用時,並不須要關心這件事情,那說明框架幫咱們完成了這個工做。
咱們在MASViewConstraint中,能夠找到這樣一段:
if (self.secondViewAttribute.view) { MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view]; NSAssert(closestCommonSuperview, @"couldn't find a common superview for %@ and %@", self.firstViewAttribute.view, self.secondViewAttribute.view); self.installedView = closestCommonSuperview; } else if (self.firstViewAttribute.isSizeAttribute) { self.installedView = self.firstViewAttribute.view; } else { self.installedView = self.firstViewAttribute.view.superview; }
注意到,closetCommonSuperview
就是Masonry爲咱們找到的最近公共父控件。
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view { MAS_VIEW *closestCommonSuperview = nil; MAS_VIEW *secondViewSuperview = view; while (!closestCommonSuperview && secondViewSuperview) { MAS_VIEW *firstViewSuperview = self; while (!closestCommonSuperview && firstViewSuperview) { if (secondViewSuperview == firstViewSuperview) { closestCommonSuperview = secondViewSuperview; } firstViewSuperview = firstViewSuperview.superview; } secondViewSuperview = secondViewSuperview.superview; } return closestCommonSuperview; }
實現也比較簡單。
至此,咱們完成了全部準備,就能夠開始愉快的自動佈局啦。
以上就是Masonry對iOS自動佈局封裝的解讀。
若有問題,歡迎指正。
問答
iOS:如何使用自動佈局約束?
相關閱讀
走進 Masonry
iOS自動佈局框架之Masonry
iOS學習——佈局利器Masonry框架源碼深度剖析
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識
此文已由做者受權騰訊雲+社區發佈,更多原文請點擊
搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社區!