如何把設計圖自動轉換爲iOS代碼? 在線等,挺急的!

這是一篇可能略顯枯燥的技術深度討論與實踐文章.如何把設計圖自動轉換爲對應的iOS代碼?做爲一個 iOS開發愛好者,這是我很感興趣的一個話題.最近也確實有了些許靈感,也確實取得了一點小成果,和你們分享一下.歡迎感興趣的iOS愛好者能和我一塊兒研究討論!ios

這是一個能夠節省 70% 工做量的話題

我以爲,若是真的能把一張設計圖自動轉換爲代碼,任何開發工程師都會感興趣的.單以 iOS 應用爲例, 在一個最經常使用的MVC架構的APP中,主要的代碼,無非就是集中於: M 的網絡請求部分, V的數據顯示部分, C的邏輯交互部分.對於controller控制器層,每每須要結合業務邏輯去處理,代碼量並不算大;對於Model數據模型層,咱們有 AFNetworing, RestKit, MJExtension等,能夠大大簡化網絡接口到數據模型的轉換;對於View視圖層,代碼最繁雜,最枯燥無趣,迭代最讓人頭疼的部分,又有什麼能夠憑藉呢?我沒有詳實的數據統計來確認各個iOS開發者的平常開發中,MVC各個層面,具體的時間成本如何;單從我我的角度來講, View佈局的拆分與轉換,佔據了我 70% 以上的時間.咱們公司一般是按單個完整任務來拆分工做的,單個任務的MVC三層,都是應該由一我的獨立完成.每次都把大把時間浪費在"畫UI"上,真的感受好無趣,好浪費生命;臨時遇到產品經理改動需求,可能一個對方看似更加"合理"的改動,我這邊幾乎要大動干戈!我想我對編程自己確實是感興趣的,可是成天浪費時間在 UI上,真的感受有點虛度光陰.因此說,在本不充裕的空閒裏,我一直在思考的一個命題就是: 如何實現 UI 的自動化與獨立化.編程

過往的嘗試: 基於Xib的視圖模塊化.

儘管做爲一名iOS開發人員,我依然對蘋果公司提供的開發技術及其發展方向持謹慎和保守態度.前一段時間,嘗試使用 Xib來佈局視圖,遇到一些坑,可是熟悉以後,也確實比原來單純基於絕對位置的純代碼佈局更靈活些,也更快捷些.在此期間,我研究的一個重要話題就是如何實現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 的視圖模塊化

Masonry,是一個基於純代碼的AutoLayout庫.初次涉及時,只是感受它很方便,既有Xib的易讀性,又有純代碼的靈活性.試用一段時間以後,忽然想到: 或許藉助Masonry,創建一個純代碼的不依賴Xib的AutoLayout視圖組件機制.學習

目前能獲得的效果

  • 視圖基於 AutoLayout;測試

  • 視圖自動適配不一樣屏幕尺寸;優化

  • 視圖徹底獨立於數據與業務邏輯;

  • 視圖嚴肅僅與父視圖有位置關係;

  • 能夠將視圖模塊的元素與模塊同名屬性自動關聯;

  • 僅需知道父視圖的寬高,模塊內某一個UI元素的寬高, UI元素的 bottom 與 right, 就能夠惟一肯定任意元素的位置.

核心理論基礎: AutoLayout中,如何惟一肯定元素在不一樣尺寸屏幕上的位置?

  • 既定方案,必須基於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界面的效率.可能尚有不完備之處,歡迎你們共同提出討論.

相關文章
相關標籤/搜索