澳门新葡新京 > 关于我们 > 一个简单的可展开和收缩的tableview,可复用而且高度解耦的用户统计埋点实现

一个简单的可展开和收缩的tableview,可复用而且高度解耦的用户统计埋点实现
2020-04-06 11:51

![Uploading Extand tableView_547722.gif . . .]写了好多的tableview,只是把常用的tableview的应用场景给大家介绍下,(__) 嘻嘻……,话不多说,今天介绍的是一个简单的可以展开和收缩的tableview,类似于qq好友类表。

  • NSString * -> NSDate *

声明:本文是本人 编程小翁 原创,转载请注明。

最近在和小伙伴聊天的时候发现初学者很容易将 设计模式、架构模式、框架给弄混,所以想将最近设计模式和架构模式做个整理,顺便和大家一起分享下。

图片 1Extand tableView.gif

图片 2用户统计.jpeg用户行为统计(User Behavior Statistics, UBS)一直是移动互联网产品中必不可少的环节,也俗称埋点。在保证移动端流量不会受较大影响的前提下,PM们总是希望埋点覆盖面越广越好。目前常规的做法是将埋点代码封装成工具类,但凡工程中需要埋点(如点击事件、页面跳转)的地方都插入埋点代码。一旦项目越来越复杂,你会发现埋点的代码散落在程序的各个角落,不利于维护以及复用。本文旨在探讨利用iOS的运行时机制实现一种可复、解耦、容易维护的用户统计方案。探讨毕竟是探讨,欢迎到在简书留言讨论。本文虽有些长却是用心之作,希望你有耐心看完。

设计模式:设计模式可以通俗的理解为实现/解决某些问题,而形成的解决方案规范。增加代码的可重用性,让代码能更容易理解和可靠。我们通常说所的代理模式、迭代器模式、策略模式就属于这一类。对各种设计模式的了解可以帮助我们更快的解决编程过程中遇到的问题。

首先,犹豫是简单的demo,我们就自己构造数据。

- (nullable NSDate *)dateFromString:(NSString *)string;

注:本文需要一些iOS的Runtime基础

架构模式:架构模式的出现时为了管理复杂的应用程序,这样可以在一个时间内专门关注一个方面。例如,您可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易。同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。我们经常说的MVC架构、MVVM架构属于此类。

for (int i = 0; i < 4; i++) { BaseDataModel *model = [[BaseDataModel alloc] init]; model.isOpen = NO; NSString *name = [NSString stringWithFormat:@"Section:%d",i]; model.name = name; NSMutableArray *array = [NSMutableArray arrayWithCapacity:5]; for (int j = 0; j < 4; j++) { NSString *cellName = [NSString stringWithFormat:@"Cell:%d",j]; [array addObject:cellName]; } model.dataArray = array; [self.dataArray addObject:model];}
  • NSDate * -> NSString *

该方案的完成将会用到以下知识:

框架:这个最好理解了,通常是代码重用。框架与设计模式的概念容易弄混,两者有相似之处,但却有着根本的不同。设计模式是对在某种环境中反复出现的问题以及解决该问题的方案的描述规范,它比框架更抽象;框架为已经解决问题的具体实现方法,能直接执行或复用;设计模式是比框架更小的元素,一个框架中往往含有一种或多种设计模式。Xcode自带的Foundation、UIKit,以及我们经常使用的AFNetworking,MJExtension,SVProgressHUD就属于这一类。

BaseModel是我们的一个model类。OK,当我们的数据构造好了,接下来就是设计我们的tableview里的section-headerview,主要是给headerview添加一个点击事件,之后在我们的mainviewcontroller里响应,这边可以有很多种解决方法(可以delegate,也可以block,也可以通知)。我用的是block,相对来说简单点。tap事件代码如下:

  • Method Swizzling

接下来我们说说今天的主题,设计模式。

if  { [UIView animateWithDuration:0.3 animations:^{ _imageView.transform = CGAffineTransformRotate(_imageView.transform, -M_PI / 2); }]; self.closeblock(self.section);}else{ [UIView animateWithDuration:0.3 animations:^{ _imageView.transform = CGAffineTransformRotate(_imageView.transform, M_PI / 2); }]; self.openblock(self.section);}self.isOpen = !self.isOpen;
- (NSString *)stringFromDate:date;
  • 单元测试

设计模式:设计模式主要分三个类型:创建型、结构型和行为型。

回到我们的mainviewcontroller里:

接着开头的话题,我们先回顾一下主流的埋点是怎么做的。我粗糙地将埋点分为两种:1、页面统计,包括页面停留时间、页面进入次数;2、交互事件统计,包括单击、双击、手势交互等。

一、Abstract Factory,抽象工厂:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们的具体类。也就是通常说的类方法

HeaderView *headerView = [[HeaderView alloc] initWithFrame:CGRectMake(0, 0, WIDTH, 40)];headerView.nameLabel.text = model.name;headerView.section = section;__weak typeof weakself = self;headerView.openblock =^(NSInteger secion){ [weakself openSection:section];};headerView.closeblock = ^(NSInteger section){ [weakself closeSection:section];};

以统计页面进入次数为例,最简单粗暴的做法是在所有页面的viewDidAppear:以及viewDidDisappear:中分别埋点,将自己对应的pageID上传给服务端。代码大概长酱紫:

二、Builder,建造模式:将一个复杂对象的构建与他的表示相分离,使得同样的构建过程可以创建不同的表示。

展开的方法是:

  • 2015-11-20 09:10:05
@implementation HomeViewController//...other methods- viewDidAppear:animated{ [super viewWillAppear:animated]; [WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_ENTER"];}- viewDidDisappear:animated{ [super viewDidDisappear:animated]; [WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_LEAVE"];}@end

三、Factory Method,工厂方法:定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory为一个产品族提供了统一的创建接口。

BaseDataModel *model = self.dataArray[section];model.isOpen = !model.isOpen;NSMutableArray *indexArray = [NSMutableArray arrayWithCapacity:10];for (int i = 0; i < model.dataArray.count; i++) { NSIndexPath *indexpath = [NSIndexPath indexPathForRow:i inSection:section]; [indexArray addObject:indexpath];}[self.tableView insertRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationFade];

+[WUserStatistics sendEventToServer:]封装网络请求,将ID上传给服务器。上述方案有以下弊端:

四、Prototype,原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型来创建新的对象。

关闭的方法是:

// 时间字符串NSString *string = @"2015-11-20 09:10:05";// 日期格式化类NSDateFormatter *fmt = [[NSDateFormatter alloc] init];// 设置日期格式fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";// NSString * -> NSDate *NSDate *date = [fmt dateFromString:string];NSLog(@"%@", date);

1、复用性差。这部分埋点代码很难给其他项目复用2、工作量大。尤其当页面较多时,需要修改的代码较多3、引入“脏代码”,不易维护

五、Singleton,单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问点。

BaseDataModel *model = self.dataArray[section];model.isOpen = !model.isOpen;NSMutableArray *indexArray = [NSMutableArray arrayWithCapacity:10];for (int i = 0; i < model.dataArray.count; i++) { NSIndexPath *indexpath = [NSIndexPath indexPathForRow:i inSection:section]; [indexArray addObject:indexpath];}[self.tableView deleteRowsAtIndexPaths:indexArray withRowAnimation:UITableViewRowAnimationFade];
  • 11月-20号/2015年 09-10:05秒

第3点提到的“脏代码”意思是用户行为分析这种业务其实跟主业务没太大关系,不应该保持如此高的耦合度,因为这些代码会干扰我们对项目主业务的维护。这个我个人看法。

一、Adapter,适配器模式:将一类的接口转换成客户希望的另外一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作那些类可以一起工作。

由于当你删除或者添加数据的时候,对应的datasource也要做出相应的改变,所以在返回numberOfRowsInSection时:

常规做法一般在交互事件的selector中获取该事件的ID并上传给服务端,代码大概长酱紫:

二、Bridge,桥模式:将抽象部分与它的实现部分相分离,使他们可以独立的变化。

BaseDataModel *model = self.dataArray[section];if (model.isOpen) { return model.dataArray.count;}else{ return 0;}
// 时间字符串NSString *string = @"11月-20号/2015年 09-10:05秒";// 日期格式化类NSDateFormatter *fmt = [[NSDateFormatter alloc] init];fmt.dateFormat = @"MM月-dd号/yyyy年 HH-mm:ss秒";NSLog(@"%@", [fmt dateFromString:string]);
- onFavBtnPressed:sender{ [WUserStatistics sendEventToServer:@"CTRL_EVENT_HOME_FAV"]; //...do other things}

三、Composite,组合模式:将对象组合成树形结构以表示部分整体的关系,Composite使得用户对单个对象和组合对象的使用具有一致性。

因为tableView有自己的重用机制,sectionHeaderView也会被重用,所以如果不设置好数据源多的时候会乱掉,在MVC的设计模式里,用于控制View的状态的是model,于是可以将控制状态的参数写入init初始化里:

  • Tue May 31 17:46:55 +0800 2011

稍微大一点的APP如果采用这种方式,那诸如此类的埋点代码将遍地都是。它的缺点参考页面统计埋点部分,其复用性基本为零,也就是在新项目中根本无法复用埋点代码。

四、Decrator,装饰模式:动态地给一个对象增加一些额外的职责,就增加的功能来说,Decorator模式相比生成子类更加灵活。

- (instancetype)initWithFrame:frame IsOpen:isOpen { if (self = [super initWithFrame:frame]) { self.backgroundColor = [UIColor whiteColor]; [self addSubview:self.nameLabel]; [self addSubview:self.imageView]; [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector]]; self.isOpen = isOpen; if (self.isOpen) { _imageView.transform = CGAffineTransformRotate(_imageView.transform, M_PI / 2); } } return self;}

小总结一下,采用常规的做法虽然直观方便,但在可复用性、可维护性等方面有所欠缺。在我看来,借助运行时可以很好地避开这些缺点。

五、Facade,外观模式:为子系统中的一组接口提供一致的界面,Facade提供了一高层接口,这个接口使得子系统更容易使用。

这样的话,一个简单的展开收缩的tableview就完成了。demo地址:https://github.com/ioscick/Extand-TableView

// 时间字符串NSString *string = @"Tue May 31 17:46:55 +0800 2011";// 日期格式化类NSDateFormatter *fmt = [[NSDateFormatter alloc] init];fmt.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy";// fmt.dateFormat = @"EEE MMM dd HH:mm:ss ZZZZ yyyy";// 设置语言区域(因为这种时间是欧美常用时间)fmt.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];NSLog(@"%@", [fmt dateFromString:string]);

由于Runtime知识不属于本文的重点,这里只简单介绍。在iOS中,我们可以在运行时替换两个方法的实现,达到“勾住”某个方法并注入代码的目的。具体做法是:

六、Proxy,代理模式:为其他对象提供一种代理以控制对这个对象的访问。

欢迎各位阅读,希望能帮到各位,如果有不正确的地方也可以一起探讨~ thanks。

  • 1745645645645

重载类的“+load”方法,在程序加载到内存时利用Runtime的method_exchangeImplementations等接口将方法的实现互相交换。当方法M被调用时就会被勾住,执行我们的方法。

七、Flyweight,享元模式: 通过共享以便有效的支持大量小颗粒对象。

这种技术也称为Method Swizzling,属于面向切面编程(Aspect-Oriented Programming)的一种实现。

一、Chain of Responsibility,职责链模式:为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。

上一篇:没有了 下一篇:没有了