CBBlockKit:我只想用Block写东西
发布于 3个月前 作者 cbangchen 394 次浏览 来自 分享

CBBlcokKit

前言

这个工具集后续会持续维护,更希望的是初学者查看源码后,对于Block的使用Category构建Delegate和DataSourse代码分离的思想能有一定的了解。感谢Welkin Xie一直以来对于我的指导。👍🏻

项目地址

Github: https://github.com/cbangchen/CBBlockKit 感谢大家的支持🙏🏻

安装方法

后续将集成到Cocoapods,现在请直接拖入工程即可。

为什么要写这个工具集

得益于Lighter View Controllers对于ViewController的轻量化技巧讲述,得益于BlocksKit万物不Block的思想影响。

我觉得,如果一个Block管理一个视图对象,所有的关于该对象的属性设置,事件监听和数据安装都只在且只在该Block中出现,这样的话对于每一个视图对象的管理将变得简单,代码风格也得到一定的程度的统一,一定是不错的事情。

这个Kit的所有功能

  • 统一代码风格,增加代码可读性。

  • Block执掌大局,调用Block代替繁杂的代理方法和数据源方法,同时原代理和数据源方法依然正常使用,且原代理和数据源方法具有更高优先级。

  • 对于AFNetworing的二次封装,自定义缓存策略,严格把控缓存有效性。

  • 对于FMDB的二次封装,简单易用,轻量级满足需求。

  • 集成了Masonry框架,构建页面更简单 <- 神器不用说吧。

  • 配合InjectionForXcode插件,动态进行界面修改,更Runtime。

关于Live Reload的几点说明

这个功能的使用是通过插件InjectionForXcode来实现的,直接点击下载安装,然后重启XCode后Load bundles一下就可以了。(不支持XCode8) 如果以上方法无法安装,请点击此处进入Github下载安装。

本工具集Category的源码分析

UIView

整个Kit最重要的部分是Category,而由于几乎所有的控件均继承于UIViewUIControl也继承于UIView),所以Category最重要的部分就是对于UIView的拓展了。

1.首先是对于坐标运算的拓展。

使用SetterGetter来做:

// Getter
- (CGFloat)originLeft {
    return self.frame.origin.x;
}
// Setter
- (void)setOriginLeft:(CGFloat)originLeft {
    if (!isnan(originLeft)) {
        self.frame = CGRectMake(originLeft, self.originUp, self.sizeWidth, self.sizeHeight);
    }
}

2.四大手势(单击,双击,长按,拖拽)的添加。

首先声明Block属性(仅分析单击手势,其他手势同)。

typedef void(^CBGestureBlock)(id s);

@property (nonatomic, copy) CBGestureBlock cb_singleTapBlock;

利用objc_getAssociatedObject方法来取得绑定的属性值。对应绑定的字符为属性名。

- (CBGestureBlock)cb_singleTapBlock {
    return objc_getAssociatedObject(self, @"cb_singleTapBlock");;
}

由于Category中的属性不会自己生成SetterGetter,所以这里使用objc_setAssociatedObject方法来绑定Block属性值。达到相同的效果。然后利用searchSpedifiedGestureWithGestureClass:numOfTouch:方法来寻找已添加的手势,防止重复添加。

- (void)setCb_singleTapBlock:(CBGestureBlock)cb_singleTapBlock {
    objc_setAssociatedObject(self,
                             @"cb_singleTapBlock",
                             cb_singleTapBlock,
                             OBJC_ASSOCIATION_COPY_NONATOMIC);
    
    if (cb_singleTapBlock) {
        UITapGestureRecognizer *singleTap = (UITapGestureRecognizer *)[self searchSpedifiedGestureWithGestureClass:[UITapGestureRecognizer class]
                                                                                                        numOfTouch:1];
        if (!singleTap) {
            singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self
                                                                action:@selector(singTapAction:)];
        }
        
        [self removeGestureRecognizer:singleTap];
        
        [self addGestureRecognizer:singleTap];
    }
}
- (UIGestureRecognizer *)searchSpedifiedGestureWithGestureClass:(Class)gestureClass
                                                     numOfTouch:(NSInteger)numOfTouch {
    __block UIGestureRecognizer *gestureObj;
    
    [self.gestureRecognizers enumerateObjectsUsingBlock:^(__kindof UIGestureRecognizer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([obj isKindOfClass:gestureClass]) {
            if (gestureClass == [UITapGestureRecognizer class]) {
                if (numOfTouch) {
                    if ([obj numberOfTouches] == numOfTouch) {
                        gestureObj = obj;
                    }
                }
            }else {
                gestureObj = obj;
            }
        }
    }];
    return gestureObj;
}

3.信号设置和信号发送方法。

设置了这样的一个属性,当它的值发生改变时,执行CBEventMonitorBlock,并将自身和改变过的字段传过去,以便于判断后执行操作。

@property (nonatomic, copy) NSString *cb_signal;

typedef void(^CBEventMonitorBlock)(id object, id signal);

@property (nonatomic, copy) CBEventMonitorBlock cb_eventMonitor;
- (void)setCb_signal:(NSString *)cb_signal {
    objc_setAssociatedObject(self,
                             @"cb_signal",
                             cb_signal,
                             OBJC_ASSOCIATION_COPY_NONATOMIC);
    
    if (self.cb_eventMonitor) {
        self.cb_eventMonitor(self, cb_signal);
    }
}

UIControl

UIControl继承于UIView并相比之多了直接添加事件,管理事件的功能,所以在这里我们添加的不再是手势Block而是事件Block,直接取代addTarget后再执行事件过程,类似RAC的事件绑定。

cb_touchUpInsideBlock,其他事件Block同。

typedef void(^CBActionBlock)(id sender);

@property (nonatomic, copy) CBActionBlock cb_touchUpInsideBlock;
- (void)setCb_touchUpInsideBlock:(CBActionBlock)cb_touchUpInsideBlock {
    objc_setAssociatedObject(self,
                             @"cb_touchUpInsideBlock",
                             cb_touchUpInsideBlock,
                             OBJC_ASSOCIATION_COPY_NONATOMIC);
    
    [self removeTarget:self
                action:@selector(touchUpInside:)
      forControlEvents:UIControlEventTouchUpInside];
    
    if (cb_touchUpInsideBlock) {
        [self addTarget:self
                 action:@selector(touchUpInside:)
       forControlEvents:UIControlEventTouchUpInside];
    }
}
- (void)valueChanged:(id)sender {
    if (self.cb_valueChangedBlock) {
        self.cb_valueChangedBlock(self);
    }
}

UITableview

Delegate和DataSourse的分离

根据代理方法和数据源,另创建了两个文件CBTableViewDataSourseCBTableViewDelegate来接收UITableView的代理方法和数据源方法。

static CBTableViewDataSourse *tableViewDataSourse;
    
static CBTableViewDelegate *tableViewDelegate;
    
tableViewDataSourse = [[CBTableViewDataSourse alloc] initWithCellIdentifier:cellIdentifier];

tableViewDelegate = [[CBTableViewDelegate alloc] init];

tableViewDataSourse.realDataSourse = delegate;
    
tableViewDelegate.realDelegate = delegate;

tableView.delegate = tableViewDelegate;
    
tableView.dataSource = tableViewDataSourse;

值得注意的是,我们在tableViewDelegatetableViewDataSourse中依然设置了@property (nonatomic, weak, readwrite) id realDataSourse;的变量来存储我们在方法外设置的原代理对象,以确保原代理和数据源方法依然正常使用且具有更高的优先级。

代理方法和数据源方法的设置

cellForRowAtIndexPath方法为例,其他方法同。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell;
    
    if (_realDataSourse && [_realDataSourse respondsToSelector:@selector(tableView:cellForRowAtIndexPath:)]) {
        cell = [_realDataSourse cellForRowAtIndexPath:indexPath];
    }else {
        if (_cb_tableViewCellConfigureBlock) {
            
            cell = [tableView dequeueReusableCellWithIdentifier:_cellIdentifier];
            
            if (!cell)
                cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:_cellIdentifier];
            
            if (_cb_tableViewCellConfigureBlock) {
                _cb_tableViewCellConfigureBlock(cell, indexPath);
            }
        }else {
            NSAssert(_cb_tableViewCellConfigureBlock, @"CellForRowAtIndexPathBlock can't be nil.");
        }
    }
    return cell;
}

从以上方法我们可以看出,首先判断的是原代理对象是否响应该代理方法或者数据源方法。 而如果该方法返回数据且原代理对象响应该方法,即仅执行原代理对象中的代理方法或者数据源方法,如果该方法返回数据但原代理对象不响应该方法,则执行Blcok。 如果该方法为void方法,不返回数据,即同时执行原代理方法或者数据源方法和Block。

代码例子

其他Category

其他的视图对象拓展大抵与UITableView相似,请下载源码查看。

本工具集Network的源码分析

此模块之前已经开源,点击进入Github查看,点击进入博客查看分析。

本工具集Store的源码分析

这个模块主要是对于FMDB的一个简单封装,支持Json数据的直接存储,非常轻量,满足大部分的应用的需求。

打开或创建数据库

- (instancetype)initDatabaseWithDBName:(NSString *)dbName tableName:(NSString *)tableName;

这个方法里面首先组合数据库存储路径,然后创建FMDatabaseQueue类型的线程block对象,判断表格是否存在,如果不存在,执行创建操作。


- (instancetype)initDatabaseWithDBName:(NSString *)dbName
                             tableName:(NSString *)tableName {
    self = [super init];
    
    if (self) {
        NSAssert(dbName.length, @"String of the dbName can't be empty.");
        
        NSAssert(tableName.length, @"String of the tableName can't be empty.");
        
        NSString * path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]stringByAppendingPathComponent:dbName];
        
        NSLog(@"%@", path);
        
        if (_dbQueue) {
            [_dbQueue close];
        }
        
        _dbQueue = [FMDatabaseQueue databaseQueueWithPath:path];
        
        __block BOOL tableExit = NO;
        
        [_dbQueue inDatabase:^(FMDatabase *db) {
            tableExit = [db tableExists:tableName];
        }];
        
        if (!tableExit) {
            NSString * sql = [NSString stringWithFormat:@"CREATE TABLE IF NOT EXISTS %@ (OBJECTID TEXT NOT NULL, DATA TEXT NOT NULL, SAVETIME TEXT NOT NULL, PRIMARY KEY(OBJECTID))", tableName];
            
            __block BOOL crateTableSuccess = NO;
            
            [_dbQueue inDatabase:^(FMDatabase *db) {
                crateTableSuccess = [db executeUpdate:sql];
            }];
            
            if (!crateTableSuccess) {
                NSLog(@"Failed to crate the table named %@", tableName);
            }
        }
    }
    return self;
}

存储数据

- (void)saveObject:(id)object withKey:(NSString *)key intoTable:(NSString *)tableName;

存储判断数据是否可以转化为JSON数据(NSString, NSNumber, NSArray, NSDictionary等),如果可以即使用NSJSONSerialization类的方法将其转化为JSON数据,然后在FMDatabaseQueue对象中执行存储操作。

- (void)saveObject:(id)object
           withKey:(NSString *)key
         intoTable:(NSString *)tableName {
    NSAssert(object, @"Object which wanna be saved can't be nil.");
    
    NSAssert(key.length, @"String of the objectID can't be empty.");
    
    NSAssert(tableName.length, @"String of the tableName can't be empty.");
    
    NSError *error;
    
    NSData *data;
    
    if ([NSJSONSerialization isValidJSONObject:object]) {
        data = [NSJSONSerialization dataWithJSONObject:object options:NSJSONWritingPrettyPrinted error:&error];
    }
    
    if (error) {
        NSLog(@"%@", error);
        
        return;
    }
    
    NSString * dataString = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
    
    NSDate * currentTime = [NSDate date];
    
    NSString * sql = [NSString stringWithFormat:@"REPLACE INTO %@ (OBJECTID, DATA, SAVETIME) values (?, ?, ?)", tableName];
    
    __block BOOL saveObjectSuccess = NO;
    
    [_dbQueue inDatabase:^(FMDatabase *db) {
        saveObjectSuccess = [db executeUpdate:sql, key, dataString, currentTime];
    }];
    
    if (!saveObjectSuccess) {
        NSLog(@"Failed to save the object with key : %@ from the table : %@", key,tableName);
    }
}

获取数据,删除数据

获取数据的过程与存储数据相似,取出数据,将其转化为JSON数据,并返回。删除数据则是根据数据的KEY来决定,相似不表。

总结

更详细的代码请大家下载源码查看,谢谢大家的关注,如果可以,请点个star支持一下,谢谢。

回到顶部