实时显示iOS编写UI代码效果(转)



编写iOS应用UI的方式大概有两种,一种是Storyboard/Xib,另外一种是手写代码。采用Storyboard/Xib方式组织UI,因为提供可视化的特性,只要从UI库中拖动UI控件,即可以显示结果,极大地提升开发速度。但面临一个问题就是多人协做开发,因为全部的UI都放在同一个Storyboard文件中,使用Git/SVN合并代码就会出现冲突。多人协做开发还不是主要问题,有人提出能够建立多个Storyboard来分开UI编写,而Storyboard/Xib最主要问题是代码复用性比较差。因此有些人就选择手写UI代码,这样不只能够解决多人协做开发问题,并且经过自定义控件在多个View使用。但每次手写UI代码后都要编译、构建和运行,最后在模拟器显示,这样会拖慢开发速度。若是每次修改UI控件后,保存修改便实时在模拟器显示修改后结果,就能够极大的提升编写UI的速度。

Live Change.gif
Live Change.gif

Auto Layout

Auto Layout是什么

Auto Layout是一个基于constraint(约束)的布局系统,它根据UI元素之间约束关系来调整UI元素的位置和大小。 html

Auto Layout解决什么问题

  • 更容易适配不一样分辨率设备的屏幕(iPhone 6 Plus, iPhone 6, iPhone 5s/5, iPhone 4s/4)
  • 当设备旋转时不须要作额外处理
  • 使用constraint来描述布局逻辑,更利于理解和清晰

如何使用Auto Layout

Auto Layout中约束的类对应是NSLayoutConstraint, 而建立NSLayoutConstraint对象主要有两种方式,第一种是 ios

+ (id)constraintWithItem:(id)view1
               attribute:(NSLayoutAttribute)attribute1
               relatedBy:(NSLayoutRelation)relation
                  toItem:(id)view2
               attribute:(NSLayoutAttribute)attribute2
              multiplier:(CGFloat)multiplier
                constant:(CGFloat)constant;

上面方法主要意思是,某个view1的attribute1等于(小于或等于/大于或等于)某个view2的attribute2的multiplier倍加上constant。而attribute主要由表示位置(上/下/左/右)和大小(宽/高)的如下几个值: git

typedef enum: NSInteger {
   NSLayoutAttributeLeft = 1,
   NSLayoutAttributeRight,
   NSLayoutAttributeTop,
   NSLayoutAttributeBottom,
   NSLayoutAttributeLeading,
   NSLayoutAttributeTrailing,
   NSLayoutAttributeWidth,
   NSLayoutAttributeHeight,
   NSLayoutAttributeCenterX,
   NSLayoutAttributeCenterY,
   NSLayoutAttributeBaseline,
   NSLayoutAttributeNotAnAttribute = 0
} NSLayoutAttribute;

简化一下,使用公式能够表达为: github

view1.attribute1 = view2.attribute2 * multiplier + constant

第二种方式是: objective-c

+ (NSArray *)constraintsWithVisualFormat:(NSString *)format 
                                 options:(NSLayoutFormatOptions)opts 
                                 metrics:(NSDictionary *)metrics 
                                   views:(NSDictionary *)views;

这种方式主要是采用Visual Format Language(可视化格式语言)来描述约束布局,虽然语法比较简洁,可是可读性比较差和容易出错。 编程

Auto Layout存在问题

虽然Auto Layout在布局view方面是很是强大和灵活,可是建立constraint的语法过于繁杂,引用Masonry一个例子: 数组

UIView *superview = self;

UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
view1.backgroundColor = [UIColor greenColor];
[superview addSubview:view1];

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[superview addConstraints:@[

    //view1 constraints
    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeTop
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeTop
                                multiplier:1.0
                                  constant:padding.top],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeLeft
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeLeft
                                multiplier:1.0
                                  constant:padding.left],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeBottom
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeBottom
                                multiplier:1.0
                                  constant:-padding.bottom],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeRight
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeRight
                                multiplier:1
                                  constant:-padding.right],

 ]];

如此简单的一个例子都要编写这么多行代码,想象一下若是建立多个view的constraint时会多么痛苦啊。另外一个方式是采用Visual Format Language (VFL),虽然语法比较简洁,可是可读性比较差和容易出错。 app

Masonry

为何使用Masonry

Masonry是采用链式DSL(Domain-specific language)来封装NSLayoutConstraint,经过这种方式编写Auto Layout布局代码更加易读和简洁。
使用Masonry的MASConstraintMaker来表达相同constraint less

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

甚至能够更短 ide

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(superview).with.insets(padding);
}];

如何使用

使用Masonry建立constraint来定义布局的方式有三种:mas_makeConstraints,mas_updateConstraints,mas_remakeConstraints。

1. mas_makeConstraints

使用mas_makeConstraints建立constraint后,你可使用局部变量或属性来保存以便下次引用它;若是建立多个constraints,你能够采用数组来保存它们。

// in public/private interface
@property (nonatomic, strong) MASConstraint *topConstraint;

...

// when making constraints
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    self.topConstraint = make.top.equalTo(superview.mas_top).with.offset(padding.top);
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
}];

...
// then later you can call
[self.topConstraint uninstall];
2. mas_updateConstraints

有时你须要更新constraint(例如,动画和调试)而不是建立固定constraint,可使用mas_updateConstraints方法

// this is Apple's recommended place for adding/updating constraints
// this method can get called multiple times in response to setNeedsUpdateConstraints
// which can be called by UIKit internally or in your code if you need to trigger an update to your constraints
- (void)updateConstraints {
    [self.growingButton mas_updateConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self);
        make.width.equalTo(@(self.buttonSize.width)).priorityLow();
        make.height.equalTo(@(self.buttonSize.height)).priorityLow();
        make.width.lessThanOrEqualTo(self);
        make.height.lessThanOrEqualTo(self);
    }];

    //according to apple super should be called at end of method
    [super updateConstraints];
}
3. mas_remakeConstraints

mas_remakeConstraints与mas_updateConstraints比较类似,都是更新constraint。不过,mas_remakeConstraints是删除以前constraint,而后再添加新的constraint(适用于移动动画);而mas_updateConstraints只是更新constraint的值。

- (void)changeButtonPosition {
    [self.button mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.size.equalTo(self.buttonSize);

        if (topLeft) {
            make.top.and.left.offset(10);
        } else {
            make.bottom.and.right.offset(-10);
        }
    }];
}

想了解以上三个代码片断的更多细节,能够下载Masonry iOS Examples工程查阅。

Classy

Classy简介和特性

Classy是一个能与UIKit无缝结合stylesheet(样式)系统。它借鉴CSS的思想,但引入新的语法和命名规则。

灵活内嵌的语法

{ } : ; 这些语法符号是可选的,你能够选择适合本身的风格来表达stylesheet。

你可使用{ } : ; 来限定stylesheet

$main-color = #e1e1e1;

MYCustomView {
  background-color: $main-color;
  title-insets: 5, 10, 5, 10;
  > UIProgressView.tinted {
    progress-tint-color: black;
    track-tint-color: yellow;
  }
}

^UIButton.warning, UIView.warning ^UIButton {
  title-color[state:highlighted]: #e3e3e3;
}

或者你使用空格来限定stylesheet

$main-color = #e1e1e1

MYCustomView 
  background-color $main-color
  title-insets 5, 10, 5, 10
  > UIProgressView.tinted 
    progress-tint-color black
    track-tint-color yellow

^UIButton.warning, UIView.warning ^UIButton 
  title-color[state:highlighted] #e3e3e3
默认样式

Classy在应用程序Bundle默认查找文件名为stylesheet.cas的样式文件。若是你采用这个文件名,你能够不用作任何东西就能加载样式文件。
但若是你想指定其余file path(样式文件名),你能够建立[CASStyler defaultStyler]

[CASStyler defaultStyler].filePath = [[NSBundle mainBundle] pathForResource:@"myStyles.cas" ofType:nil];

若是你还想当发生错误时,获取错误信息以便于调试,可使用-(void)setFilePath:error:

NSError *error = nil;
NSString filePath = [[NSBundle mainBundle] pathForResource:@"myStyles.cas" ofType:nil];
[[CASStyler defaultStyler] setFilePath:filePath error:&error];

若是你是使用Storyboard/Xib组织UI界面,那就须要在main.m的int main(int argc, char * argv[])方法设置 filePath,这样能够确保在建立UIWindow以前加载stylesheet。不然(采用手写UI代码),你在 AppDelegate.m的- (BOOL)application:didFinishLaunchingWithOptions:方法设置filePath

Live Reload

Live Reload是实时显示编写UI代码效果的关键特性,它可以实时检查stylesheet文件变化,无需从新编译、构建和运行模拟器,从而极大提升开发速度。
为了启用Live Reload,你须要指定stylesheet路径,而且只运行在模拟器上。

#if TARGET_IPHONE_SIMULATOR
    NSString *absoluteFilePath = CASAbsoluteFilePath(@"../Styles/stylesheet.cas");
    [CASStyler defaultStyler].watchFilePath = absoluteFilePath;
#endif

Selectors

Style Selectors是指定哪一个view使用哪一种样式的方式。主要有三种方法来指定目标view:

  1. Object Class
  2. View Hierarchy
  3. Style Class

你能够混合使用三种方法,例子以下:

/* match views
 * where class is UIButton or UIButton subclass
 * and styleClass is "large"
 * and superview class is UITabBar
 */

UITabBar > ^UIButton.large { }

想了解具体如何使用,请查阅官网Selectors章节

为了不与Objective-C的message selectors混淆,术语style selectors表示Classy stylesheets的selectors

Properties

Classy支持全部UIAppearance的属性和方法,也支持与UIAppearance无关的不少属性。Classy使用与UIKit相同属性命名,因此你没必要考虑如何将style property映射到Objective-C的property
UIPageControl类的属性以下:

@property (nonatomic,retain) UIColor *pageIndicatorTintColor;
@property (nonatomic,retain) UIColor *currentPageIndicatorTintColor;

style property的名字采用与objective-c同样的名字

UIPageControl {
  pageIndicatorTintColor black
  currentPageIndicatorTintColor purple
}

style property的命名规则采用kebab case

UIPageControl {
  page-indicator-tint-color black
  current-page-indicator-tint-color purple
}

想了解具体如何使用,请查阅官网Properties章节

Keep it DRY(Don't Repeat Yourself)

在编程中一个很重要的原则就是避免重复,这不只能够大量减小重复代码,而且使得代码更加容易复用和维护。Classy提供三种方式避免代码重复:grouping,nestingvariables

Grouping

若是有两个以上的style selectors共用相同的属性时

UISlider.info {
  minimum-track-tint-color black
  maximum-track-tint-color purple
}

UISlider.error {
  minimum-track-tint-color black
  maximum-track-tint-color purple
  thumb-tint-color red
}

咱们能够提取相同的属性到分组style selector中

UISlider.info, UISlider.error {
  minimum-track-tint-color black
  maximum-track-tint-color purple
}

UISlider.error {
  thumb-tint-color red
}
Nesting

若是两个以上style selectors共用相同的view hierarchy时

UICollectionView {
  background-color #a2a2a2
}

UICollectionView > UICollectionViewCell {
  clips-to-bounds NO
}

UICollectionView > UICollectionViewCell UILabel {
  text-color purple
}

UICollectionView > UICollectionViewCell UILabel.title {
  font 20
}

咱们经过nesting方式将view hierarchies表达成这样方式

UICollectionView {
  background-color #a2a2a2

  > UICollectionViewCell {
    clips-to-bounds NO

    UILabel {
      text-color purple

      &.title {
        font 20
      }
    }
  }
}
Variables

Classy让你经过定义variables来将多个相同的style property值存储以便共享。Variable命名规则以下:

  • 必须以大小写字母或$符号开头
  • 能够包含_,-或任何字母数字

    // prefix with ' $ ' to help distinguish variables
    $brand-color = #e1e1e1
    
    // OR not
    insets = 5, 10, 5, 10
    
    UIButton {
      background-color $brand-color
      contentEdgeInsets insets
      background-image[state:selected] bg_button insets
    }

    最后官方还提供一个实例来解释具体如何使用:Custom Views Example

ClassyLiveLayout

ClassyLiveLayout经过结合Classy stylesheets与Masonry一块儿使用,可以在运行的模拟器中微调Auto Layout约束实时显示效果的工具。

ClassyLiveLayout一个核心category:UIView+ClassyLayoutProperties,在UIView定义如下属性:

@property(nonatomic, assign) UIEdgeInsets cas_margin;
@property(nonatomic, assign) CGSize cas_size;

// shorthand properties for setting only a single constant value
@property(nonatomic, assign) CGFloat cas_sizeWidth;
@property(nonatomic, assign) CGFloat cas_sizeHeight;

@property(nonatomic, assign) CGFloat cas_marginTop;
@property(nonatomic, assign) CGFloat cas_marginLeft;
@property(nonatomic, assign) CGFloat cas_marginBottom;
@property(nonatomic, assign) CGFloat cas_marginRight;

cas_margin和cas_size分别表示UI元素的位置和大小,而其他的属性都是对两个属性进一步细分。咱们能够从stylesheets中访问style properties来定义constraints布局,作到将数据与代码分离,有利于修改和复用代码。

UIView.blue-box {
    cas_size: 80 100
    cas_margin-top: 60
    cas_margin-left: 50
}

UIView.red-box {
    cas_size-width: 120
    cas_margin-left: 20
}

咱们能够在updateConstraints或updateViewConstrains定义布局时引用style properties

- (void)updateViewConstraints {
  [super updateViewConstraints];

  [_blueBoxView mas_updateConstraints:^(MASConstraintMaker *make) {
      make.width.equalTo(@(_blueBoxView.cas_size.width));
      make.height.equalTo(@(_blueBoxView.cas_size.height));
      make.top.equalTo(@(_blueBoxView.cas_margin.top));
      make.left.equalTo(@(_blueBoxView.cas_margin.left));
  }];

  [_redBoxView mas_updateConstraints:^(MASConstraintMaker *make) {
      make.width.equalTo(@(_redBoxView.cas_size.width));
      make.height.equalTo(_blueBoxView);
      make.top.equalTo(_blueBoxView);
      make.left.equalTo(_blueBoxView.mas_right).with.offset(_redBoxView.cas_margin.left);
  }];
}

当定义view layouts时,将Auto Layout的constraints都放在stylesheets中实时加载(Live reload)。若是你修改constraints,无需从新编译、构建和运行模拟器便能实时看到修改后的效果。

示例工程

配置工程

因为须要引用Masonry,Classy和ClassyLiveLayout,Podfile配置以下:

pod 'Masonry', '~> 0.6.1'
pod 'Classy', '~> 0.2.4'
pod 'ClassyLiveLayout', '~> 0.6.0'

编写代码

1. 添加stylesheet.cas文件到工程

当安装好Masonry,Classy和ClassyLiveLayout后,第一次运行项目会出现没有stylesheet.cas文件错误:

No stylesheet.cas file error.png
No stylesheet.cas file error.png

只要向工程添加空的stylesheet.cas文件便可。

Create stylesheet.cas file.png
Create stylesheet.cas file.png
2. 建立LiveView类,该类继承SHPAbstractView。
Create LiveView inherit SHPAbstractView.png
Create LiveView inherit SHPAbstractView.png

在ViewController建立LiveView对象,而后被self.view引用。

Setup root view in ViewController.png
Setup root view in ViewController.png

当编译运行时,在SHPAbstractView.h因为找不到UIView出现编译错误。

SHPAbstractView Compile error.png
SHPAbstractView Compile error.png

只需引入UIKit即可以解决,但运行一下应用程序,出现一下错误:

Must override methods.png
Must override methods.png

主要缘由是任何自定义UIView继承SHPAbstractView都须要override两个方法:- (void)addSubviews和- (void)defineLayout,咱们能够查看SHPAbstractView的源码可知:

SHPAbstractView Source Code .png
SHPAbstractView Source Code .png

因此只要在LiveView.m文件覆盖两个方法便可

#pragma mark - Add subviews and define layout
- (void)addSubviews
{
}

- (void)defineLayout
{
}
3. LiveView类设计

LiveView主要由包含redBoxView和blueBoxView两个属性,redBoxView表示红色方块,blueBoxView表示蓝色方块。

#import "SHPAbstractView.h"

@interface LiveView : SHPAbstractView

@property (strong, nonatomic) UIView *redBoxView;
@property (strong, nonatomic) UIView *blueBoxView;

@end
4. LiveView类实现

因为SHPAbstractView类如何初始化View已经作了处理,暴露两个接口- (void)addSubviews和-(void)defineLayout分别处理构建view hierarchy和定义布局,子类只要覆盖SHPAbstractView这两个方法就能够建立LiveView了。
可是咱们将Auto Layout的constraints都放在stylesheets中实时加载(Live reload),即放在本工程的stylesheet.cas文件,将布局数据和布局代码分离。

UIView.redBox {
    cas_marginTop 50
    cas_marginLeft 20

    cas_size 100 100
}

UIView.blueBox {
    cas_marginTop 50
    cas_marginRight -20

    cas_size 100 100
}

有了constraints数据后,即可以在代码布局:

@implementation LiveView

#pragma mark - Add subviews and define layout
- (void)addSubviews
{
    self.backgroundColor = [UIColor whiteColor];
    [self addSubview:self.redBoxView];
    [self addSubview:self.blueBoxView];
}

- (void)defineLayout
{
    [self.redBoxView mas_updateConstraints:^(MASConstraintMaker* make){
        make.top.equalTo(@(self.redBoxView.cas_marginTop));
        make.left.equalTo(@(self.redBoxView.cas_marginLeft));
        make.width.equalTo(@(self.redBoxView.cas_sizeWidth));
        make.height.equalTo(@(self.redBoxView.cas_sizeHeight));
    }];

    [self.blueBoxView mas_updateConstraints:^(MASConstraintMaker *make){
        make.top.equalTo(@(self.blueBoxView.cas_marginTop));
        make.right.equalTo(@(self.blueBoxView.cas_marginRight));
        make.width.equalTo(@(self.blueBoxView.cas_sizeWidth));
        make.height.equalTo(@(self.blueBoxView.cas_sizeHeight));
    }];
}

#pragma mark - Lazy initialization
- (UIView*)redBoxView
{
    if (!_redBoxView) {
        _redBoxView = [UIView new];
        _redBoxView.cas_styleClass = @"redBox";
        _redBoxView.backgroundColor = [UIColor redColor];
    }

    return _redBoxView;
}

- (UIView*)blueBoxView
{
    if (!_blueBoxView) {
        _blueBoxView = [UIView new];
        _blueBoxView.cas_styleClass = @"blueBox";
        _blueBoxView.backgroundColor = [UIColor blueColor];
    }

    return _blueBoxView;
}
5. 模拟器支持Live Reload

为了启用Live Reload,你须要指定stylesheet路径,而且只运行在模拟器上。

Support Live Reload.png
Support Live Reload.png

最后效果

Live Change.gif
Live Change.gif

示例代码存放地址:LiveAutoLayout

总结

以前手写UI代码每次更改通常都要从新编译、构建和运行模拟器才能看到效果,但结合使用Masonry,Classy和ClassLiveLayout以后,告别这个费时过程,极大地提升开发速度;不只如此,咱们将Auto Layout的constraints都放在stylesheets中实时加载(Live reload),将布局数据和布局代码分离,使得代码更加复用和维护。Classy还提供三种避免重复方法:Grouping, Nestting和Variable,尽量复用样式数据。 这是本人第一次编写技术博客,可能有不少错误和漏洞,但愿你们多多指点,也但愿这篇文章可以帮助到你们。

相关文章
相关标签/搜索