在iOS开发中,经常是要用到UITableView的,我曾经思考过这样一个问题,为什么任何种类的model放到TableView和所需的cell里面,都可以正常显示?而我自己写的很多view却只是能放一种特定的model,就好像我这个view是专门为了展示这个model所设计的?有没有一种设计方法,使得我所设计的一些view也可以放任何合适种类的model,并且按照预期的那样正确展示呢?
(前一篇记录了个人理解的OC开发中代码规范以及代码通用性,这一篇主要是设计一个通用的进度条。)
为了解决这个问题,我特地去看了iOS开发中的一些设计模式,也研究过别人写的一些框架以及苹果给出的UITableView的方法,发现最主要的原因在于UITableViewDataSource和UITableViewDelegate上。
在这里,得说说我对iOS开发中数据源协议(dataSource)和代理协议(delegate)的理解,个人认为主要是为遵守这些个协议的类新增一些方法,而这些方法用来与拥有dataSourcedelegate的那个类进行通信(也可以说是处理事件、传递数据等)。比如说,TableView中的dataSource协议中必须要实现下面这三个方法:
#pragma mark - Table view 数据源方法 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView; - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
其中这两个方法,是tableView在向遵守UITableViewDataSource协议的类拿它所需要的数据,比如说,它将要展示多少组数据?每组数据多少行?
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
至于它内部的实现,我猜想应该是这么实现的:
NSInteger totalSection = [self.dataSource numberOfSectionsInTableView:self]; int rowsOfSection[totalSection]; for (int i = 0; i < totalSection; i++) { rowsOfSection[i] = [self.dataSource tableView:self numberOfRowsInSection:i]; }
上面的self代指tableView自己本身。内部通过这样类似的方法,就可以拿到所要展示的组数以及每组展示的行数了(苹果具体如何实现我并不知道,但这不妨碍我猜想,按照其设计的思想猜想其内部如何实现)。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
这个方法,在tableView里面是很有地位的。我猜想,苹果代码可能是通过dataSource调用这个方法,拿到cell(也就是一个view)然后去展示,也不仅仅是展示这么简单,还需要做重复利用这一操作,也就是缓存池的实现。如果让我来实现,我会在拿到对应位置的cell时,通过delegate里面的
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
得到cell的高度,设计好其在scrollView上的frame,然后在scrollView滚动的时候,拿到它的contentOffset(事实上,这是UIScrollView的属性,不过,UITableView继承自UIScrollView,所要也具有此属性),然后判断哪些cell需要显示在屏幕上,哪些cell不需要显示在屏幕上,需要显示在屏幕上得,先判断其是否正显示在屏幕上,如果没有,先从缓存池中找是否存在相同identifie的cell,不存在的话,那么就返回nil......当然,如果要实现一个缓存池,也是做得到的,但是在这一篇,并不打算详细写,具体可以查看以后的第四篇,关于一个瀑布流的实现
以上,是我猜想官方实现tableView的过程,事实上,UIKit框架不开源,我并不知道其内部如何实现,但是,按照我这种猜想的思路,我是可以实现一个UITableView的(也许,效率上会不如官方的)
基于此,我可以确定了,为什么UITableView可以展示不同种类的model数据?主要就是,它并不持有model,甚至可以理解为,UITableView内部并未有model,所以也就不存在只能展示某一种model数据了。
为什么会如此:
可以仔细看看,它是怎么得到数据的?是通过它的dataSource,不断的像遵守dataSource的类(控制器)问数据!
喂喂,控制器,你没告诉我,这个表格有多少组啊:
喂,控制器,你赶紧把每组多少行告诉我.....
控制器,你丫的不告诉我cell(view),我怎么知道如何展示它啊?
基于这样的思路,我在自己设计控件的时候,完全也可以是搞出一个dataSource协议,不断的向那些遵守了dataSource协议的家伙,要人要钱,不给?!信不信我分分钟崩溃一个给你看......
delegate也是可以参考上面思路实现的,不过它与dataSource所不同的是,dataSource所索要的是data(也就是数据),而delegate所需要是一些处理功能性上的方法,比如说UITableView中某个cell被点击了(选中了),那么这是事件处理,放在delegate里面比较合适,就比如我们总不能硬是将一种狗叫(说)成是一只猫......这个问题,在代码规范层面上看来,是很严肃的。
下面正式进入一个通用进度条的代码实现,用上面提到的方法实现,先来看看效果图:
只有四个进度:
有6个进度:
直接从码农调到了CTO,中间两个无效:
//如果引用我写个这个框架,以上只需要在控制器里面的代码:
#import "ViewController.h" #import "ZYProgressView.h" @interface ViewController () <ZYProgressViewDataSource, ZYProgressViewDelegate> @property (nonatomic, weak) ZYProgressView *progressView; @property (nonatomic, strong) NSArray *titles; @end @implementation ViewController - (NSArray *)titles { if (_titles == nil) { _titles = @[@"菜鸟", @"码农", @"高级工程师", @"项目经理", @"CTO", @"迎娶白富美"]; } return _titles; } - (void)viewDidLoad { [super viewDidLoad]; ZYProgressView *progressView = [[ZYProgressView alloc] init]; progressView.frame = CGRectMake(0, 100, self.view.frame.size.width, 170); progressView.dataSource = self; progressView.delegate = self; //纯代码实现,这个方法可调,可不调。但是如果是通过xib创建,必须要调用此方法 [progressView reloadData]; self.progressView = progressView; [self.view addSubview:progressView]; //显示到当前进度,从1开始 self.progressView.currentProgress = 6; //中间跳过两个状态,注意,存放的值,要从1开始 self.progressView.items = @[@(3),@(4)]; } #pragma mark ----ZYProgressViewDataSource //告诉progressView,总共要显示多少个进度 - (NSUInteger)numberOfProgressInProgressView { return self.titles.count; } //告诉progressView,每个进度的title,索引从0开始 - (NSString *)progressView:(ZYProgressView *)progressView titleAtIndex:(NSUInteger)index { return self.titles[index]; } @end
其他的,我也提供了实现各种不同颜色的方法,只需要遵守delegate协议,实现相应方法即可,比如说高亮的时候,为黄色:
//只需要在控制器里面加上如下代码:
#pragma mark ----ZYProgressViewDelegate - (UIColor *)highlightColorForCircleViewInProgressView:(ZYProgressView *)progressView { return [UIColor yellowColor]; }
其他更多特色的颜色、字体、间距等,.h文件里面有详细介绍,这里主要说说我实现的思路:
先附上代码:
// 用法与UITableView相当,需要遵守ZYProgressViewDataSource,ZYProgressViewDelegate // 其中ZYProgressViewDataSource里面的方法,是必须实现的 // ZYProgressViewDelegate里面的方法为可选择的(有待完善) #import <UIKit/UIKit.h> @class ZYProgressView; @protocol ZYProgressViewDataSource <NSObject> /** * 进度数目 * */ - (NSUInteger)numberOfProgressInProgressView; /** * 每个进度对应的标题 * * @param progressView * @param index 在index下对应的标题(index从0开始) * * @return 标题 */ - (NSString *)progressView:(ZYProgressView *)progressView titleAtIndex:(NSUInteger)index; @end @protocol ZYProgressViewDelegate <NSObject> @optional /** * 圆的normal颜色(默认normal颜色为灰色) * * */ - (UIColor *)colorForCircleViewInProgressView:(ZYProgressView *)progressView; /** * 圆的highlight颜色(默认为红色) * */ - (UIColor *)highlightColorForCircleViewInProgressView:(ZYProgressView *)progressView; /** * 标题的normal颜色(默认normal颜色为灰色) * * */ - (UIColor *)colorForTitleViewInProgressView:(ZYProgressView *)progressView; /** * 标题的hightlight颜色(默认颜色为红色) * */ - (UIColor *)highlightColorForTitleViewInProgressView:(ZYProgressView *)progressView; /** * 设置圆的半径,默认为10 * * @param progressView * */ - (CGFloat)radiusForCircleViewInProgressView:(ZYProgressView *)progressView; /** * 设置标题的字体,默认为11 * * @param progressView * */ - (UIFont *)fontForTitleViewInProgressView:(ZYProgressView *)progressView; @end @interface ZYProgressView : UIView @property (nonatomic, weak) id<ZYProgressViewDataSource>dataSource; @property (nonatomic, weak) id<ZYProgressViewDelegate>delegate; /** * 处理任务已经到了第n阶段,但是中间第n-4,n-5等阶段未完成的情况 * * items 数组,如果是第n-4,n-5阶段未完成,那么数组中存放@(n-4),@(n-5) 注意,存放的值,要从1开始 */ @property (nonatomic, strong) NSArray *items; /** * 当前进度,可显示高亮颜色,进度值应当从1开始 */ @property (nonatomic, assign) int currentProgress; /** * 刷新数据,当需要动态添加一个进度时,可重新刷新数据 * 如果是直接是在xib/storyboard里面创建,那么创建之后,在设置好dataSource和delegate之后,请马上调用此方法刷新数据 */ - (void)reloadData; @end
#import "ZYProgressView.h" @interface ZYProgressView () @property (nonatomic, strong) NSMutableArray *circles; @property (nonatomic, strong) NSMutableArray *lines; @property (nonatomic, strong) NSMutableArray *titles; @end #define DefaultRadius 10 #define DefaultFont [UIFont systemFontOfSize:11.0] #define DefaultCircleColor [UIColor colorWithRed:218 / 255.0 green:208 / 255.0 blue:209 / 255.0 alpha:1]; #define DefaultTitleColor [UIColor colorWithRed:218 / 255.0 green:208 / 255.0 blue:209 / 255.0 alpha:1]; #define DefaultHighCircleColor [UIColor colorWithRed:251.0 / 255.0 green:0 blue:52.0 / 255.0 alpha:1]; #define DefaultHighTitleColor [UIColor colorWithRed:102.0 / 255.0 green:102.0 / 255.0 blue:102.0 / 255.0 alpha:1]; @implementation ZYProgressView - (NSMutableArray *)circles { if (!_circles) { _circles = [NSMutableArray array]; } return _circles; } - (NSMutableArray *)lines { if (!_lines) { _lines = [NSMutableArray array]; } return _lines; } - (NSMutableArray *)titles { if (!_titles) { _titles = [NSMutableArray array]; } return _titles; } - (void)setCurrentProgress:(int)currentProgress { int numberOfProgress = (int)[self.dataSource numberOfProgressInProgressView]; _currentProgress = currentProgress; if (_currentProgress > numberOfProgress) { NSLog(@"Error: ZYProgressView中的currentProgress > numberOfProgress"); return; } [self statusViewForCurrentProgress:_currentProgress]; if (_items && _items.count > 0) { [self setItems:_items]; } } - (void)setItems:(NSArray *)items { _items = items; for (NSNumber *obj in items) { int number = obj.intValue - 1; UILabel *label = self.titles[number]; label.textColor = [self titleNormalColor]; UIView *circleView = self.circles[number]; circleView.backgroundColor = [self circleNormalColor]; } } - (void)reloadData { [self.circles makeObjectsPerformSelector:@selector(removeFromSuperview)]; [self.circles removeAllObjects]; [self.lines makeObjectsPerformSelector:@selector(removeFromSuperview)]; [self.lines removeAllObjects]; [self.titles makeObjectsPerformSelector:@selector(removeFromSuperview)]; [self.titles removeAllObjects]; int numberOfProgress = (int)[self.dataSource numberOfProgressInProgressView]; if (numberOfProgress == 0) return; for (int i = 0; i < numberOfProgress; i++) { NSString *title = [self.dataSource progressView:self titleAtIndex:i]; UILabel *label = [self labelWithTitle:title]; [self.titles addObject:label]; [self addSubview:label]; UIView *circleView = [[UIView alloc] init]; [self.circles addObject:circleView]; [self addSubview:circleView]; if (i != 0) { UIView *lineView = [[UIView alloc] init]; [self.lines addObject:lineView]; [self addSubview:lineView]; } } } - (void)layoutSubviews { [super layoutSubviews]; int numberOfProgress = (int)[self.dataSource numberOfProgressInProgressView]; if (numberOfProgress == 0) return; CGFloat marginLeft = 15; CGFloat marginRight = 15; CGFloat marginTop = 25; CGFloat marginRow = 12; CGFloat radiusOfCircle = [self radiusForCircle]; CGFloat lineHeight = 2; CGFloat lineWidth = (self.frame.size.width - numberOfProgress * radiusOfCircle - marginLeft - marginRight ) / ((double)numberOfProgress - 1.0) + 0.1; CGFloat circleViewX = marginLeft; for (int i = 0; i < numberOfProgress; i++) { UIView *circleView = self.circles[i]; circleView.frame = CGRectMake(circleViewX, marginTop, radiusOfCircle, radiusOfCircle); [self circleViewWithView:circleView]; UILabel *label = self.titles[i]; if (i == 0) { label.frame = CGRectMake(circleViewX, CGRectGetMaxY(circleView.frame) + marginRow, 0, 0); [label sizeToFit]; } else if (i != numberOfProgress - 1) { [label sizeToFit]; label.center = CGPointMake(circleView.center.x, 0); label.frame = CGRectMake(label.frame.origin.x, CGRectGetMaxY(circleView.frame) + marginRow, label.frame.size.width, label.frame.size.height); } else { [label sizeToFit]; label.frame = CGRectMake(CGRectGetMaxX(circleView.frame) - label.frame.size.width, CGRectGetMaxY(circleView.frame) + marginRow, label.frame.size.width, label.frame.size.height); } if (i != 0) { UIView *lineView = self.lines[i - 1]; lineView.frame = CGRectMake(CGRectGetMaxX([self.circles[i - 1] frame]), 0, lineWidth, lineHeight); lineView.center = CGPointMake(lineView.center.x, circleView.center.y); lineView.backgroundColor = [self circleNormalColor]; } circleViewX += lineWidth + circleView.frame.size.width; } if (self.currentProgress) { self.currentProgress = self.currentProgress; } } #pragma mark ---- private方法 - (UILabel *)labelWithTitle:(NSString *)title { UIFont *fontOfTitle = [self fontForTitle]; UIColor *colorOfTitle = [self titleNormalColor]; UILabel *label = [[UILabel alloc] init]; label.text = title; label.textAlignment = NSTextAlignmentCenter; label.textColor = colorOfTitle; label.font = fontOfTitle; return label; } - (void)circleViewWithView:(UIView *)view { UIColor *colorOfCircle = [self circleNormalColor]; view.layer.masksToBounds = YES; view.layer.cornerRadius = view.frame.size.width / 2.0; view.backgroundColor = colorOfCircle; } - (CGFloat)radiusForCircle { if ([self.delegate respondsToSelector:@selector(radiusForCircleViewInProgressView:)]) { return [self.delegate radiusForCircleViewInProgressView:self]; } return DefaultRadius; } - (UIFont *)fontForTitle { if ([self.delegate respondsToSelector:@selector(fontForTitleViewInProgressView:)]) { return [self.delegate fontForTitleViewInProgressView:self]; } return DefaultFont; } - (UIColor *)circleNormalColor { if ([self.delegate respondsToSelector:@selector(colorForCircleViewInProgressView:)]) { return [self.delegate colorForCircleViewInProgressView:self]; } return DefaultCircleColor; } - (UIColor *)circleHighColor { if ([self.delegate respondsToSelector:@selector(highlightColorForCircleViewInProgressView:)]) { return [self.delegate highlightColorForCircleViewInProgressView:self]; } return DefaultHighCircleColor; } - (UIColor *)titleNormalColor { if ([self.delegate respondsToSelector:@selector(colorForTitleViewInProgressView:)]) { return [self.delegate colorForTitleViewInProgressView:self]; } return DefaultTitleColor; } - (UIColor *)titleHighColor { if ([self.delegate respondsToSelector:@selector(highlightColorForTitleViewInProgressView:)]) { return [self.delegate highlightColorForTitleViewInProgressView:self]; } return DefaultHighTitleColor; } - (void)statusViewForCurrentProgress:(int)currentProgress { int numberOfProgress = (int)[self.dataSource numberOfProgressInProgressView]; UIColor *colorOfTitle = [self titleNormalColor]; UIColor *colorOfCircle = [self circleNormalColor]; for (int i = 0; i < numberOfProgress; i++) { UILabel *label = self.titles[i]; label.textColor = colorOfTitle; UIView *circleView = self.circles[i]; circleView.backgroundColor = colorOfCircle; if (i != 0) { UIView *lineView = self.lines[i - 1]; lineView.backgroundColor = colorOfCircle; } } for (int i = 0; i < currentProgress; i++) { UILabel *label = self.titles[i]; label.textColor = [self titleHighColor]; UIView *circleView = self.circles[i]; circleView.backgroundColor = [self circleHighColor]; if (i != 0) { UIView *lineView = self.lines[i - 1]; lineView.backgroundColor = [self circleHighColor]; } } } - (void)willMoveToSuperview:(UIView *)newSuperview { [self reloadData]; } @end
可以看到,我就是按照UITableView的设计思路来实现这样一个进度条的(其实还是有很多其他更好的方法实现的)。有一个dataSource专为询问所要的数据,一个delegate专处理各种事件(事实上,内部高度、间距、颜色等改变,应该是放在delegate里面的,具体可以看UITableViewDelegate的设计)。
如果,不需要这个进度条通用,加入一个进度条只有四个进度,那么我会这么做:
用一个xib文件来描述所需4个的UILabel、四个圆圈可以用UIView实现,三根线条也是UIView实现,然后默认颜色为灰色,然后拉线出来,再根据具体情况改变其高亮状态的颜色即可......但是这样做,扩展性及其不好,即使只是要多加入一个状态,就得重新布局xib文件
如此,我想到了,为何不设计一个通用的进度条?这样以后遇到进度条的View,我直接把文件拖过来就是了......于是,它出来了。
在dataSource协议里面,我只是需要外界给我具体的进度数目、每个进度对应的标题。而delegate里面,说复杂呢,其实完全没必要实现那么多方法,毕竟我都是设置为@optional,只是考虑到代码的通用性以及同时用起来的舒畅性(主要是太懒,后期用到,不想再来修改)才将当时考虑到得都写上的。
这样,得到dataSource的数据之后,在.m文件里面,我就只需要考虑要创建多少个UIView和UILabel的问题了,然后就是排版~~简直不要太容易
当然,项目后期有时候中间一些状态是不高亮也要可以跳过,也是就加了个数组,让数组内对应下标的View和Label颜色改变下~~
(此篇只是按照我自己对iOS开发的理解所写,如果有错误的地方,还请指明,谢谢~~)
本progressBar的git地址:https://github.com/wzpziyi1/ZYProgressViewMode/tree/master/ZYProgressViewTest