實時顯示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,儘量複用樣式數據。 這是本人第一次編寫技術博客,可能有不少錯誤和漏洞,但願你們多多指點,也但願這篇文章可以幫助到你們。

相關文章
相關標籤/搜索