iOS开发:一个瀑布流的设计与实现(已实现缓存池功能,该功能使得瀑布流cell可以循环利用)
一个瀑布流的实现有三种方式:
- 继承自UIScrollView,仿写UITableView的dataSource和delegate,创造一个缓存池用来实现循环利用cell
- 写多个UITableview(UITableView的cell宽度是与UITableView宽度一样的,那么每行可以摆设多个宽度相等的UITableView,从而实现瀑布流),不过这种方法是最差的,因为不能有效的做到循环利用cell
- 可以自定义UICollectionViewCell的布局,从而实现瀑布流,UICollectionView自带cell的循环利用功能
这里是使用UIScorllView实现的,就是第一种方案实现的,这种实现方式,自由度比之第三种方案更高。有时间我会写篇基于UICollectionView实现瀑布流的博客。
在此篇之前,最好是对UITableView里面dataSource和delegate有些深入的了解,如果有需要,可以参考这篇博客:http://www.cnblogs.com/ziyi--caolu/p/4769703.html
这里面提到了一些UITableView内部实现细节(当然是个人的猜想),然后仿照UITableView的delegate和dataSource写出了一个比较简单的例子,瀑布流可以算是前面那个例子的深入版。
从图中,可以得到基本的需求,也就是说,瀑布流每张图片的宽度是确定的,而高度是变化的(还有一种是高度是固定的,而宽度是变化的,实现原理一样)。
流布局结构,这说明要可以滚动。每张图片上,既要可以显示图片,又要可以显示文字,这仅仅是我写的这个medo的需求,在实际编程中,很有可能里面会装有各种控件。这种情况下,第一想法是UITableView和UITableViewCell,因为除了尺寸的问题外,UITableView的UITableViewCell是满足上述要求的,也就是一个cell里面可以装有各种控件,同时还可以循环利用cell。
那么,是否可以根据UITableView和UITableViewCell的设计,从而实现瀑布流呢?那么对应的,应该有ZYWaterFlowView和ZYWaterFlowViewCell吧?
首先,每张图片和文字会放在一个view里面,可以称呼它为waterFlowViewCell,如此先创建一个ZYWaterFlowViewCell类,里面有一个UIImageView和一个UILabel,这样cell部分好像可以满足基本需求了,至于要如何显示这个cell,应该是它父控件,也就是ZYWaterFlowView的事情。(先这样设计,后面有需要再修改)
再就是ZYWaterFlowView了,它要可以滚动,那么应当继承自UIScrollView,接下来就是获取cell如何排列的数据了。
这一部分,可以参考UITableView的设计,UITableVIew是通过让viewController遵守它的UITableViewDataSource和UITableViewDelegate协议,从而不断的向viewController要各种必须的数据,进而展示出cell的,而UITableView的重新排列是reLoaddata方法。(开篇提到的参考博客里详细说了这个过程,这里不再重述)
那么,ZYWaterFlowView完全可以参照这种设计,从而拿到自己必须的数据,处理响应的事件。
- ZYWaterFlowView肯定是要知道会有多少数目的ZYWaterFlowViewCell的,就如遵守UITableViewDataSource总要实现这两个方法:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
UITableView通过在.m文件里调用[self.dataSource numberOfSectionsInTableView:self]从而得知,有多少组?
通过调用[self.dataSource tableView:self numberOfRowsInSection:section]每组有多少行?(开篇提到的参考博客里详细说了这个过程,这里不再重述)
那么,我们可以对应的写出下面这个方法,创建一个ZYWaterFlowViewDataSource协议,在协议里面声明下面的这个必须要实现的方法,从而让ZYWaterFlowView可以通过同样的方式而得知cell的具体数目:
//返回cell的数目
- (NSInteger)numberOfCellsInWaterFlowView:(ZYWaterFlowView *)waterFlowView;
2. 我们总得拿到cell吧?不然怎么展示它?怎么实现循环利用功能?就如UITableViewDataSource里面的这个方法:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
UITableView是通过这样调用[self.dataSource tableView:self cellForRowAtIndexPath:indexPath],从而得到相对应位置的cell的,那么我的ZYWaterFlowView完全可以参照这种方式写出一个方法,从而得到我对应index的cell,如此,应该有下面这个方法:
//返回index位置对应的cell
- (ZYWaterFlowViewCell *)waterFlowView:(ZYWaterFlowView *)waterFlowView cellAtIndex:(NSUInteger)index;
3. 上面两个方法,在ZYWaterFlowViewDataSource协议是要用@required关键字修饰的,意为遵守协议就必须实现的方法,负责就会产生错误。那么总该有一些不那么重要的数据,给我们来自定义一些东西吧?这里我是声明了一个返回自定义列数(默认为三列)的方法:
@optional
//一共多少列
- (NSInteger)numberOfColumnsInWaterFlowView:(ZYWaterFlowView *)waterFlowView;
下面就是仿照UITableViewDelegate的一些设计了,用法和UITableViewDataSource是相同的,这里不再复述,只是需要注意的是,我们应该尽可能好的设计UIWaterFlowViewDelegate,从而让用这套框架的同事觉得很好用,不需要再修改。(我并未去设计很多方法,主要是没时间,事实上,我觉得完全可以将UITableViewDelegate里面的很多方法在这里写上)。
@optional
//返回index的cell的高度
- (CGFloat)waterFlowView:(ZYWaterFlowView *)waterFlowView heightAtIndex:(NSUInteger)index;
//各种间距
- (CGFloat)waterFlowView:(ZYWaterFlowView *)waterFlowView marginForType:(ZYWaterFlowViewMarginType)type;
//返回被选中的(孩子~~)cell
- (ZYWaterFlowViewCell *)waterFlowView:(ZYWaterFlowView *)waterFlowView didSelectedAtIndex:(NSUInteger)index;
仅有上面说的这些还不够,参考UITableView,他还有reloadData方法,可以猜想,这个方法是用来重新布局cell的(也就是从新计算cell尺寸的),如果需要创建缓存池,从而实现cell的循环利用,那么UITableView提供了这个方法:
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;
那么从仿照的角度上来说,ZYWaterFlowView也得实现这两个方法,从而处理相应的事情。到此,ZYWaterFlowView的.h文件,有了下面的设计:
#import <UIKit/UIKit.h> typedef enum {
ZYWaterFlowViewMarginTypeTop,
ZYWaterFlowViewMarginTypeLeft,
ZYWaterFlowViewMarginTypeBottom,
ZYWaterFlowViewMarginTypeRight,
ZYWaterFlowViewMarginTypeRow,
ZYWaterFlowViewMarginTypeColumn }ZYWaterFlowViewMarginType; @class ZYWaterFlowView, ZYWaterFlowViewCell; @protocol ZYWaterFlowViewDataSource <NSObject> @required
//返回cell的数目
- (NSInteger)numberOfCellsInWaterFlowView:(ZYWaterFlowView *)waterFlowView;
//返回index位置对应的cell
- (ZYWaterFlowViewCell *)waterFlowView:(ZYWaterFlowView *)waterFlowView cellAtIndex:(NSUInteger)index; @optional
//一共多少列
- (NSInteger)numberOfColumnsInWaterFlowView:(ZYWaterFlowView *)waterFlowView;
@end @protocol ZYWaterFlowViewDelegate <UIScrollViewDelegate> @optional
//返回index的cell的高度
- (CGFloat)waterFlowView:(ZYWaterFlowView *)waterFlowView heightAtIndex:(NSUInteger)index; //各种间距
- (CGFloat)waterFlowView:(ZYWaterFlowView *)waterFlowView marginForType:(ZYWaterFlowViewMarginType)type; //返回被选中的(孩子~~)cell
- (ZYWaterFlowViewCell *)waterFlowView:(ZYWaterFlowView *)waterFlowView didSelectedAtIndex:(NSUInteger)index; @end @interface ZYWaterFlowView : UIScrollView @property (nonatomic, weak) id<ZYWaterFlowViewDataSource> dataSource; @property (nonatomic, weak) id<ZYWaterFlowViewDelegate> delegate; //刷新数据
- (void)reloadData; //根据identifier去缓存池找到对应的cell
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;
@end
接下来就是.m文件里面的实现了。上面提到,我们应该在reloadData方法里面布局cell,那么瀑布流的cell应该如何布局呢?是按照从左到右、从上到下的顺序依次放入cell么?肯定不是的,前面提到,瀑布流的cell只是宽度相同,而高度不相同,如果是按照从左到右、从上到下的顺序依次放入cell的排列,最后非常有可能出现某一列高度很高,某一列高度很低等的情况。
基于这样的思考,我们该这样,如果要排列当前的一个cell,那么应该知道各列的最高高度,将当前的这个cell放入最高高度最小的那一列,然后更新那一列的最高高度,并保存。这里我用c语言中的数组保存,因为oc数组必须保存对象,操作也耗时些。
想想,在reloadData方法里面,我们必须拿到总的cell数目,还好,dataSource协议里面已经提供了方法,我们只需要调用那个方法,然后拿到相应的数据即可,然后是根据代理里面提供的各种间距、宽度、高度,根据“将当前的这个cell放入最高高度最小的那一列”布局好cell(也就是设置好所有的cell的frame),并更新最大高度,设置好UIScrollView的contentSize属性(也就是UIScrollView的滚动范围,应该是最高列+底部间距)。还有一个问题,就是该如何保存这些cell的frame呢?很简单,放到数组(NSMutableArray *cellFrames)里面即可,要用到的时候,根据相应的index(索引)取出来即可。
既然每个cell的frame已经计算好了,那么该拿到相应的cell(view),展示在屏幕上了。scrollView在滚动的时候,除了会调用它delegate里面的一些方法之外,还会调用
- (void)layoutSubviews
如此,我们在这里面拿到cell,并拿到对应下标的cellFrames里面的元素设置cell的frame。初看,并没有什么问题,但实际上,这样做的话,并未实现循环利用,不仅仅如此,还会重复创建cell的bug。
因此,在这个方法里面,应该先拿到对应下标的cell的frame,再判断这个cell是否正展示在屏幕上?可以与scrollView的contentOffSet属性判断,只要,cell的最大的y大于contentOffset.y,最小的y小于contentOffset.y + self(这里应该是屏幕)的高度,那么该cell就是要展示在屏幕上的。这里会有一个字典(NSMutableDictionary *displayingCells),会装有这一次滚动前展示在屏幕上得cell,如果只是小范围滚动的话,那么displayingCells里可能还有一些cell展示在屏幕上,而我们应该并那些没有展示在当前屏幕上得cell从scrollView(也就是ZYWaterFlowView,也就是self)里面移除掉,又因为displayingCells存放的是当前正展示在屏幕上的cell,那么相应的,应当将之从displayingCells里面移除,放到缓存池里面去(这里的缓存池,设计为NSMutableSet *reusableCells)。下面是主要代码:
//当scrollView滚动,除了会调用它的代理方法之外,还会时时调用这个方法
//所以,在这个方法里面拿到当前显示在屏幕的cell,设置尺寸~~
- (void)layoutSubviews
{
[super layoutSubviews]; int numberOfCells = (int)[self.dataSource numberOfCellsInWaterFlowView:self]; for (int i = 0; i < numberOfCells; i++) {
//先在scrollView上存在的cell里看当前cell是否存在scrollView(self)上
ZYWaterFlowViewCell *cell = self.displayingCells[@(i)];
CGRect cellF = [self.cellFrames[i] CGRectValue]; if ([self isInScreen:cellF]) { //判断当前cell是否有在屏幕展示(注意,这里与在scrollView(self)上展示不同)
if (cell == nil) { //需要在屏幕展示,所以cell不存在的时候需要创建
cell = [self.dataSource waterFlowView:self cellAtIndex:i];
cell.frame = cellF;
[self addSubview:cell];
self.displayingCells[@(i)] = cell; //添加到了scrollView上,所以将它加入字典中
}
}else
{
if (cell) { //没在屏幕上,所以不需要cell,把它放入缓存池
[cell removeFromSuperview];
[self.displayingCells removeObjectForKey:@(i)]; //这个字典是用来记录正展示在屏幕上得数组的,没有在
// 屏幕上了,应当移除相应的cell
//放入缓存池
[self.reusableCells addObject:cell]; //既然要循环利用,那么创建了,就不应该在
//ZYWaterFlowView生命周期还未结束之前被销毁
//那么将之放入缓存池
}
} }
}
接下来,应担是缓存池的实现主要代码,其实有了上面的铺垫,是很简单的,主要是根据identifier找到对应identifier的cell,如果没找到,就返回nil,下面试代码:
//需要根据标示符去缓存池找到对应的cell
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
__block ZYWaterFlowViewCell *cell;
[self.reusableCells enumerateObjectsUsingBlock:^(ZYWaterFlowViewCell *obj, BOOL *stop) {
if ([obj.identifier isEqualToString:identifier]) {
cell = obj;
*stop = YES;
}
}]; if (cell) { //被用了,就从缓存池中移除
[self.reusableCells removeObject:cell];
}
return cell;
}
相对应的,上面所说的ZYWaterFlowViewCell的设计不合理,事实上,参考UITableViewCell的设计,其实ZYWaterFlowViewCell只需要有一个identifier属性就好,如何你要用的cell有很复杂的控件,那么只需要继承ZYWaterFlowViewCell,给identifier标示,就可以了,用法和UITableViewCell一致,所以,我最终的ZYWaterFlowViewCell是这样设计的:
#import <UIKit/UIKit.h> @interface ZYWaterFlowViewCell : UIView
@property (nonatomic, copy) NSString *identifier; - (instancetype)initWithIdentifier:(NSString *)identifier;
@end #import "ZYWaterFlowViewCell.h" @implementation ZYWaterFlowViewCell
- (instancetype)initWithIdentifier:(NSString *)identifier
{
if (self = [super init]) {
self.identifier = identifier;
}
return self;
}
@end
写到了这,最主要的功能就剩下点击时间没处理了,比如说,我点击了某个cell,想要对它进行一定的处理,也就是ZYWaterFlowViewDelegate里面的这个方法碰触时:
//返回被选中的(孩子~~)cell
- (ZYWaterFlowViewCell *)waterFlowView:(ZYWaterFlowView *)waterFlowView didSelectedAtIndex:(NSUInteger)index;
如何实现这样一个功能呢?其实就是在touches系类方法里面,判断此次点击事件在哪个位置,然后哪个cell包含这个位置,调用上面的方法,返回响应索引即可,代码:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
//如果没有实现此代理,直接返回
if (![self.delegate respondsToSelector:@selector(waterFlowView:didSelectedAtIndex:)]) return; UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self]; __block NSNumber *index = nil;
//判断触摸点在哪个cell上,没必要遍历所有的cell,只需要遍历,当前展示在scrollView的cell(屏幕更好)
[self.displayingCells enumerateKeysAndObjectsUsingBlock:^(id key, ZYWaterFlowViewCell *obj, BOOL *stop) {
if (CGRectContainsPoint(obj.frame, currentPoint)) {
index = key;
*stop = YES;
}
}];
if (index) {
[self.delegate waterFlowView:self didSelectedAtIndex:index.unsignedIntegerValue];
}
}
下面是真个.m文件的代码,一些次要的东西,就没有详细说明了,我觉得,这玩意主要是思路,因为相对来说,技术是学不完的,最值钱的是思想。
#import "ZYWaterFlowView.h"
#import "ZYWaterFlowViewCell.h"
#define ZYWaterFlowViewDefaultNumberOfColumns 3
#define ZYWaterFlowViewDefaultCellH 65
#define ZYWaterFlowViewDefaultMargin 10 @interface ZYWaterFlowView ()
//存放所有cell的frame
@property (nonatomic, strong)NSMutableArray *cellFrames;
//存放在scrollView的cell,之所以用字典,因为可以用key来存取,方便添加和移除
//当,发现当前key值得cell在字典里面(也就是还在scrollView上,也许正在屏幕上,也许没有在屏幕上,但在scrollView上),直接从字典中取出即可,如果字典中不存在
//则去缓存池里取,如果缓存池没有,那么创建cell
@property (nonatomic, strong)NSMutableDictionary *displayingCells; //“缓存池”,存放离开屏幕的cell
@property (nonatomic, strong)NSMutableSet *reusableCells;
@end @implementation ZYWaterFlowView - (NSMutableArray *)cellFrames
{
if (_cellFrames == nil) {
_cellFrames = [NSMutableArray array];
}
return _cellFrames;
} - (NSMutableDictionary *)displayingCells
{
if (_displayingCells == nil) {
_displayingCells = [NSMutableDictionary dictionary];
}
return _displayingCells;
} - (NSMutableSet *)reusableCells
{
if (_reusableCells == nil) {
_reusableCells = [NSMutableSet set];
}
return _reusableCells;
} - (CGFloat)cellWidth
{
int numberOfColumns = (int)[self numberOfColumns];
CGFloat marginOfColumn = [self marginForType:ZYWaterFlowViewMarginTypeColumn];
CGFloat marginOfRight = [self marginForType:ZYWaterFlowViewMarginTypeRight];
CGFloat marginOfLeft = [self marginForType:ZYWaterFlowViewMarginTypeLeft];
CGFloat cellW = (self.frame.size.width - (numberOfColumns - 1) * marginOfColumn - marginOfLeft - marginOfRight) / numberOfColumns;
return cellW;
} //刷新数据
- (void)reloadData
{ [self.displayingCells.allValues makeObjectsPerformSelector:@selector(removeFromSuperview)];//先把当前展示在scrollView上得移除,再去清空其他容器
[self.displayingCells removeAllObjects];
[self.cellFrames removeAllObjects];
[self.reusableCells removeAllObjects];
int numberOfCells = (int)[self.dataSource numberOfCellsInWaterFlowView:self]; int numberOfColumns = (int)[self numberOfColumns]; CGFloat marginOfTop = [self marginForType:ZYWaterFlowViewMarginTypeTop];
CGFloat marginOfBottom = [self marginForType:ZYWaterFlowViewMarginTypeBottom];
CGFloat marginOfRow = [self marginForType:ZYWaterFlowViewMarginTypeRow];
CGFloat marginOfLeft = [self marginForType:ZYWaterFlowViewMarginTypeLeft];
CGFloat marginOfColumn = [self marginForType:ZYWaterFlowViewMarginTypeColumn];
//这里,使用一个c语言数组,来装载每一列的最大高度
//为什么不是oc数组?因为oc数组要装对象,还不能预先开好位置
CGFloat maxYOfColumns[numberOfColumns];
for (int i = 0; i < numberOfColumns; i++) {
maxYOfColumns[i] = 0.0;
} CGFloat cellW = [self cellWidth]; for (int i = 0; i < numberOfCells; i++) {
//最小的y所在列
int minYAtCol = 0;
//所有列中最小的y
CGFloat minY = maxYOfColumns[minYAtCol]; for (int j = 1; j < numberOfColumns; j++) {
if (minY > maxYOfColumns[j]) {
minY = maxYOfColumns[j];
minYAtCol = j;
}
}
CGFloat cellH = [self cellHeightAtIndex:i]; CGFloat cellX = marginOfLeft + minYAtCol * (cellW + marginOfColumn); CGFloat cellY = 0; if (minY == 0.0) {
cellY = marginOfTop;
}else
{
cellY = minY + marginOfRow;
} CGRect cellFrame = CGRectMake(cellX, cellY, cellW, cellH);
[self.cellFrames addObject:[NSValue valueWithCGRect:cellFrame]]; //更新这一行的y
maxYOfColumns[minYAtCol] = CGRectGetMaxY(cellFrame);
} //设置scrollView的滚动范围
CGFloat maxY = maxYOfColumns[0];
for (int i = 1; i < numberOfColumns; i++) {
if (maxY < maxYOfColumns[i]) {
maxY = maxYOfColumns[i];
}
}
maxY += marginOfBottom;
self.contentSize = CGSizeMake(0, maxY);
} //当scrollView滚动,除了会调用它的代理方法之外,还会时时调用这个方法
//所以,在这个方法里面拿到当前显示在屏幕的cell,设置尺寸~~
- (void)layoutSubviews
{
[super layoutSubviews]; int numberOfCells = (int)[self.dataSource numberOfCellsInWaterFlowView:self]; for (int i = 0; i < numberOfCells; i++) {
//先在scrollView上存在的cell里看当前cell是否存在scrollView(self)上
ZYWaterFlowViewCell *cell = self.displayingCells[@(i)];
CGRect cellF = [self.cellFrames[i] CGRectValue]; if ([self isInScreen:cellF]) { //判断当前cell是否有在屏幕展示(注意,这里与在scrollView(self)上展示不同)
if (cell == nil) { //需要在屏幕展示,所以cell不存在的时候需要创建
cell = [self.dataSource waterFlowView:self cellAtIndex:i];
cell.frame = cellF;
[self addSubview:cell];
self.displayingCells[@(i)] = cell; //添加到了scrollView上,所以将它加入字典中
}
}else
{
if (cell) { //没在屏幕上,所以不需要cell,把它放入缓存池
[cell removeFromSuperview];
[self.displayingCells removeObjectForKey:@(i)]; //这个字典是用来记录正展示在屏幕上得数组的,没有在
// 屏幕上了,应当移除相应的cell
//放入缓存池
[self.reusableCells addObject:cell]; //既然要循环利用,那么创建了,就不应该在
//ZYWaterFlowView生命周期还未结束之前被销毁
//那么将之放入缓存池
}
} }
} //需要根据标示符去缓存池找到对应的cell
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
__block ZYWaterFlowViewCell *cell;
[self.reusableCells enumerateObjectsUsingBlock:^(ZYWaterFlowViewCell *obj, BOOL *stop) {
if ([obj.identifier isEqualToString:identifier]) {
cell = obj;
*stop = YES;
}
}]; if (cell) { //被用了,就从缓存池中移除
[self.reusableCells removeObject:cell];
}
return cell;
} #pragma Private方法 //判断是否在屏幕上,只需要,最大的y大于contentOffset.y,最小的y小于contentOffset.y + self高度
- (BOOL)isInScreen:(CGRect)rect
{
return (CGRectGetMaxY(rect) > self.contentOffset.y) && (CGRectGetMinY(rect) < self.contentOffset.y + self.frame.size.height);
} - (CGFloat)cellHeightAtIndex:(int)index
{
if ([self.delegate respondsToSelector:@selector(waterFlowView:heightAtIndex:)]) {
return [self.delegate waterFlowView:self heightAtIndex:index];
} return ZYWaterFlowViewDefaultCellH;
} - (int)numberOfColumns
{
if ([self.dataSource respondsToSelector:@selector(numberOfColumnsInWaterFlowView:)]) {
return (int)[self.dataSource numberOfColumnsInWaterFlowView:self];
} return ZYWaterFlowViewDefaultNumberOfColumns;
} - (CGFloat)marginForType:(ZYWaterFlowViewMarginType)type
{
if ([self.delegate respondsToSelector:@selector(waterFlowView:marginForType:)]) {
return [self.delegate waterFlowView:self marginForType:type];
} return ZYWaterFlowViewDefaultMargin;
} - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
//如果没有实现此代理,直接返回
if (![self.delegate respondsToSelector:@selector(waterFlowView:didSelectedAtIndex:)]) return; UITouch *touch = [touches anyObject];
CGPoint currentPoint = [touch locationInView:self]; __block NSNumber *index = nil;
//判断触摸点在哪个cell上,没必要遍历所有的cell,只需要遍历,当前展示在scrollView的cell(屏幕更好)
[self.displayingCells enumerateKeysAndObjectsUsingBlock:^(id key, ZYWaterFlowViewCell *obj, BOOL *stop) {
if (CGRectContainsPoint(obj.frame, currentPoint)) {
index = key;
*stop = YES;
}
}];
if (index) {
[self.delegate waterFlowView:self didSelectedAtIndex:index.unsignedIntegerValue];
}
} //当view将要移到superView时,刷新数据,避免手动刷新
- (void)willMoveToSuperview:(UIView *)newSuperview
{
[self reloadData];
}
@end
这是这个项目的github地址,medo已经集成上拉刷新、下拉刷新:https://github.com/wzpziyi1/waterFlowView-demo
iOS开发:一个瀑布流的设计与实现(已实现缓存池功能,该功能使得瀑布流cell可以循环利用)的更多相关文章
- iOS开发一个制作Live Photo的工具
代码地址如下:http://www.demodashi.com/demo/13339.html 1.livePhoto简介 livePhoto是iOS 9.0 之后系统相机提供的拍摄动态照片的功能,但 ...
- iOS开发一个用户登录注册模块需要解决的坑
最近和另外一位同事负责公司登录和用户中心模块的开发工作,开发周期计划两周,减去和产品和接口的协调时间,再减去由于原型图和接口的问题,导致强迫症纠结症状高发,情绪不稳定耗费的时间,能在两周基本完成也算是 ...
- iOS开发 - 一个天真的搜索控制器的独白
文/Azen(简书作者)原文链接:http://www.jianshu.com/p/6d5327111511著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. 正文 一.关于横向模块开发 ...
- iOS开发-- 一个苹果证书如何多次使用
苹果的开发者账号限制开发者证书只能有5个,我们开发过程中遇到超过5个人需要真机调试的情况,如何解决这个问题呢? 有两种方式可以解决问题: 1. Revoke原来的证书----不推荐 将以前的证书“re ...
- ios开发--一个苹果证书怎么多次使用——导出p12文件
为什么要导出.p12文件 当我们用大于三个mac设备开发应用时,想要申请新的证书,如果在我们的证书里,包含了3个发布证书,2个开发证书,可以发现再也申请不了开发证书和发布证书了(一般在我们的证书界面中 ...
- [分享]IOS开发-简单实现搜索框显示历史记录的本地缓存及搜索历史每次只能获取到一个的解决方案
注:原文:http://www.zhimengzhe.com/IOSkaifa/40433.html 1.首先,我们需要对进行过搜索的textField的输入内容进行一个NSUserDefaults的 ...
- iOS开发:一个高仿美团的团购ipad客户端的设计和实现(功能:根据拼音进行检索并展示数据,离线缓存团购数据,浏览记录与收藏记录的批量删除等)
大致花了一个月时间,利用各种空闲时间,将这个客户端实现了,在这里主要是想记录下,设计的大体思路以及实现过程中遇到的坑...... 这个项目的github地址:https://github.com/wz ...
- 文顶顶iOS开发博客链接整理及部分项目源代码下载
文顶顶iOS开发博客链接整理及部分项目源代码下载 网上的iOS开发的教程很多,但是像cnblogs博主文顶顶的博客这样内容图文并茂,代码齐全,示例经典,原理也有阐述,覆盖面宽广,自成系统的系列教程 ...
- iOS开发UI篇—无限轮播(功能完善)
iOS开发UI篇—无限轮播(功能完善) 一.自动滚动 添加并设置一个定时器,每个2.0秒,就跳转到下一条. 获取当前正在展示的位置. [self addNSTimer]; } -(void)addNS ...
随机推荐
- SQLDumpSplitter sql文件分割工具
数据库误操作,只好使用使用原来的备份数据去恢复数据,但是数据量太大,只好使用SQLDumpSplitter将大文件分割成小文件,然后恢复指定的表即可.
- NFS安装及优化过程--centos6.6
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 3 ...
- 忘记mysqlroot密码/我的电脑 管理服务里面没有mysql启动项/mysql启动不了net start mysql
http://www.cnblogs.com/andy_tigger/archive/2012/04/12/2443652.html 通过绕过不需密码 http://www.jb51.net/arti ...
- Android APP通用型拒绝服务、漏洞分析报告
点评:记得曾经有段时间很多SRC平台被刷了大量APP本地拒绝服务漏洞(目前腾讯金刚审计系统已经可检测此类漏洞),移动安全团队发现了一个安卓客户端的通用型拒绝服务漏洞,来看看他们的详细分析吧. 0xr0 ...
- Python 文件 write() 方法
概述 Python 文件 write() 方法用于向文件中写入指定字符串. 在文件关闭前或缓冲区刷新前,字符串内容存储在缓冲区中,这时你在文件中是看不到写入的内容的. 语法 write() 方法语法如 ...
- 用 Fiddler 来弥补 Chrome Network 的小缺点
由于经常要查看后端的接口详情,但Chrome控制台的Network并不会全显api路径,而且每次需要先启动控制台,再进行请求才能记录到.大多数情况下都是要刷新页面,这会浪费很多时间. 还不如开一个 F ...
- 转 redis 锁
原文链接: http://www.promptness.cn/article/34 前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的 ...
- 最简单的一个java驱动jdbc链接mysql数据库
导入jar包:mysql.connector-java-5.0.8-bin.jar String driver = "com.mysql.jdbc.Driver"; String ...
- SpringBoot actuator 应用监控。
前言 : 今天在阅读 <SpringCloud微服务实战>一书时看到了SpringBoot actuator相关知识,并且自己也本地调试实践.觉得SpringBoot这一套监控还是挺有意思 ...
- php分享二十八:mysql运行中的问题排查
一:杀掉mysql连接的方法: kill thread_id: 杀掉当前进程,断开连接 kill query thread_id: 只杀掉某连接当前的SQL,而不断开连接. 批量杀死MySQL连接的 ...