自己动手实现一个简单的 UITableView
UITableView 是 iOS 开发中常用的一个控件,可惜他默认不支持横向滚动(其实是支持的,给 tableView 和 cell 的 contentView 分别 setTransform 二分之 M_PI即可)
但是为了更方便的定制里面的 cell,还是来手动实现一个横向滚动的 UITableView 吧,这次我们要实现一个跑马灯效果的横向滚动的 tableView,所以暂时取名叫 BayCarouselView
先分享结论,实现的过程中发现一些比较好玩的问题,比如
- 当回收某个 view 的时候,用 removeFromSuperView,会造成页面卡糊糊的,重用某个 view 的时候,addSubview 方法也是卡糊糊的
- 所以我们用
hidden
属性来代替这两个方法,CPU 占用和内存占用确实有大幅下降 scrollViewDidScroll:
方法随着滑动的速度加快,调用频次是越来越低,所以,如果想在这个回调方法里面,根据contentOffset
来做一些判断,就不太容易了。即使在滑动结束再做一次运算抢救一下,好像还是有点问题,也有可能我代码写的有问题- 相似的,虽然我没实现 delete 和 insert 操作,即使要 delete 某个 cell,最好用 hidden,不要用 removeFromSuperView
- 具体的实现就是在 alloc init 的时候 addSubview 从此之后只用 hidden 属性来控制 cell 的显示与否
- 这样可能会造成一些问题,比如如果后面的 cell 的 size 很大,实际上 reusableViewPool 里面有很多 object 是再也没有出头之日了,这个问题的话,UITableView 现在也是这么实现的,如果想测试可以在 cell 里面写上 dealloc 方法看看什么时候调用
- UIScrollView 里面默认有两个 UIImageView 这是滚动条的 image,想要去除也很简单,加上
_scrollView.showsVerticalScrollIndicator = NO
和_scrollView.showsHorizontalScrollIndicator = NO
实现的过程不是那么重要,本身并不难,更主要的还是通过学习苹果自己的 Cocoa 框架,模仿他们规范的写法,比如 Delegate Protocol 方法名的规范,如果这个方法不需要参数,一般这样写:
- (NSInteger)numberOfRowInCarouselView:(BayCarouselView *)carouselView;
如果需要参数,一般会这样写:
- (void)carouselView:(BayCarouselView *)carouselView didEndDisplayView:(BayCarouselItemView *)view forRowAtIndex:(NSInteger)index;
- (void)carouselView:(BayCarouselView *)carouselView didSelectRowAtIndex:(NSInteger)index;
其次,我们还发现了一个小细节,在使用 tableView 的时候,相信大家对 dequeueReusableCellWithIdentifier
这个方法不陌生,这个方法的返回值类型,也是有一点细节在里面
- (__kindof BayCarouselItemView *)dequeueReusableView;
相信细心的同学已经发现,前面有一个 __kindof
修饰符,__kindof
是在 WWDC 2015 上提出来的,加上 __kindof
修饰符,就不再需要强制转换类型了,最典型的例子就是 UITableViewCell
,而且加上这个修饰符,可以在某些场景下消除一些不必要的警告
delegate 和 dataSource 使用 weak
__nullable
两个修饰符相信大家已经不陌生了
如果有人看过苹果自己的头文件,不难发现文件头尾分别有一个
NS_ASSUME_NONNULL_BEGIN
...
NS_ASSUME_NONNULL_END
关于 NS_ASSUME_NONNULL_BEGIN
和 NS_ASSUME_NONNULL_END
,这一对儿宏也是在 WWDC 2015 提出的,就不再需要开发者写一大堆 nonnull
了
To ease adoption of the new annotations, you can mark certain regions of your Objective-C header files as audited for nullability. Within these regions, any simple pointer type will be assumed to be nonnull. -- See Nullability and Objective-C - https://developer.apple.com/swift/blog/?id=25
大家在日常里面也没少用到 registerClass
dequeueReusableView
这些方法,我实现的方法比较简单粗暴,下面是代码
@property (nonatomic, strong) NSMutableSet *reusableViewPool;
@property (nonatomic, strong) Class registerClass;
- (void)registerClass:(Class)viewClass {
self.registerClass = viewClass;
}
- (BayCarouselItemView *)dequeueReusableView {
if (self.reusableViewPool.count > 0) {
BayCarouselItemView *view = [self.reusableViewPool anyObject];
view.transform = CGAffineTransformIdentity;
[self.reusableViewPool removeObject:view];
return view;
} else {
BayCarouselItemView *view = [[self.registerClass alloc] init];
[self.scrollView addSubview:view];
return view;
}
}
- (void)queueReusableView:(BayCarouselItemView *)view {
[self.reusableViewPool addObject:view];
}
最后是如何判断哪一个 cell 应该隐藏,哪一个应该显示,具体实现在 scrollViewDidScroll:
方法
我用的方法比较简单,如果有更好的算法,欢迎推荐给我,感谢
判断哪个 view 越界,并且将它 hidden 掉
for (BayCarouselItemView *view in scrollView.subviews) {
if ([view isKindOfClass:self.registerClass]) {
if (CGRectGetMaxX(view.frame) < minX - padding * 2 || CGRectGetMinX(view.frame) > maxX + padding * 2) {
if (!view.hidden) {
[self queueReusableView:view];
#ifdef DEBUG
//NSLog(@"remove %ld", view.itemIndex);
#endif
view.hidden = YES;
if ([self.delegate respondsToSelector:@selector(carouselView:didEndDisplayView:forRowAtIndex:)]) {
[self.delegate carouselView:self didEndDisplayView:view forRowAtIndex:view.itemIndex];
}
}
}
}
}
判断哪个地方有空缺,并且给它加上去
NSArray *subviewsArray = [self.scrollView.subviews sortedArrayUsingComparator:^NSComparisonResult(BayCarouselItemView *obj1, BayCarouselItemView *obj2) {
return [[NSNumber numberWithFloat:obj1.frame.origin.x] compare:[NSNumber numberWithFloat:obj2.frame.origin.x]];
}];
BayCarouselItemView *firstView;
for (NSInteger i = 0; i < subviewsArray.count; i++) {
if ([subviewsArray[i] isKindOfClass:self.registerClass]) {
if (!((BayCarouselItemView *)(subviewsArray[i])).hidden) {
firstView = subviewsArray[i];
break;
}
}
}
BayCarouselItemView *lastView;
for (NSInteger j = subviewsArray.count - 1; j >= 0; j--) {
if ([subviewsArray[j] isKindOfClass:self.registerClass]) {
if (!((BayCarouselItemView *)(subviewsArray[j])).hidden) {
lastView = subviewsArray[j];
break;
}
}
}
// 如果符合条件就生成右边的 view
if (lastView && CGRectGetMaxX(lastView.frame) < maxX + padding - self.cardInset) {
self.currentIndex = lastView.itemIndex;
if (lastView.itemIndex + 1 < self.numberOfRows) {
[self generateViewWithIndex:lastView.itemIndex + 1];
}
}
// 如果符合条件就生成左边的 view
if (firstView && CGRectGetMinX(firstView.frame) > minX - padding + self.cardInset) {
self.currentIndex = firstView.itemIndex;
if (firstView.itemIndex > 0) {
[self generateViewWithIndex:firstView.itemIndex - 1];
}
}
// 如果一个 subview 都没有,执行类似 reloadData 的操作
if (!firstView && !lastView) {
self.currentIndex = scrollView.contentOffset.x / self.rowWidth;
[self renderCurrentView];
}
接下来是大家最喜闻乐见的源代码了:点我可见