这篇文章源于对一个可横向滚动并选择的类目控件的重构。最近在学Swift
,所以把这个控件的核心逻辑(Cell重用)用Swift
重写出来分享给大家。当然这不是Apple官方的Cell重用机制,因为Apple不开源.这篇文章应该会对正在学习Swift
并且想了解Cell重用实现的同学有一定帮助。文章中涉及到的代码都放在了Github(ZXOptionBar-Swift ),欢迎大家提issues,求血求虐求羞辱~
第一个版本中这个控件承载了数据、视图、动画等所有的逻辑,使得这个控件在后期变得很难维护。所以我必须重构这个控件,而想到UITableView
也是一个可滚动可选择的控件,我为什么不把它做成一个类似UITableView
一样的横向滚动的控件呢。通过delegate
和dataSource
把数据和动画从控件中分离出来,通过cell重用减小cell初始化的开支。
可是,Apple的UIKit
不开源啊,怎么办呢?twitter为我们提供了很好的参考,那就是TWUI ,这是twitter当年在MacOS上实现的类似UIKit
的一个库,虽然已经2年没有更新了,但是里面还是有不少可以挖掘的好东西,比如Twitter版的UITabelView
的Cell重用机制。
UITableView
是分很多section
的,但是横向滚动的控件一般只会有一种Cell,不会像UITableView
一样需要分很多不同种类的Cell,所以我们可以改造TWUI 的Cell重用机制,使它更适合横向滚动的视图控件。
Note: 之前在团队内部做过一个这个控件的Cell重用的分享,用Keynote演示,不过是用Objective-C描述的,对于理解上会有帮助。Keynote文件也在Github上。
设计分析
需要通过delegate
设计模式将数据和视图分离
需要用继承自UIScrollView
的类来处理类似UITableView
的逻辑(ZXOptionBar
)
需要用继承自UIView
的类来处理类似UITableViewCell
的逻辑(ZXOptionBarCell
)
ZXOptionBar
中需要一个容器来存放可以重用的Cell
ZXOptionBarCell
中需要一个容器来存放当前显示的Cell
使用layoutSubviews()
方法来处理Cell重用逻辑
需要的工具方法:
计算可见区域Rect visibleRect()
计算可见的Cell的下标index indexsForVisibleColumns()
计算指定区域Rect下的Cell的下标index indexsForColumnInRect()
计算指定下标的Cell的Rect大小 rectForColumnAtIndex()
核心实现
通过delegate设计模式分离数据与视图
使用过UITableView
的同学肯定知道,我们在使用UITableView
的时候都是通过它的delegate和dataSource的代理方法将数据告诉我们的UITableView
。ZXOptionBar
也可以通过这样的方式将我们的数据和视图分离:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// MARK : - ZXOptionBarDataSource
protocol ZXOptionBarDataSource : NSObjectProtocol {
func numberOfColumnsInOptionBar ( optionBar : ZXOptionBar ) -> Int
func optionBar ( optionBar : ZXOptionBar , cellForColumnAtIndex index : Int ) -> ZXOptionBarCell
}
// MARK : - ZXOptionBarDelegate
@objc protocol ZXOptionBarDelegate : UIScrollViewDelegate {
// Display customization
optional func optionBar ( optionBar : ZXOptionBar , willDisplayCell cell : ZXOptionBarCell , forColumnAtIndex index : Int )
optional func optionBar ( optionBar : ZXOptionBar , didEndDisplayingCell cell : ZXOptionBarCell , forColumnAtIndex index : Int )
// Variable height support
optional func optionBar ( optionBar : ZXOptionBar , widthForColumnsAtIndex index : Int ) -> Float
// Select
optional func optionBar ( optionBar : ZXOptionBar , didSelectColumnAtIndex index : Int )
optional func optionBar ( optionBar : ZXOptionBar , didDeselectColumnAtIndex index : Int )
// Reload
optional func optionBarWillReloadData ( optionBar : ZXOptionBar )
optional func optionBarDidReloadData ( optionBar : ZXOptionBar )
}
然后在ZXOptionBar中声明两个变量:
1
2
3
// Mark : Var
weak var barDataSource : ZXOptionBarDataSource ?
weak var barDelegate : ZXOptionBarDelegate ?
并且在初始化方法中对其赋值
1
2
3
4
5
6
7
8
// MARK : Method
convenience init ( frame : CGRect , barDelegate : ZXOptionBarDelegate , barDataSource : ZXOptionBarDataSource ) {
self . init ( frame : frame )
self . delegate = delegate
self . barDataSource = barDataSource
self . barDelegate = barDelegate
. . . . .
}
心细的同学可能已经发现了,delegate和dataSource为什么在初始化方法里赋值啊,我记得 UITableView
不是这样子的,一定是我打开的方式不对!限于这已经超出本文的范畴,想深入了解的可以看看Matt大神的API Design ,可以看看文中的Rule 3: Required settings should be initializer parameters ,不过我建议大家花点时间全文阅读一遍,肯定受益匪浅!
Cell重用逻辑实现
定义容器类
通过上面的办法我们可以把数据从我们的控件中分离出去,接下去就是最激动人心的时候了,我们需要实现我们的Cell重用的逻辑。
首先,我们申明两个Dictionary的容器类,一个用来存放可重用的Cell,一个用来存放当前屏幕显示的Cell
1
2
3
// MARK : Private Var
private var reusableOptionCells : Dictionary < String , NSMutableArray >!
private var visibleItems : Dictionary < String , ZXOptionBarCell >!
当然别忘了在我们的convenience
init
方法中对其赋值,这里我就不一一贴代码了,大家可以在我的Github上的ZXOptionBar-Swift 库自行下载代码对应这边博客阅读,这里只贴一些核心的关键代码。
重写layoutSubviews()
layoutSubviews()
填塞所有Cell重用的逻辑,所以我们可以将一些独立模块的逻辑先抽离出来成一个个独立方法,读者可以先看看源码中以下几个方法先熟悉以下:
1
2
3
4
5
6
7
8
9
10
11
12
private func visibleRect () -> CGRect {
. . . 计算可见区域 Rect
}
private func indexsForVisibleColumns () -> Array < String > {
. . . 计算可见的 Cell 的下标 index
}
private func indexsForColumnInRect ( rect : CGRect ) -> Array < String > {
. . . 计算指定区域 Rect 下的 Cell 的下标 index
}
private func rectForColumnAtIndex ( index : Int ) -> CGRect {
. . . 计算指定下标的 Cell 的 Rect 大小
}
好了,咱先来分析分析我们的逻辑
1
2
3
4
左滑前可见 Cell 的 index : 0 1 2 3 4 5 6 7
左滑后可见 Cell 的 index : 2 3 4 5 6 7 8 9
需要删除的 Cell 的 index : 0 1
需要添加的 Cell 的 index : 8 9
这样我们就可以发现我们需要计算的信息了:
滑动前老的可见的Cell的index(oldVisibleIndex
);
滑动后新的可见的Cell的index(newVisibleIndex
);
需要删除的Cell的index(indexsToRemove
);
需要添加的Cell的index(indexsToAdd
).
1
2
3
4
5
6
7
8
let oldVisibleIndex : Array < String > = self . indexsForVisibleColumns ()
let newVisibleIndex : Array < String > = self . indexsForColumnInRect ( visible )
var indexsToRemove : NSMutableArray = NSMutableArray ( array : oldVisibleIndex )
indexsToRemove . removeObjectsInArray ( newVisibleIndex )
var indexsToAdd : NSMutableArray = NSMutableArray ( array : newVisibleIndex )
indexsToAdd . removeObjectsInArray ( oldVisibleIndex )
然后我们要做的就是将滚动出屏幕的Cell删除掉:
1
2
3
4
5
6
7
// delete the cells which frame out
for i in indexsToRemove {
let cell : ZXOptionBarCell = self . cellForColumnAtIndex ( self . indexFromIdentifyKey ( i as String )) !
self . enqueueReusableCell ( cell )
cell . removeFromSuperview ()
self . visibleItems . removeValueForKey ( i as String )
}
enqueueReusableCell()
方法就是将滚出屏幕的Cell放到我们的重用Cell容器中(reusableOptionCells
),具体实现可以看工程源码。
最后就是将进入屏幕的Cell添加进来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// add the new cell which frame in
for i in indexsToAdd {
let indexToAdd : Int = self . indexFromIdentifyKey ( i as String )
var cell : ZXOptionBarCell = self . barDataSource! . optionBar ( self , cellForColumnAtIndex : indexToAdd )
cell . frame = self . rectForColumnAtIndex ( indexToAdd )
cell . layer . zPosition = 0
cell . setNeedsDisplay ()
cell . prepareForDisplay ()
cell . index = indexToAdd
cell . selected = ( indexToAdd == self . selectedIndex )
if self . barDelegate! . respondsToSelector ( Selector ( "optionBar:willDisplayCell:forColumnAtIndex:" )) {
self . barDelegate! . optionBar! ( self , willDisplayCell : cell , forColumnAtIndex : indexToAdd )
}
self . addSubview ( cell )
self . visibleItems . updateValue ( cell , forKey : ( i as String ))
}
这里我们通过dataSource的 cellForColumnAtIndex
和delegate的willDisplayCell
来实现cell的构建和delegate的通知。
对外接口使用
这样我们就可以像使用UITableView一样使用我们的ZXOptionBar:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import UIKit
class ViewController : UIViewController , ZXOptionBarDelegate , ZXOptionBarDataSource {
internal var optionBar : ZXOptionBar ?
override func viewDidLoad () {
super . viewDidLoad ()
optionBar = ZXOptionBar ( frame : CGRectMake ( 0 , 100 , UIScreen . mainScreen () . bounds . size . width , 100 ), barDelegate : self , barDataSource : self )
self . view . addSubview ( optionBar! )
}
// MARK : - ZXOptionBarDataSource
func numberOfColumnsInOptionBar ( optionBar : ZXOptionBar ) -> Int {
return 20
}
func optionBar ( optionBar : ZXOptionBar , cellForColumnAtIndex index : Int ) -> ZXOptionBarCell {
var cell : CustomOptionBarCell ? = optionBar . dequeueReusableCellWithIdentifier ( "ZXOptionBarDemo" ) as? CustomOptionBarCell
if cell == nil {
cell = CustomOptionBarCell ( style : . ZXOptionBarCellStyleDefault , reuseIdentifier : "ZXOptionBarDemo" )
}
cell! . textLabel . text = "Bra-\(index)"
return cell!
}
// MARK : - ZXOptionBarDelegate
func optionBar ( optionBar : ZXOptionBar , widthForColumnsAtIndex index : Int ) -> Float {
return 60
}
func optionBar ( optionBar : ZXOptionBar , willDisplayCell cell : ZXOptionBarCell , forColumnAtIndex index : Int ) {
println ( cell )
println ( index )
}
}
cellForColumnAtIndex
其实和UITableView
的cellForColumnAtIndex
类似,都会调用一个叫dequeueReusableCellWithIdentifier
的接口,这个接口会问存放重用Cell的容器reusableOptionCells
有没有可以重用的Cell,有就给我,没有就返回nil,我让cellForColumnAtIndex
自己新建一个,这样我们整个重用链就构建出来了:
一开始重用Cell的容器reusableOptionCells
是空的,所以所有的Cell都是新建的。
当有cell滑出屏幕的时候这个cell被放入reusableOptionCells
中。
当有cell要滑入屏幕的时候就像reusableOptionCells
要可重用的Cell,有就重用,没有就再新建一个。
写在最后
Note1: 本篇文章只是抽离总结分享了一个OptionBar的核心功能(Cell重用和数据视图分离),读者可以根据自己应有的需要继承或者改写该类,添加新功能(比如:下标指示器indicator,indicator的样式,indicator的动画,类目选中动画之类的)。
Note2: 文章中只是罗列了核心代码,具体代码已放在Github上:ZXOptionBar-Swift ,感兴趣的同学可以下载下来。