这是一篇可能略显枯燥的技术深度讨论与实践文章.如何把设计图自动转换为对应的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界面的效率.可能尚有不完备之处,欢迎你们共同提出讨论.