背景
说起链式编程,在iOS领域内,最有名的莫过于Masonry框架了,它是一个轻量级的布局框架。其拥有自己的描述语法,同时采用更优雅的链式语法封装自动布局,使得代码简洁明了并具有高可读性。应该算是在iOS开发上,链式编程的典范了。
就因为Masonry成功的将链式语法引入到了iOS开发中,才使我这种一脸懵逼的吃瓜群众开始喜欢上链式编程的风格,更因为其出色的可读性而心持向往。因此,也开始自己尝试在某些场景下运用链式编程。
通常来讲,我们需要一些固定成场景(需求)来使用链式编程,就如同Masonry来解决自动布局一样(当然,同时屏蔽了大量繁琐的Autolayout约束的语法),我自己也需要设计一个场景。
为UIView添加链式属性
其实,我的场景很简单,就是对UI层进行大量属性设置时,希望使用链式编程。大概设计成这个样子:
UIView* theView = [UIView new];
theView.left(5).top(150).size(100,200).backgroundColor([UIColor redColor]).addToView(superView);
就这么简单,只要完成这个东西,我就心满意足了,为啥呢?因为目前其实对链式编程所知甚少,仍旧是一脸懵逼,所以不要贪多嚼不烂嘛(说白了就是能力有限)~
根据上面的语法设计,我们大概能知道这么几点:
- 支持()这种形式的语法入参
- 要支持"."(点)语法的连续调用
- 请注意上面的addToView(superView),也就是对于自己的操作始终保持在自己的对象上。想想之前我们是如何加一个UIView上去的?[superView addSubview:theView];这时,你发现,如果还按照这个思路来,链式不就打断了嘛,所以还是要进行一些必要的函数改造
基于以上三点,我们来看看应该来如何实现。首先括号"( )"和点" . "语法在OC里就比较受限制,换句话说,如果打算这么做,那基本上实现形式也就定了。在OC里,属性的getter方法是可以使用点" . "语法来调用的,而要想使用括号传参,第一反应就是使用block。
基于前面说的,我们就得出了以下结论:
- 为需要链式的方法设置类似于getter的方法
- 返回值必须是个block,这个block可以接收参数,然后执行
- 执行后,block的返回值必须是自己(即 self),否则就链接不到一起了
除了以上三点外,我肯定是希望所有的UIView都能有这样的能力,所以我打算给UIView写个Category,我们以backgroundColor为例。这个属性本来就是UIView的基本属性,而我们想让它支持链式的语法调用,就需要这么写
@interface UIView (Chained)
(UIView* (^)(UIColor* newcolor))ch_backgroundColor;
@end
我们首先定一个ch_backgroundColor的方法,由于系统本身就已经用了backgroundColor了,同时,也为了体现该方法与backgroundColor的getter方法的区别,因此我们增加了方法前缀ch_,即chain的简写(暂且说是简写吧,不是也凑合用了 0 )
这个方法说起来,作用就是当你调用它时,他会返回一个block,这个block的返回值是一个UIView的指针,同时这个block会接收一个( UIColor* )的入参,并执行。这就算完事了。
接着,我们在实现文件里把它的实现完成,具体如下:
-(UIView*(^)(UIColor*))ch_backgroundColor
{
WEAKENING_SELF;
return ^(UIColor* newcolor){
USING_SELF;
self.backgroundColor = newcolor;
return self;
};
}
代码中的WEAKENING_SELF和USING_SELF,两个宏实际上就是__weak和__strong的封装,具体的可以查看ReactCocoa里,@weakify和strongly的代码,意思差不多,除了没有装逼般的添加对@符的支持(如果不知道的同学可以去看看实现,记得第一次看的时候,恍然大明白~颇有技巧,颇感装逼呀~~)。
另外,我们还需要对addSubview做一些修改,具体如下:
-(UIView*(^)(UIView* parentView))ch_addToView;
-(UIView*(^)(UIView*))ch_addToView
{
WEAKENING_SELF;
return ^(UIView* parentView){
USING_SELF;
if(self.superview == nil)
{
[parentView addSubview:self];
}
else if(self.superview && parentView != self.superview)
{
[self removeFromSuperview];
[parentView addSubview:self];
}
return self;
};
}
之后,我们可以用代码验证一下:
UIView* theView = [UIView new];
theView.ch_backgroundColor([UIColor redColor])
.ch_addToView(self.view); //self.view是当前UIViewController的view
在验证完没有问题后,我开始针对UIView增加其他属性,我会在这片文字的最后面全部贴出来,有需要的拿走不谢!!!
为UIView的子类添加链式属性
在编写了一大堆UIView的属性后,我开始不满足于这些基本的UIView属性了。之后,我们还希望让所有的子类都支持。
由于之前写的是Category,因此,只要引入,所有UIView的子类就都具有之前定义的所有的链式属性了。因此,我们只需要针对UILabel自有的属性进行链式化(自己发明的词,其实就是添加链式语法的属性设置,实在不想写这么多字~~)就可以了。
当然,此时作为吃瓜群众的我,还不知道困难来临了。
我们招葫芦画瓢,对UILabel的text属性进行链式化(哎呦,这词儿还挺好使 0 )
@interface UILabel (Chained)
-(UILabel*(^)(NSString* text))ch_text;
@end
-(UILabel*(^)(NSString*))ch_text
{
WEAKENING_SELF;
return ^(NSString* text){
USING_SELF;
self.text = text;
return self;
};
}
根之前一样,就这么搞起,木有悬念~
好了,让我们试试吧
UILabel* theLabel = [[UILabel alloc] init];
theLabel.ch_left(10)
.ch_top(50)
.ch_size(CGSizeMake(80,30))
.ch_te
哎呦我靠~ 当设置完UIView的属性后,我本想去设置一下文本,即调用ch_text(@""),但是,我发现我找不到这个属性!我立即意识到,完了,是类型问题!由于我们在UIView的Category中返回的都是UIView的对象,所以在调用ch_left,ch_top,ch_size时,其实返回的都是UIView对象,即使我们知道这个对象其实就是theLabel本身....
如果我们把顺序调整一下,先调用ch_text,再调用ch_left,ch_size等等,就没有问题了。这也验证了就是返回类型导致的问题,毕竟,子类调父类方法当然没问题,而当父类型要调子类型方法时当然就不行。
好吧,我们来着手解决这一问题,虽然不知道能不能解决......
首先可以排除id 和 instancetype这两个了,原因很简单,返回id更是什么都“点”不出来。而instancetype的问题也一样,它最后认的还是UIView,因为之前的方法都是写在了UIView的Category上了嘛。
之后,我尝试用模版类去解决,由于我们无法将UIView改写成模版类(Category内也不允许将UIView改写为模版类,即@interface UIView
(Chained)这么写是不被允许的)所以我试图使用模版对象,再从模版对象的方法中返回对应类型的对象。
模版类方案(其实这个方案并没有成功,我只是在这里写出来表明我挣扎过......)
具体方法如下:
@interface TAnyType : NSObject
typedef T MyType;
-(__kindof T(^)(T tobj))transfom;
@end
首先定义了模版T,之后,将T定义为MyType。
之后,定义了一个方法,它也是返回了一个block,其返回值和入参都是T,其实这个方法的设想很简单,就是将UILabel传进去,再把它的对象反出来。具体实现如下:
@implementation TAnyType
-(__kindof MyType(^)(MyType))transfom
{
return ^(MyType tobj){
return tobj;
};
}
@end
随后,我又在之前UIView的Category上增加了一个方法,具体如下:
-(TAnyType*(^)(TAnyType* obj))ch_change;
-(TAnyType*(^)(TAnyType*))ch_change
{
return ^(TAnyType* obj){
return obj;
};
}
实际上,我的想法很简单,就是想通过模版类,返回真正的指针,设计的语法是下面这个样子:
UILabel* theLabel = [UILabel new];
theLabel.ch_size(CGSizeMake(50,50))
.ch_change([TAnyType new]).transform(self)
ch_size()返回的是UIView的对象,然后通过调用ch_change()来生成并返回一个模版对象,模版指定为UILabel,然后,通过transform()方法把当前的self以UILabel对象的类型再返回出来。
一切设想的是那么美好,但现实是操蛋的.....transform(self)返回的是id类型,尽管这个id对象就是UILabel的对象,但是你仍旧“点”不出来UILabel的方法,宣告失败!
最后的粗糙方案
到最后,我索性把事情搞的简单点,不就是要个类型转换吗,行~我直接给你。于是我做了这么一件事
@interface UIView (Chained)
...
DefChainProp(UILabel);
@end
@implementation UIView (Chained)
...
ImpChainProp(UILabel);
@end
两个宏分别如下:
#define DefChainProp(Type) -(Type*)Type
#define ImpChainProp(Type) -(Type*)Type{ return (Type*)self;}
哈~ 是的,我将你需要的类型转换,以类型同名的方式添加了getter方法,而返回值就是强转后的对象本身,最后,我们的调用成了这个样子:
UILabel* theLabel = [[UILabel alloc] init];
theLabel.ch_left(10)
.ch_top(50)
.ch_size(CGSizeMake(80,30))
.UILabel.ch_text(@"12345");
最终我达到了效果,至少是表面上的效果,保证了整个链式的连贯性。同时,整个继承上也不是问题,即使是UILabel的子类也同样可以调用,当然,如果你仍旧需要调用UILabel的字类,只需要在之前的UIView的Category或者UILabel的Category定义信的同名getter方法即可,虽然不是什么特别好的办法,但至少可以顺利的用下去。阿门~~
附上之前的类,有需要的请拿走~
// .h file
#define DefChainProp(Type) -(Type*)Type
#define ImpChainProp(Type) -(Type*)Type{ return (Type*)self;}
@interface UIView (Chained)
@property CGPoint origin;
@property CGSize size;
@property (readonly) CGPoint bottomLeft;
@property (readonly) CGPoint bottomRight;
@property (readonly) CGPoint topRight;
@property CGFloat height;
@property CGFloat width;
@property CGFloat top;
@property CGFloat left;
@property CGFloat bottom;
@property CGFloat right;
-(UIView*(^)(CGSize newsize))ch_size;
-(UIView*(^)(CGPoint neworigin))ch_origin;
-(UIView*(^)(CGFloat newheight))ch_height;
-(UIView*(^)(CGFloat newwidth))ch_width;
-(UIView*(^)(CGFloat newtop))ch_top;
-(UIView*(^)(CGFloat newleft))ch_left;
-(UIView*(^)(CGFloat newbottom))ch_bottom;
-(UIView*(^)(CGFloat newright))ch_right;
-(UIView*(^)(UIColor* newcolor))ch_backgroundColor;
-(UIView*(^)(BOOL newinteractive))ch_userInteractionEnabled;
-(UIView*(^)(CGFloat newradius))ch_cornerRadius;
-(UIView*(^)(BOOL newclipbounds))ch_clipsToBounds;
-(UIView*(^)(UIView* parentView))ch_addToView;
-(UIView*(^)(NSInteger newtag))ch_tag;
-(UIView*(^)(CGFloat newalpha))ch_alpha;
-(UIView*(^)(BOOL newhidden))ch_hidden;
DefChainProp(UILabel); //此处只是举个栗子
@end
下面是实现文件
//.m file
#define WEAKENING(param) __weak typeof(param) __weak##param__ = param
#define STRONGING(param) __strong typeof(__weak##param__) param = __weak##param__
#define WEAKENING_SELF WEAKENING(self)
#define USING_SELF STRONGING(self)
@implementation UIView (Chained)
- (CGPoint) origin{
return self.frame.origin;
}
- (void) setOrigin: (CGPoint) aPoint{
CGRect newframe = self.frame;
newframe.origin = aPoint;
self.frame = newframe;
}
-(UIView*(^)(CGPoint))ch_origin{
WEAKENING_SELF;
return ^(CGPoint neworigin){
USING_SELF;
self.origin = neworigin;
return self;
};
}
- (CGSize) size{
return self.frame.size;
}
- (void) setSize: (CGSize) aSize{
CGRect newframe = self.frame;
newframe.size = aSize;
self.frame = newframe;
}
-(UIView*(^)(CGSize))ch_size{
WEAKENING_SELF;
return ^(CGSize newsize){
USING_SELF;
self.size = newsize;
return self;
};
}
// Query other frame locations
- (CGPoint) bottomRight{
CGFloat x = self.frame.origin.x + self.frame.size.width;
CGFloat y = self.frame.origin.y + self.frame.size.height;
return CGPointMake(x, y);
}
- (CGPoint) bottomLeft{
CGFloat x = self.frame.origin.x;
CGFloat y = self.frame.origin.y + self.frame.size.height;
return CGPointMake(x, y);
}
- (CGPoint) topRight{
CGFloat x = self.frame.origin.x + self.frame.size.width;
CGFloat y = self.frame.origin.y;
return CGPointMake(x, y);
}
- (CGFloat) height{
return self.frame.size.height;
}
- (void) setHeight: (CGFloat) newheight{
CGRect newframe = self.frame;
newframe.size.height = newheight;
self.frame = newframe;
}
-(UIView*(^)(CGFloat))ch_height{
WEAKENING_SELF;
return ^(CGFloat newheight){
USING_SELF;
self.height = newheight;
return self;
};
}
- (CGFloat) width{
return self.frame.size.width;
}
- (void) setWidth: (CGFloat) newwidth{
CGRect newframe = self.frame;
newframe.size.width = newwidth;
self.frame = newframe;
}
-(UIView*(^)(CGFloat))ch_width{
WEAKENING_SELF;
return ^(CGFloat newwidth){
USING_SELF;
self.width = newwidth;
return self;
};
}
- (CGFloat) top{
return self.frame.origin.y;
}
- (void) setTop: (CGFloat) newtop{
CGRect newframe = self.frame;
newframe.origin.y = newtop;
self.frame = newframe;
}
-(UIView*(^)(CGFloat))ch_top{
WEAKENING_SELF;
return ^(CGFloat newtop){
USING_SELF;
self.top = newtop;
return self;
};
}
- (CGFloat) left{
return self.frame.origin.x;
}
- (void) setLeft: (CGFloat) newleft{
CGRect newframe = self.frame;
newframe.origin.x = newleft;
self.frame = newframe;
}
-(UIView*(^)(CGFloat))ch_left{
WEAKENING_SELF;
return ^(CGFloat newleft){
USING_SELF;
self.left = newleft;
return self;
};
}
- (CGFloat) bottom{
return self.frame.origin.y + self.frame.size.height;
}
- (void) setBottom: (CGFloat) newbottom{
CGRect newframe = self.frame;
newframe.origin.y = newbottom - self.frame.size.height;
self.frame = newframe;
}
-(UIView*(^)(CGFloat))ch_bottom{
WEAKENING_SELF;
return ^(CGFloat newbottom){
USING_SELF;
self.bottom = newbottom;
return self;
};
}
- (CGFloat) right{
return self.frame.origin.x + self.frame.size.width;
}
- (void) setRight: (CGFloat) newright{
CGFloat delta = newright - (self.frame.origin.x + self.frame.size.width);
CGRect newframe = self.frame;
newframe.origin.x += delta ;
self.frame = newframe;
}
-(UIView*(^)(CGFloat))ch_right{
WEAKENING_SELF;
return ^(CGFloat newright){
USING_SELF;
self.right = newright;
return self;
};
}
-(UIView*(^)(UIColor*))ch_backgroundColor{
WEAKENING_SELF;
return ^(UIColor* newcolor){
USING_SELF;
self.backgroundColor = newcolor;
return self;
};
}
-(UIView*(^)(BOOL))ch_userInteractionEnabled{
WEAKENING_SELF;
return ^(BOOL newinteraction){
USING_SELF;
self.userInteractionEnabled = newinteraction;
return self;
};
}
-(UIView*(^)(CGFloat))ch_cornerRadius{
WEAKENING_SELF;
return ^(CGFloat newradius){
USING_SELF;
self.layer.cornerRadius = newradius;
return self;
};
}
-(UIView*(^)(BOOL))ch_clipsToBounds{
WEAKENING_SELF;
return ^(BOOL newclipbounds){
USING_SELF;
self.clipsToBounds = newclipbounds;
return self;
};
}
-(UIView*(^)(UIView*))ch_addToView{
WEAKENING_SELF;
return ^(UIView* parentView){
USING_SELF;
if(self.superview == nil){
[parentView addSubview:self];
}
else if(self.superview && parentView != self.superview){
[self removeFromSuperview];
[parentView addSubview:self];
}
return self;
};
}
-(UIView*(^)(NSInteger))ch_tag{
WEAKENING_SELF;
return ^(NSInteger newtag){
USING_SELF;
self.tag = newtag;
return self;
};
}
-(UIView*(^)(CGFloat))ch_alpha{
WEAKENING_SELF;
return ^(CGFloat newalpha){
USING_SELF;
self.alpha = newalpha;
return self;
};
}
-(UIView*(^)(BOOL))ch_hidden{
WEAKENING_SELF;
return ^(BOOL newhidden){
USING_SELF;
self.hidden = newhidden;
return self;
};
}
ImpChainProp(UILabel);
@end
以上就是我近期对于链式的简单尝试,感觉还是挺有用的。另外,对于链式化的定义,其实也是可以用宏来统一定义的,但这样的话,为了避免不同类型的问题,可能入餐都要以对象的形式。比如入参都变为ch_width(@(100)),ch_hidden(@(YES))这类的,感觉有些费事,所以我还是保持了原有的基本入参类型,而最终也没有以宏的方式做成统一的定义。
不过,没准哪天写烦了,也就会去做定义了吧,也许......