這是一篇可能略顯枯燥的技術深度討論與實踐文章.如何把設計圖自動轉換爲對應的iOS代碼?做爲一個 iOS開發愛好者,這是我很感興趣的一個話題.最近也確實有了些許靈感,也確實取得了一點小成果,和你們分享一下.歡迎感興趣的iOS愛好者能和我一塊兒研究討論!ios
我以爲,若是真的能把一張設計圖自動轉換爲代碼,任何開發工程師都會感興趣的.單以 iOS 應用爲例, 在一個最經常使用的MVC架構的APP中,主要的代碼,無非就是集中於: M 的網絡請求部分, V的數據顯示部分, C的邏輯交互部分.對於controller控制器層,每每須要結合業務邏輯去處理,代碼量並不算大;對於Model數據模型層,咱們有 AFNetworing, RestKit, MJExtension等,能夠大大簡化網絡接口到數據模型的轉換;對於View視圖層,代碼最繁雜,最枯燥無趣,迭代最讓人頭疼的部分,又有什麼能夠憑藉呢?我沒有詳實的數據統計來確認各個iOS開發者的平常開發中,MVC各個層面,具體的時間成本如何;單從我我的角度來講, View佈局的拆分與轉換,佔據了我 70% 以上的時間.咱們公司一般是按單個完整任務來拆分工做的,單個任務的MVC三層,都是應該由一我的獨立完成.每次都把大把時間浪費在"畫UI"上,真的感受好無趣,好浪費生命;臨時遇到產品經理改動需求,可能一個對方看似更加"合理"的改動,我這邊幾乎要大動干戈!我想我對編程自己確實是感興趣的,可是成天浪費時間在 UI上,真的感受有點虛度光陰.因此說,在本不充裕的空閒裏,我一直在思考的一個命題就是: 如何實現 UI 的自動化與獨立化.編程
儘管做爲一名iOS開發人員,我依然對蘋果公司提供的開發技術及其發展方向持謹慎和保守態度.前一段時間,嘗試使用 Xib來佈局視圖,遇到一些坑,可是熟悉以後,也確實比原來單純基於絕對位置的純代碼佈局更靈活些,也更快捷些.在此期間,我研究的一個重要話題就是如何實現Xib之間的嵌套複用,即在一個Xib上如何直接嵌入另外一個Xib.乍聽起來很簡單,可是在親身實踐以後,才發現其難度.我不是來吐槽的,箇中曲折再也不一一贅述,下面是我研究的成果:網絡
上圖,是一個Xib模塊,其中的色塊部分,嵌套的是另外一個Xib模塊.最終顯示是,色塊會自動被對應的Xib模塊替代.架構
核心代碼以下:模塊化
// // MCComponent.h // iOS122 // // Created by 顏風 on 15/7/5. // Copyright (c) 2015年 iOS122. All rights reserved. // #import "MCConstants.h" /** * 可複用組件.用於編寫可嵌套的 xib 組件. * * 適用場景: 須要靜態肯定佈局的頁面內的UI元素的複用性問題. * 使用方法: 在xib或storyboard中,將某一用於佔位的view的 custom class 設爲對一個的 component, 則初始化時,會自動使用此component對應的xib文件中的內容去替換對應位置. * 注意: 對於可動態肯定佈局的部分,如tableView中的cell,直接自行從xib初始化便可,沒必要繼承於 MCComponent. */ @interface MCComponent : UIView @property (strong, nonatomic) UIView * contentView; //!< 真正的內容視圖. @property (weak, nonatomic, readonly) UIViewController * viewController; //!< 當前視圖所在的控制器. @property (weak, nonatomic, readonly)NSLayoutConstraint * heightContronstraint; //!< 高度的約束.不存在,則返回nil. @property (strong, nonatomic) id virtualModel; //!< 虛擬model.用於測試.默認返回nil.當不爲nil,優先使用它. @property (strong, nonatomic) id model; //!< 視圖數據模型.內部會自動根據virtualModel的值,進行不一樣的處理. @property (assign, nonatomic, readonly) BOOL isTest; //!< 是不是測試.若是是,將優先使用 virtualModel來替換model.系統內部處理.默認爲NO. /** * 初始化. * * 子類須要繼承此方法,以完成自定義初始化操做. 不要手動調用此方法. */ - (void)setup; /** * 從新加載數據. * * 子類可根據須要,具體實現此方法. */ - (void)reloadData; /** * 返回上一級. */ - (void) back; /** * 便利構造器.子類應根據須要重寫. * * @return 默認返回self. */ + (instancetype)sharedInstance; /** * 更新視圖. * * 子類應根據須要重寫此方法.默認不作任何處理. */ - (void) updateView; @end
// // MCComponent.m // iOS122 // // Created by 顏風 on 15/7/5. // Copyright (c) 2015年 iOS122. All rights reserved. // #import "MCComponent.h" @interface MCComponent () @end @implementation MCComponent @dynamic virtualModel; @synthesize model = _model; - (instancetype)init { self = [super init]; if (nil != self) { [self mcSetup: NO]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame: frame]; if (nil != self) { [self mcSetup: NO]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (nil != self) { [self mcSetup: YES]; } return self; } /** * 是否從xib初始化此類. * * @param isFromXib 是否從xib或sb初始化此類. * * 注意: 不管此類是否從xib或sb初始化,組件內部都將從xib文件初始化. * * @return 實例對象. */ - (instancetype) mcSetup: (BOOL) isFromXib { UIView * contentView = [[[NSBundle mainBundle] loadNibNamed: NSStringFromClass([self class]) owner:self options:nil] firstObject]; self.contentView = contentView; contentView.translatesAutoresizingMaskIntoConstraints = NO; // 這一句,是區別初始化方式後的,核心不一樣. self.translatesAutoresizingMaskIntoConstraints = ! isFromXib; [self addSubview: contentView]; self.backgroundColor = contentView.backgroundColor; if (nil == self.backgroundColor) { self.backgroundColor = [UIColor clearColor]; } [self addConstraint: [NSLayoutConstraint constraintWithItem: contentView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem: self attribute:NSLayoutAttributeLeft multiplier: 1.0 constant: 0]]; [self addConstraint: [NSLayoutConstraint constraintWithItem: contentView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem: self attribute:NSLayoutAttributeRight multiplier: 1.0 constant: 0]]; [self addConstraint: [NSLayoutConstraint constraintWithItem: contentView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem: self attribute:NSLayoutAttributeTop multiplier: 1.0 constant: 0]]; [self addConstraint: [NSLayoutConstraint constraintWithItem: contentView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem: self attribute:NSLayoutAttributeBottom multiplier: 1.0 constant: 0]]; [self setup]; return self; } - (void)setup { /* 子類須要繼承此方法,以完成自定義初始化操做. */ } - (void)reloadData { /* 子類根據須要,自行實現. */ } - (UIViewController*)viewController { for (UIView* next = [self superview]; next; next = next.superview) { UIResponder* nextResponder = [next nextResponder]; if ([nextResponder isKindOfClass:[UIViewController class]]) { return (UIViewController*)nextResponder; } } return nil; } - (void)back { if (nil != self.viewController.navigationController) { [self.viewController.navigationController popViewControllerAnimated: YES]; } else{ [self.viewController dismissViewControllerAnimated: YES completion:NULL]; } } - (NSLayoutConstraint *)heightContronstraint { __block NSLayoutConstraint * heightCons = nil; [self.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint * obj, NSUInteger idx, BOOL *stop) { if (NSLayoutAttributeHeight == obj.firstAttribute && nil == obj.secondItem && [obj.firstItem isEqual: self]) { heightCons = obj; * stop = YES; } }]; return heightCons; } + (instancetype)sharedInstance { /* 子類應根據須要重寫這個方法. */ return nil; } - (id)virtualModel { return nil; } - (void)setModel:(id)model { _model = model; // 更新視圖. [self updateView]; } - (id)model { id model = _model; if(YES == self.isTest){ model = self.virtualModel; } return model; } - (void)updateView { /*子類應根據須要重寫此方法.默認不作任何處理.*/ } - (BOOL)isTest { /* 子類應根據本身須要,重寫這個方法. */ return NO; } @end
你的Xib視圖組件,應該由一個 MCComponent的子類的.h/.m與一個同名的 .xib 文件組成,如MCTextComponent.h, MCTextComponent.m, MCTextComponent.xib.此時應把XIB的File's Owder與自定義的MCComponent關聯起來.按照以上步驟,便可實現圖示效果.工具
此策略已經在咱們的項目中試用了一段時間,也已經填了些坑,屢次優化,感興趣的能夠直接拿過去用.可是,基於XIB的視圖模塊化,終究仍是須要手動的參與,對工做效率的提高也彷佛達到了一個極限:由於它終究須要人工深度參與.關於它的討論,暫時到此爲止.佈局
Masonry,是一個基於純代碼的AutoLayout庫.初次涉及時,只是感受它很方便,既有Xib的易讀性,又有純代碼的靈活性.試用一段時間以後,忽然想到: 或許藉助Masonry,創建一個純代碼的不依賴Xib的AutoLayout視圖組件機制.學習
視圖基於 AutoLayout;測試
視圖自動適配不一樣屏幕尺寸;優化
視圖徹底獨立於數據與業務邏輯;
視圖嚴肅僅與父視圖有位置關係;
能夠將視圖模塊的元素與模塊同名屬性自動關聯;
僅需知道父視圖的寬高,模塊內某一個UI元素的寬高, UI元素的 bottom 與 right, 就能夠惟一肯定任意元素的位置.
既定方案,必須基於AutoLayout,至於AutoLayout與Frame的區別於優點,不作贅述.
在不考慮多屏幕兼容的狀況下, AutoLayout,能夠直接使用固定的約束常量值來肯定,可是 立刻iPhone 7 都要出來了,指不定什麼尺寸呢? 一個機型,一個UI代碼?是否是想一想都讓人頭大!
考慮到多屏幕尺寸,UI設計圖等比縮放的經常使用狀況,我分享一個能夠惟一肯定UI元素的方案:
[subView makeConstraints:^(MASConstraintMaker *make) { UIView * superView = subView.superview; make.width.equalTo(superView).multipliedBy(subWidth / superWidth); make.height.equalTo(superView).multipliedBy(subHeight / superHeight); make.right.equalTo(superView).multipliedBy(subRight / superWidth); make.bottom.equalTo(superView).multipliedBy(subBottom / superHeight); }];
以上代碼,是整個代碼的核心,其巧妙之處在於:不使用constant,而是使用比例來指定約束.選取的是 width,height,right,bottom,而不是其餘屬性,其巧妙之處,你們試用下其餘屬性就知道了.
直接繼承YFViewComponent類,而後實現類方法 subViewsConfig 便可.
// // YFViewComponent.h // iOS122 // // Created by 顏風 on 15/10/6. // Copyright (c) 2015年 iOS122. All rights reserved. // #import <UIKit/UIKit.h> /** * 預約義常量的聲明. */ extern const NSString * YFViewComponentSelfHolderWidthKey; //!< 同一設計圖中,視圖模塊自己的寬度. extern const NSString * YFViewComponentSelfHolderHeightKey; //!< 同一設計圖中,視圖模塊自己的高度. extern const NSString * YFViewComponentSubViewsKey; //!< 同一設計圖中,模塊的全部子視圖. extern const NSString * YFViewComponentSubViewClassNameKey; //!< 子視圖的類型. extern const NSString * YFViewComponentSubViewPropNameKey; //!< 子視圖對應的屬性,模塊中應有屬性與其對應,且可經過此屬性訪問對應的子視圖. extern const NSString * YFViewComponentSubViewHolderWidthKey; //!< 同一設計圖中,子視圖的寬度. extern const NSString * YFViewComponentSubViewHolderHeightKey; //!< 同一設計圖中,子視圖的高度. extern const NSString * YFViewComponentSubViewHolderRightKey; //!< 同一設計圖中,子視圖的右內邊距值(right). extern const NSString * YFViewComponentSubViewHolderBottomKey; //!< 同一設計圖中,子視圖的底部邊距值(bottom). @interface YFViewComponent : UIView /** * 子視圖配置信息. * * 子類應重寫覆蓋此方法. * 一個示例: @{ YFViewComponentSelfHolderWidthKey: @640.0, YFViewComponentSelfHolderHeightKey: @155.0, YFViewComponentSubViewsKey: @[@{ YFViewComponentSubViewClassNameKey: NSStringFromClass([UIImageView class]) , YFViewComponentSubViewPropNameKey: @"imageView", YFViewComponentSubViewHolderWidthKey: @160, YFViewComponentSubViewHolderHeightKey: @120, YFViewComponentSubViewHolderBottomKey: @140, YFViewComponentSubViewHolderRightKey: @180 }]} * * @return 返回子視圖的配置信息. */ + (NSDictionary *) subViewsConfig; @end
// // YFViewComponent.m // iOS122 // // Created by 顏風 on 15/10/6. // Copyright (c) 2015年 iOS122. All rights reserved. // #import "YFViewComponent.h" /** * 預約義常量的定義. */ const NSString * YFViewComponentSelfHolderWidthKey = @"YFViewComponentSelfHolderWidthKey"; const NSString * YFViewComponentSelfHolderHeightKey = @"YFViewComponentSelfHolderHeightKey"; const NSString * YFViewComponentSubViewsKey = @"YFViewComponentSubViewsKey"; const NSString * YFViewComponentSubViewClassNameKey = @"YFViewComponentSubViewClassNameKey"; const NSString * YFViewComponentSubViewPropNameKey = @"YFViewComponentSubViewPropNameKey"; const NSString * YFViewComponentSubViewHolderWidthKey = @"YFViewComponentSubViewHolderWidthKey"; const NSString * YFViewComponentSubViewHolderHeightKey = @"YFViewComponentSubViewHolderHeightKey"; const NSString * YFViewComponentSubViewHolderRightKey = @"YFViewComponentSubViewHolderRightKey"; const NSString * YFViewComponentSubViewHolderBottomKey = @"YFViewComponentSubViewHolderBottomKey"; @implementation YFViewComponent - (instancetype)init { self = [super init]; if (nil != self) { UIView * holderView = self; NSDictionary * config = [[self class] subViewsConfig]; CGFloat superHeight = [[config objectForKey: YFViewComponentSelfHolderHeightKey] floatValue]; CGFloat superWidth = [[config objectForKey: YFViewComponentSelfHolderWidthKey] floatValue];; NSArray * locatArray = [config objectForKey: YFViewComponentSubViewsKey]; [locatArray enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL *stop) { NSString * classString = [obj objectForKey: YFViewComponentSubViewClassNameKey]; Class viewClass = NSClassFromString(classString); if (YES != [viewClass isSubclassOfClass:[UIView class]]) { return; } UIView * subView = [[viewClass alloc] init]; [holderView addSubview: subView]; NSString * viewKey = [obj objectForKey: YFViewComponentSubViewPropNameKey]; [holderView setValue: subView forKey: viewKey]; CGFloat subWidth = [[obj objectForKey: YFViewComponentSubViewHolderWidthKey] floatValue]; CGFloat subHeight = [[obj objectForKey: YFViewComponentSubViewHolderHeightKey] floatValue]; CGFloat subBottom = [[obj objectForKey: YFViewComponentSubViewHolderBottomKey] floatValue]; CGFloat subRight = [[obj objectForKey: YFViewComponentSubViewHolderRightKey] floatValue]; [subView makeConstraints:^(MASConstraintMaker *make) { UIView * superView = subView.superview; make.width.equalTo(superView).multipliedBy(subWidth / superWidth); make.height.equalTo(superView).multipliedBy(subHeight / superHeight); make.right.equalTo(superView).multipliedBy(subRight / superWidth); make.bottom.equalTo(superView).multipliedBy(subBottom / superHeight); }]; }]; } return self; } + (NSDictionary *) subViewsConfig{ return nil; } @end
這個示例,取材自網易新聞.圖示中已經標註了單元格的寬高,單元格內各個UI元素的width,height,bottom,right.此處UI設計師可根據屏幕尺寸出圖,咱們根據一份跟定的設計圖,直接使用 MarkMan(一個很是好用的標準工具)丈量標記便可. 由於咱們是基於比例來添加約束,不一樣屏幕下,會自動等比變換.
這是一個簡單的示例,爲了方便演示,臨時加上了:
// // YFAutoTransView.h // iOS122 // // Created by 顏風 on 15/10/6. // Copyright (c) 2015年 iOS122. All rights reserved. // #import "YFViewComponent.h" @interface YFAutoTransView : YFViewComponent @property (weak, nonatomic) UIImageView * imageView; @property (weak, nonatomic) UILabel * titleLabel; @property (weak, nonatomic) UILabel * detailLabel; @property (weak, nonatomic) UIButton * chatBtn; @end
// // YFAutoTransView.m // iOS122 // // Created by 顏風 on 15/10/6. // Copyright (c) 2015年 iOS122. All rights reserved. // #import "YFAutoTransView.h" @implementation YFAutoTransView + (NSDictionary *) subViewsConfig{ NSNumber * holderWidth = @640.0; NSNumber * holderHeight = @155.0; NSArray * subConfig = @[ @[NSStringFromClass([UIImageView class]), @"imageView", @160, @120, @140, @180], @[NSStringFromClass([UILabel class]), @"titleLabel", @420, @31, @55, @615], @[NSStringFromClass([UILabel class]), @"detailLabel", @410, @60, @136, @605], @[NSStringFromClass([UIButton class]), @"chatBtn", @120, @28, @141, @628]]; NSMutableArray * subViewsConfig = [NSMutableArray arrayWithCapacity: 42]; [subConfig enumerateObjectsUsingBlock:^(NSArray * obj, NSUInteger idx, BOOL *stop) { if (6 != obj.count) { return; } NSDictionary * configDict = @{ YFViewComponentSubViewClassNameKey: obj[0], YFViewComponentSubViewPropNameKey: obj[1], YFViewComponentSubViewHolderWidthKey: obj[2],YFViewComponentSubViewHolderHeightKey: obj[3], YFViewComponentSubViewHolderBottomKey: obj[4], YFViewComponentSubViewHolderRightKey: obj[5] }; [subViewsConfig addObject: configDict]; }]; NSDictionary * config = @{ YFViewComponentSelfHolderWidthKey: holderWidth, YFViewComponentSelfHolderHeightKey: holderHeight, YFViewComponentSubViewsKey: subViewsConfig}; return config; } @end
這是運行時,咱們看到對應屬性,確實與UI元素關聯了起來.
這是與數據結合以後的效果圖.只是個初稿,還須要進一步調試.也就是說,之後再寫UI界面,你的注意力將能夠集中在 數據與視圖自己的交互處理上.
YFAutoTransView * autoTestView = [[YFAutoTransView alloc] init]; autoTestView.frame = CGRectMake(0, 100, [UIScreen mainScreen].bounds.size.width, 155.0/2); autoTestView.imageView.image = [UIImage imageNamed:@"autoTrans.png"]; autoTestView.titleLabel.text = @"愛馬仕版蘋果表開售8688元起"; autoTestView.titleLabel.font = [UIFont systemFontOfSize:15]; [autoTestView.titleLabel adjustsFontSizeToFitWidth]; autoTestView.detailLabel.text = @"愛馬仕版蘋果錶盤和錶帶並不會單獨銷售."; autoTestView.detailLabel.numberOfLines = 0; autoTestView.detailLabel.font = [UIFont systemFontOfSize:12]; [autoTestView.chatBtn setTitle:@"跟帖" forState: UIControlStateNormal]; autoTestView.chatBtn.backgroundColor = [UIColor redColor]; [self.view addSubview: autoTestView];
我在此文着重分享了我目前正在研究的 基於Masonry的視圖模塊化方案.在之後的工做和學習中,我會繼續使用與完善,以期進一步提升寫UI界面的效率.可能尚有不完備之處,歡迎你們共同提出討論.