iOS开发-链式实现数据源

前言

链式调用chained calls指在函数调用返回了一个对象的时候使得这个调用链可以不断的调用下去,从概念上可以看做是一环扣一环的铁链,也能被称作方法链调用。

假设需求是在网络请求完成之后先筛选过期数据,然后转换成对应的数据模型进行展示。在Swift中可以直接这么写:

let dataArr = result["data"] as! [Dictionary]
self.models = dataArr.filter{ $0["status"] == "1" }.map{ Model($0) }

OC的语法决定了这步操作不能像Swift一样简洁,最常见的代码就是这样:

NSArray * dataArr = result[@"data"];
NSMutableArray * models = @[].mutableCopy;
for (NSDictionary * dict in dataArr) {
    if ([dict[@"status"] isEqualToString: @"1"]) {
        [models addObject: [[Model alloc] initWithDict: dict]];
    }
}
self.models = [NSArray arrayWithArray: models];

对比两段代码,不难看出方法链调用的优点包括:

  • 代码简洁优雅,可读性强
  • 减少了重复使用同一变量的代码量
  • 把复杂的操作分割成多个小操作连续调用

Swift的特性决定了其在链式调用上的先天优势,但是有没有办法让OC也能拥有这种链式调用的特性呢?答案是毫无疑问的,block是一种非常优秀的机制,允许我们使用点语法的方式调用属性block

其他要求

实现链式调用做法并不复杂,但是符合这些要求会让你用起来更加得心应手。譬如:

  • block有过足够深的使用和了解
  • retain cycle深恶痛疾,网上很多教程实际上存在着循环引用的问题
  • 升级到Xcode8.3以上的版本,理由无他,加强了属性block调用的代码联想

其中第三点是笔者最推崇的要求,要知道8.3之前的链式封装多多少少吃了不少代码联想的苦头

丑陋的数据源

UITableView是个非常牛逼的控件,但是对于开发者而言也并不是那么的友善,甚至有些丑陋。实现一次tableView的代理起码要有以下代码:

#pragma mark - UITableViewDataSource
- (NSInteger)tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger)section {
    return self.dataSource.count;
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier: @"reuseIdentifier"];
    /// configure cell
    return cell;
}

#pragma mark - UITableViewDelegate
- (void)tableView: (UITableView *)tableView didSelectRowAtIndexPath: (NSIndexPath *)indexPath {
    /// do something
}

Protocol是一种非常优雅的设计方案,这点是毋庸置疑的。但即便再优雅的代码设计,在重复的代码面前,也会让人感到丑陋、厌倦。

block相较起代理,是一种更加强大的机制。比前者更解耦更简洁,当然两者的优劣比较不是本文要讨论的问题,通过block调用的返回对象,大可以给NSArray实现这样的代码:

NSArray * dataArr = result[@"data"];
self.models = dataArr.filter(^BOOL(NSDictionary * dict) {
    return [dict[@"status"] isEqualToString: @"1"];
}).map(^id(NSDictionary * dict) {
    return [[Model alloc] initWithDict: dict];
});

虽然代码简洁性上仍然差了Swift一筹,但是相较起原执行代码逻辑性跟可读性都强了不少,这部分的代码详见由浅至深学习block

链式数据源的实现

由于谁都可能是UITableView的数据源对象,那么最粗暴的做法是为NSObject提供一个category用来实现相关的数据源并且动态添加属性。但是作为有追求的攻城狮,我们有追求,要优雅,所以这种方式直接cut

UITableView的繁杂代码一直是热议的话题之一,在不同的代码架构中有不同的解决方案,比如MVVM中封装一个专门的VM来统一处理这部分逻辑。因此可以提供一个对象作为实际数据源对象跟UITableView之间的中介。由中介实现相关协议方法,然后从实际数据源对象方获取数据


中介被我命名为LXDTableViewProtocolHelper,为了保证能够生成方法链,所有的属性block应当返回中介对象:

@class LXDTableViewProtocolHelper;
typedef LXDTableViewProtocolHelper *(^TVNumberOfSections)(NSInteger(^)(void));
typedef LXDTableViewProtocolHelper *(^TVRowsNumberAtSection)(NSInteger(^)(NSInteger section));
typedef LXDTableViewProtocolHelper *(^TVDidSelectedCellHandle)(void(^)(NSIndexPath * index));
typedef LXDTableViewProtocolHelper *(^TVConfigureCell)(void(^)(__kindof UITableViewCell * cell, NSIndexPath * index));

typedef LXDTableViewProtocolHelper *(^TVBindAndRegister)(UITableView * tableView, Class cellCls, BOOL isNib, NSString * reuseIdentifier);

@interface LXDTableViewProtocolHelper : NSObject<UITableViewDelegate, UITableViewDataSource>

@property (nonatomic, readonly) TVConfigureCell configurateCell;
@property (nonatomic, readonly) TVBindAndRegister bindTableView;
@property (nonatomic, readonly) TVNumberOfSections sectionsNumber;
@property (nonatomic, readonly) TVRowsNumberAtSection rowsNumber;
@property (nonatomic, readonly) TVDidSelectedCellHandle didSelectedCell;

@end

使用只读属性修饰block之后我们只需重写getter方法返回对应的处理就行了,拿rowsNumber做个例子,按照网上很多教程都会这么写:

- (TVRowsNumberAtSection)rowsNumber {
    return ^LXDTableViewProtocolHelper *(NSInteger(^numberOfRowsInSection)(NSInteger section)) {
        self.numberOfRowsInSection = numberOfRowsInSection;
        return self;
    };
}

但是实际上这个返回的block__NSMallocBlock__类型的,这意味着这种做法会让中介被引用。当然笔者没去测试中介是否能正确释放而是直接采用__weak做法,感兴趣的读者可以重写dealloc来检测。最后tableView的协议方法就能被这样实现:

- (void)configureTableViewProtocol {
    WeakDefine
    self.listHelper.bindTableView(_list, [UITableViewCell class], NO, @"cell").rowsNumber(^NSInteger(NSInteger section) {
        return weakself.models.count;
    }).configurateCell(^(SingleTitleCell * cell, NSIndexPath * index) {
        cell.titleLabel.text = weakself.models[index.row];
    }).didSelectedCell(^(NSIndexPath *index) {
        [weakself updateInfoWithModel: weakself.models[index.row]];
    });
}

更进一步

将协议相关方法抽离之后,不免要持有LXDTableViewProtocolHelper的实例对象,这样难免有些不友善。通过category的方式对UITableView添加相关的接口来生成并绑定一个helper是一个不错的选择:

@interface UITableView (LXDProtocolConfigure)

- (void)configureHelper: (void(^)(LXDTableViewProtocolHelper * helper))configuration;

@end

- (void)configureHelper: (void(^)(LXDTableViewProtocolHelper * helper))configuration {
    LXDTableViewProtocolHelper * helper = [LXDTableViewProtocolHelper new];
    configuration(helper);
    self.helper = helper;
}

使用associate相关函数动态绑定helper,由于后者对前者的属性修饰为assign或者weak也不会导致循环引用的发生,这样通过下面的代码就完成了数据源的抽离:

[_dishesList configureHelper: ^(LXDTableViewProtocolHelper * _Nonnull helper) {
    helper.bindTableView(_dishesList, [KMCDishesCell class], YES, @"cell").rowsNumber(^NSInteger(NSInteger section) {
        return 0;
    }).configurateCell(^(__kindof UITableViewCell *cell, NSIndexPath *index) {
        
    }).didSelectedCell(^(NSIndexPath *index) {
        
    });
}];

更多

得益于强大的block,即便OC没有Swift那么优雅的高阶函数,依旧能够实现让代码紧凑已读,当然也会提高debug的难度。除了将数据源链式之外,你还可以尝试把网络请求进行封装,做成链式处理,比如笔者的请求代码:

Get(Component(@"user/getUserInfo", nil)).then(^(NSDictionary * result) {
    /// request success
}).failed(^(NSError * error) {
    /// request failed
}).start();
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 你身边是否也有过一个人缘极好的人,只要有他在的地方都是欢声笑语,且这类人办起事情来效率都很高,而每次你都是...
    老商长谈阅读 323评论 0 3
  • 俗话说自强不息,我一直都以为自己是一个特别软弱的人。没错,我的确是软弱的人,在很多方面我都不能将自己的能力发挥出来...
    落叶知秋寒意凉阅读 551评论 0 1
  • 执笔作刀枪,负箧行万里。 甘为苦行僧,淡泊名利场。 世人生来此,糊涂常相忘。 利刃头上悬,不肯名利放。 笔锋所到处...
    乐陵君阅读 250评论 0 1
  • 今天天气很热,主要的任务就是进行塔吊基础混凝土养护。养护开始标准是脚踩在混泥土上没有脚印。之后便开始喷水养护,由于...
    地底一人阅读 254评论 0 0