PPPan's 平凡之路

做一个互联网内容的贡献者

PPPan's 平凡之路 一个技术博客覆盖范围包括 iOS Objecticve-C Swift Xcode 等


谈谈组件封装的思路和实现--PSCarouselView

前两天面试了一个应聘者,他的演示项目里有广告轮播功能。恰好之前我封装过一个实现了此功能的控件,于是就顺着他广告轮播的实现一直往下聊,从需求的抽象一直聊到各种实现的细节和需要考虑的问题等等。组件的封装是开发中比较有趣的一件事。今天我们就拿轮播控件举例,聊聊组件的封装。

授人予渔先要授人予鱼。先给出鱼(PSCarouselView),再慢慢讲渔。

##分析需求
一般来说,我们在封装组件的时候,会先思考以下几点

  • 这个组件要做什么
  • 这个组件至少需要知道哪些信息
  • 这个组件会反馈哪些信息

这个阶段,我们还不会考虑组件的具体实现,仅仅对其做抽象。我们要做的东西不仅仅是适用于单个项目的,而应该是通用的,可以适应大部分同种类需求的。

####Scene I
BA说,客户要在首页加个轮播。于是半天后设计师出了张图,看起来大概像这样:

我们按照上面的思路,拆分一下这个需求。

###这个组件要做什么
显而易见的:

1.展示多张图片

2.可以向左向右翻页

3.PageControl的状态会根据图片的滚动而相应改变

隐含可能要做的:

1.支持左右两侧无限循环滚动

2.自动轮播

3.支持手动滑动

4.支持点击并进行相关的响应

5.图片的缓存

###这个组件至少需要知道哪些信息
一个封装得优秀的复杂度不高组件就像一个魔法盒子,只需要触发启动开关,就可以达到你期待的效果。极简的触发参数和条件是组件封装的精髓。在内容型App中,轮播图一般会用作推荐内容展示区域。在O2O类App中,轮播图一般会用作广告位。因此轮播区域图片的内容绝大多数都是动态的。在一般的C/S开发中,客户端要获取存储在服务器上的图片会获取它的URL,然后在需要的时候根据URL异步地加载这些图片。因此,我们找出了的轮播空间第一个必备条件:

一个图片URL地址数组。

@property (nonatomic, strong) NSArray *imageURLs;/**< 必须赋值。只要给这个imageURL赋值,会自动获取图片*/

这个时候我们已经可以根据数组内的URL,数组内URL的数量,完成轮播效果了。但还不够完善。试想,当数组为空的时候,我们的轮播控件是一个什么状态?网络比较慢,图片还在加载中的时候,我们的轮播控件是什么状态?就目前来说—-空白一片。

每个App都有每个App的设计,但无论如何谁也不会容忍首页最醒目的部分出现一大片空白,因此占位图是必须的。我们告诉我们的控件,“没有图片的时候别傻愣着,显示这张图”。必备条件二:

占位图

@property (nonatomic, strong) UIImage *placeholder;/**< 没有轮播图时的占位图*/

有了以上信息我们的轮播控件已经可以在运行得比较好了。但是缺少了一定的定制性,能满足的需求面还不够广。比如说,让控件在需要自动轮播的地方自动轮播,在不需要自动轮播的地方保留无限滚动的特性,可以手动拨动?(之前有个产品提过这样的需求)比如说,这个控件是否应该提供可选的PageControl实现?依据设计师给的图不同,我们可能需要实现不同的PageControl,不管是颜色,布局,还是其可见隐藏都可能会有不同。这一部分复杂度不应该归到轮播控件中。但考虑到一些快速开发的需求,控件还是提供了一个默认选项。这些都是让控件变得更加可配置和灵活(同时也变得复杂)的可选信息。综上我们大概可以整理出以下属性:

@property (nonatomic,getter=isAutoMoving)      BOOL autoMoving;/**< 是否自动轮播,默认为NO*/
@property (nonatomic,getter=isShowPageControl) BOOL showPageControl;/**< 是否展示默认的PageControl,默认为YES*/

###这个组件会反馈哪些信息
上一点中,如果使用者需要自己实现PageControl,那么当前控件滚动到哪一页了,应该给出反馈。

不论在O2O的App中,还是资讯类App中,用户点击轮播必定会有相对应的响应,例如之乎日报点击跳转到对应的文章,淘宝点击查看大图等。因此用户点击的信息也必须反馈出来,以让控制器根据用户的操作进行不同的响应。

View层对Controller层的反馈一般有两种,一种是Target-Action,一种是Delegate。此处我们类似UITableView,给出一定的代理方法。遵循苹果的设计规范,让使用的开发者容易上手。

@protocol PSCarouselDelegate <NSObject>
@optional
/**
 *  告诉代理滚动到哪一页了
 *
 *  @param carousel self
 *  @param page     已经计算好,直接使用即可
 */
- (void)carousel:(PSCarouselView *)carousel didMoveToPage:(NSUInteger)page;

/**
 *   告诉代理用户点击了某一页
 *
 *  @param carousel
 *  @param index  imageURL的index  
 */
- (void)carousel:(PSCarouselView *)carousel didTouchPage:(NSUInteger)page;
@end

至此,轮播组件的架子已经明晰,剩下的就是如何实现,以及实现的时候需要考虑的一些细节。

##实现过程

###选择基类
苹果的UIKit提供了非常多优秀的类,可以作为我们轮播视图的基类。根据以上的分析,我们根据直觉,立马可以联想到以下三个类:
UIPageViewControllerUIScrollViewUICollectionView

UIPageViewController提供了非常好的翻页封装,仅需要指定翻页的方向,和子ViewController的数组,就可以提供类似轮播的功能。并且UIPageViewController提供了供翻页的接口和必要的反馈信息,定义在UIPageViewControllerDelegate协议中。用UIPageViewController,我们只需要将每个轮播页封装成一个仅包含一个UIImageView的ViewController,并将其设为UIPageViewController的ViewControllers,并实现它的代理方法就很容易实现我们的轮播图。

UIScrollView提供了最基本的滚动封装。采用UIScrollView作为基类,需要自己根据图片的数量计算ContentSize,并在ScrollView相对应的代理方法中,根据Frame相关信息来计算页码,会比UIPageViewController稍微复杂。但好在,它是一个View

UICollectionView做为UIScrollView的子类提供了比UIScrollView更好的封装,也提供了”翻页”的接口,并提供了一系列定位CollectionView的状态信息。相比UIScrollView,他提供了更深层的封装。同UIPageViewController一样,非常适合本作为本组件的基类。同时,当UICollectionView的一些代理方法不足以提供相关信息时,还可以通过UIScrollView的代理方法来解决。

采用UIPageViewController作为子类,考虑到使用者需要将其作为subViewController添加到它们的项目中,没有将一个View添加到ViewController中来得直观,综合考虑,PSCarouselView选择了UICollectionView作为其基类。

###实现功能
如何利用UICollectionView在有限的图片数量下实现无限轮播?只需在原有图片数组头尾各新增一张图片就可以实现。如图

将imageURL数组中的URL,扩充为N+2。同时,将imageURLs的最后一个URL作为新数组的第一个URL,将imageURLs的第一个URL作为新数组的最后一个URL。然后在我们的CollectionView滚动到最后一个Cell时,跳转到第二个URL表示的IndexPath即可。自动轮播采用计时器来完成。在实现的时候,我们需要注意一下的几点

1.计时器的开启与暂停

自动轮播,通常我们会使用到计时器NSTimer。当我们在页面切换的过程中,需要注意计时器的开启与暂停,不然可能会出现一些不可预料的BUG。PSCarouselView 作为一个 View 是不可能也不应该知道一个 ViewController 的生命周期的。因此我们需要提供两个接口,供使用者开启/暂停计时器。

1
2
- (void)startMoving;
- (void)stopMoving;

另外,我们还需要注意App的生命周期,当App进入不活动的状态时,我们也需要将计时器暂停,并在回到活动状态时相应地启动。

#pragma mark - Notification
//程序被暂停的时候,应该停止计时器
- (void)applicationWillResignActive
{
    [self stopMoving];
}

//程序从暂停状态回归的时候,重新启动计时器
- (void)applicationDidBecomeActive
{
    if (self.isAutoMoving)
    {
        [self startMoving];
    }
}

2.用户操作与自动轮播的冲突
用户操作时如果轮播还是继续滚动,会导致用户产生不可控的错觉。《iOS Human Interface Guide》中强调过App需要给于用户控制感。因此,在用户手动”翻页”的时候,我们需要暂时暂停一下自动轮播,并在用户手动”翻页”完成后,重新进行轮播。

//用户手动拖拽,暂停一下自动轮播
- (void)scrollViewWillBeginDragging:(UIScrollView     *)scrollView
{
    [self removeTimer];
}

//用户拖拽完成,恢复自动轮播(如果需要的话)
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    if (self.isAutoMoving)
    {
        [self addTimer];
    }
}

3.图片的缓存与展示
如何通过图片的URL来获得相对应的图片,并做好图片的缓存,以实现最大限度地节省流量?这个模块其实应该单独做一个有关图片缓存的库,而不应该包含到View层中来。本作采用了SDWebImage来实现这一功能。有兴趣的朋友可以尝试着实现一个图片缓存库。

以上,一个轮播组件的封装基本上完成了。更多实现上的细节请参看GitHub上的Demo

That’s all.Hope you enjoy it :)

Newer Post

[译] iOS 9 开发小技巧

###前言“小黄鸭”法不仅适用于debug,也适用于学习新知识。表达是最好的吸收。本文原文发表在realm.io上。我翻译并整理成此文。希望可以为国内的iOS朋友提供一些资料。 ###LayoutGuide 在iOS9.0 和 OS X10.11中,分别有两个新的类:UILayoutGuide 和 …

继续阅读
Older Post

漫谈iOS中的MVC

###前言 做过一段iOS开发的开发者们对MVC肯定不陌生。这是Apple推荐的开发iOS应用程序的标准设计模式。 我们从一张图开始,谈谈MVC。 ###MVC传统的MVC如上图。将不同的对象划分到三个阵营Model、View、Controller。View负责绘图、接收用户的交互,并将交互以Bli …

继续阅读