『iOS开发』基于链式语法快速生成 UI

218 阅读3分钟

博客首发地址 www.thatisawesome.club

背景

在日常的业务迭代开发工作中,UI 开发占据了我们很大一部分时间,这部分工作的流程大概是:

  • new 各种 UI 控件
  • 各种 UI 控件属性赋值
  • addSubview:
  • 添加布局约束

举个例子,我们在 Controller 的 View 上加一个 Button,一般我们的写法如下:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 一般写法    
    UIButton *btn = [[UIButton alloc] init];
    btn.center = CGPointMake(self.view.center.x, self.view.center.y + 50);
    [self.view addSubview:btn];
    [btn setTitle:@"Click Me!" forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    btn.backgroundColor = [UIColor whiteColor];
    btn.layer.cornerRadius = 20;
    btn.layer.shadowOffset = CGSizeMake(0, 1);
    btn.layer.shadowColor = [UIColor blackColor].CGColor;
    btn.layer.shadowOpacity = 0.1;
    btn.layer.shadowRadius = 1;
    [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)btnClick:(UIButton *)btn {
    // Button Click Action
}

代码显然不够优雅, 一堆属性赋值看着让人窒息。这个时候我们通常的做法是为 UI 控件的生成写一个工厂方法,这样在生成控件的时候直接调用工厂方法来快速生成,能够在一定程度上减少一些重复性代码。代码改进之后可能长这样

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 1. 改进之后的写法
    UIButton *btn = [UIButton buttonInView:self.view
                                     title:@"Click Me!"
                                    target:self
                                    action:@selector(btnClick:)];
    btn.center = CGPointMake(self.view.center.x, self.view.center.y + 50);
    [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    btn.backgroundColor = [UIColor whiteColor];
    btn.layer.cornerRadius = 20;
    btn.layer.shadowOffset = CGSizeMake(0, 1);
    btn.layer.shadowColor = [UIColor blackColor].CGColor;
    btn.layer.shadowOpacity = 0.1;
    btn.layer.shadowRadius = 1;
}

- (void)btnClick:(UIButton *)btn {
    // Button Click Action
}

但是工厂方法不够灵活,参数相对固定,如果有新的属性想加入到工厂方法中去,就要重新再声明一个工厂方法,如此,在写代码的时候你将会在一堆代码提示中挑花眼。有没有更优雅的方式呢?

链式语法

说到链式语法,我猜每一个 iOSer 最先想到的应该是 Masonry 这个开源库了吧,Masonry 是一个自动布局框架,对苹果原生的 NSAutoLayout 进行了封装,使得我们能够以一种更优雅的方式来写布局约束,借鉴 Masonry 这种链式语法的方式,自己封装了一个 UI 生成框架 CHUIPropertyMaker。 先看看使用 CHUIPropertyMaker 如何一个 Button:

#import "ViewController.h"
@import CHUIPropertyMaker;

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 使用 CHUIPropertyMaker 声明
    UIButton *newBtn = [[UIButton alloc] init];
    [newBtn ch_makeButtonProperties:^(CHButtonPropertyMaker *make) {
        make.title(@"Click Me").forState(UIControlStateNormal);
        make.titleColor(UIColor.blackColor).forState(UIControlStateNormal);
        make.cornerRadius(20);
        make.shadow(UIColor.blackColor, 0.1, CGSizeMake(0, 1), 1);
        make.superView(self.view);
        make.action(@selector(btnClick:)).withTarget(self).forEvent(UIControlEventTouchUpInside);
    } constrains:^(MASConstraintMaker *make) {
        make.width.equalTo(@100);
        make.height.equalTo(@40);
        make.top.equalTo(self.timeLabel.mas_bottom).offset(12);
        make.centerX.equalTo(self.view);
    }];
}

- (void)btnClick:(UIButton *)btn {
    // Button Click Action
}

有没有稍微优雅那么一丢丢呢?

实现原理

对于链式语法的实现,其实就是使用到了 block。 生成控件时,最先调用

- (void)ch_makeProperties:(void (^)(CHViewPropertyMaker *))properties
            constrains:(void (^)(MASConstraintMaker *))constrains;

方法,这个方法声明在 UIView 的分类 UIView+PropertyMaker.h 中,通过调用 propertys 来为 UI 控件做属性赋值,调用 constrains 来为控件做布局约束。当回调 propertys 时,传了一个 CHViewPropertyMaker 的实例对象。

@interface CHViewPropertyMaker : NSObject

@property (nonatomic, weak, readonly) UIView *view;

- (instancetype)initWithView:(UIView *)view;

/// 设置父控件
@property (nonatomic, copy, readonly) CHViewPropertyMaker *(^superView)(UIView *superView);

/// 是否响应交互
@property (nonatomic, copy, readonly) CHViewPropertyMaker *(^userInteractionEnabled)(BOOL enable);

/// 添加阴影
@property (nonatomic, copy, readonly) CHViewPropertyMaker *(^shadow)(UIColor *shadowColor, CGFloat shadowOpacity, CGSize shadowOffset, CGFloat shadowRadius);

/// 设置背景颜色
@property (nonatomic, copy, readonly) CHViewPropertyMaker *(^backgroundColor)(UIColor *backgroundColor);

/// 设置圆角
@property (nonatomic, copy, readonly) CHViewPropertyMaker *(^cornerRadius)(CGFloat cornerRadius);

@property (nonatomic, copy, readonly) CHViewPropertyMaker *(^clipToBounds)(BOOL clipToBounds);

/// 添加点击手势事件
@property (nonatomic, copy, readonly) CHViewPropertyMaker *(^tapGestureAction)(id target, SEL selector);

/// 添加长按手势事件
@property (nonatomic, copy, readonly) CHViewPropertyMaker *(^longPressGestureAction)(id target, SEL selector);

/// 设置Frame
@property (nonatomic, copy, readonly) CHViewPropertyMaker *(^frame)(CGFloat x, CGFloat y, CGFloat width, CGFloat height);

@end

CHViewPropertyMaker 的声明如上,里面有一堆 block 属性,和一个初始化方法,- (instancetype)initWithView:(UIView *)view 这个初始化方法用来绑定控件,并且持有该控件的弱引用,至于为什么是弱引用,自然是避免循环引用了。 当我们通过点语法去调用每个属性的 get 方法时,实际内部就是在对控件的对应属性赋值。

- (CHViewPropertyMaker * _Nonnull (^)(BOOL))userInteractionEnabled {
    return ^CHViewPropertyMaker *(BOOL enable) {
        self.view.userInteractionEnabled = enable;
        return self;
    };
}

结语

以上对 UIPropertyMaker 的简单使用做了说明,并且简单阐述了下实现原理,源代码可以访问 CHUIPropertyMaker 如果想集成使用可以通过 Cocoapods 来集成使用

pod 'CHUIPropertyMaker'