在上教程中,我们介绍了二级导航栏的开发,今天我们来讲解iOS开发中非常常用和重要的组件:“列表”,即UITableView。本节课程将会介绍横向滚动列表和竖向滚动列表,分别来实现二级栏目滑动切换和新闻内容列表的功能。
在OC中,UITableView是用来展示列表数据的控件,基本使用方法是:
1.首先,Controller需要实现两个delegate ,分别是UITableViewDelegate 和UITableViewDataSource
2.然后 UITableView对象的 delegate要设置为 self。
3.实现这些delegate的一些方法,重写。
1.新建横向滚动列表类LandscapeTableView
//LandscapeTableView.h
#import <UIKit/UIKit.h>
#import "LandscapeCell.h"
@protocol LandscapeTableViewDelegate;
@protocol LandscapeTableViewDataSource;
@interface LandscapeTableView : UIView <UIScrollViewDelegat\> {
// 存储页面的滚动条容器
UIScrollView *_scrollView;
// 单元格之间的间隔,缺省20
CGFloat _gapBetweenCells;
// 预先加载的单元格数,在可见单元格的两边预先加载不可见单元格的数目
NSInteger _cellsToPreload;
// 单元格总数
NSInteger _cellCount;
// 当前索引
NSInteger _currentCellIndex;
// 上次选择的单元格索引
NSInteger _lastCellIndex;
// 加载当前可见单元格左边的索引
NSInteger _firstLoadedCellIndex;
// 加载当前可见单元格右边的索引
NSInteger _lastLoadedCellIndex;
// 可重用单元格控件的集合
NSMutableSet *_recycledCells;
// 当前可见单元格集合
NSMutableSet *_visibleCells;
// 是否正在旋转
BOOL _isRotationing;
// 页面容器是否正在滑动
BOOL _scrollViewIsMoving;
// 回收站是否可用,是否将不用的页控件保存到_recycledCells集合中
BOOL _recyclingEnabled;
}
@property(nonatomic, assign) IBOutlet id<LandscapeTableViewDataSource> dataSource;
@property(nonatomic, assign) IBOutlet id<LandscapeTableViewDelegate> delegate;
@property(nonatomic, assign) CGFloat gapBetweenCells;
@property(nonatomic, assign) NSInteger cellsToPreload;
@property(nonatomic, assign) NSInteger cellCount;
@property(nonatomic, assign) NSInteger currentCellIndex;
// 重新加载数据
- (void)reloadData;
// 由索引获得单元格控件,如果该单元格还没有加载将返回nil
- (LandscapeCell *)cellForIndex:(NSUInteger)index;
// 返回可以重用的单元格控件,如果没有可重用的,返回nil
- (LandscapeCell *)dequeueReusableCell;
@end
@protocol LandscapeTableViewDataSource
@required
- (NSInteger)numberOfCellsInTableView:(LandscapeTableView *)tableView;
- (LandscapeCell *)cellInTableView:(LandscapeTableView *)tableView atIndex:(NSInteger)index;
@end
@protocol LandscapeTableViewDelegate
@optional
- (void)tableView:(LandscapeTableView *)tableView didChangeAtIndex:(NSInteger)index;
- (void)tableView:(LandscapeTableView *)tableView didSelectCellAtIndex:(NSInteger)index;
// a good place to start and stop background processing
- (void)tableViewWillBeginMoving:(LandscapeTableView *)tableView;
- (void)tableViewDidEndMoving:(LandscapeTableView *)tableView;
@end
#import "LandscapeTableView.h"
@interface LandscapeTableView (LandscapeTableViewPrivate) <UIScrollViewDelegate>
- (void)configureCells;
- (void)configureCell:(LandscapeCell *)cell forIndex:(NSInteger)index;
- (void)recycleCell:(LandscapeCell *)cell;
- (CGRect)frameForScrollView;
- (CGRect)frameForCellAtIndex:(NSUInteger)index;
- (void)willBeginMoving;
- (void)didEndMoving;
@end
@implementation LandscapeTableView
#pragma mark - Lifecycle methods
- (void)addContentView
{
_scrollView = [[UIScrollView alloc] initWithFrame:[self frameForScrollView]];
_scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_scrollView.pagingEnabled = YES;
_scrollView.backgroundColor = [UIColor whiteColor];
_scrollView.showsVerticalScrollIndicator = NO;
_scrollView.showsHorizontalScrollIndicator = NO;
_scrollView.bounces = YES;
_scrollView.delegate = self;
[self addSubview:_scrollView];
}
- (void)internalInit
{
_visibleCells = [[NSMutableSet alloc] init];
_recycledCells = [[NSMutableSet alloc] init];
_currentCellIndex = -1;
_lastCellIndex = 0;
_gapBetweenCells = 20.0f;
_cellsToPreload = 1;
_recyclingEnabled = YES;
_firstLoadedCellIndex = _lastLoadedCellIndex = -1;
self.clipsToBounds = YES;
[self addContentView];
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if ((self = [super initWithCoder:aDecoder])) {
[self internalInit];
}
return self;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self internalInit];
}
return self;
}
- (void)dealloc
{
self.delegate = nil;
self.dataSource = nil;
}
- (void)layoutSubviews
{
if (_isRotationing)
return;
CGRect oldFrame = _scrollView.frame;
CGRect newFrame = [self frameForScrollView];
if (!CGRectEqualToRect(oldFrame, newFrame)) {
// Strangely enough, if we do this assignment every time without the above
// check, bouncing will behave incorrectly.
_scrollView.frame = newFrame;
}
if (oldFrame.size.width != 0 && _scrollView.frame.size.width != oldFrame.size.width) {
// rotation is in progress, don't do any adjustments just yet
} else if (oldFrame.size.height != _scrollView.frame.size.height) {
// some other height change (the initial change from 0 to some specific size,
// or maybe an in-call status bar has appeared or disappeared)
[self configureCells];
}
}
#pragma mark - Propertites methods
- (void)setGapBetweenCells:(CGFloat)value
{
_gapBetweenCells = value;
[self setNeedsLayout];
}
- (void)setPagesToPreload:(NSInteger)value
{
_cellsToPreload = value;
[self configureCells];
}
- (void)setCurrentCellIndex:(NSInteger)newCellIndex
{
if (_scrollView.frame.size.width > 0 && fabs(_scrollView.frame.origin.x - (-_gapBetweenCells/2)) < 1e-6) {
_scrollView.contentOffset = CGPointMake(_scrollView.frame.size.width * newCellIndex, 0);
}
_currentCellIndex = newCellIndex;
_lastCellIndex = _currentCellIndex;
}
- (NSInteger)firstVisibleCellIndex
{
CGRect visibleBounds = _scrollView.bounds;
return MAX(floorf(CGRectGetMinX(visibleBounds) / CGRectGetWidth(visibleBounds)), 0);
}
- (NSInteger)lastVisibleCellIndex
{
CGRect visibleBounds = _scrollView.bounds;
return MIN(floorf((CGRectGetMaxX(visibleBounds)-1) / CGRectGetWidth(visibleBounds)), _cellCount - 1);
}
#pragma mark - Utility methods
- (void)reloadData
{
_cellCount = [_dataSource numberOfCellsInTableView:self];
// recycle all cells
for (LandscapeCell *cell in _visibleCells) {
[self recycleCell:cell];
}
[_visibleCells removeAllObjects];
[self configureCells];
}
- (LandscapeCell *)cellForIndex:(NSUInteger)index
{
for (LandscapeCell *cell in _visibleCells) {
if (cell.tag == index)
return cell;
}
return nil;
}
- (LandscapeCell *)dequeueReusableCell
{
LandscapeCell *result = [_recycledCells anyObject];
if (result) {
[_recycledCells removeObject:result];
}
return result;
}
#pragma mark - FZPageViewPrivate methods
- (void)configureCells
{
if (_scrollView.frame.size.width <= _gapBetweenCells + 1e-6)
return; // not our time yet
if (_cellCount == 0 && _currentCellIndex > 0)
return; // still not our time
// normally layoutSubviews won't even call us, but protect against any other calls too (e.g. if someones does reloadPages)
if (_isRotationing)
return;
// to avoid hiccups while scrolling, do not preload invisible pages temporarily
BOOL quickMode = (_scrollViewIsMoving && _cellsToPreload > 0);
CGSize contentSize = CGSizeMake(_scrollView.frame.size.width * _cellCount+2, _scrollView.frame.size.height);
if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
_scrollView.contentSize = contentSize;
_scrollView.contentOffset = CGPointMake(_scrollView.frame.size.width * _currentCellIndex, 0);
}
CGRect visibleBounds = _scrollView.bounds;
NSInteger newCellIndex = MIN(MAX(floorf(CGRectGetMidX(visibleBounds) / CGRectGetWidth(visibleBounds)), 0), _cellCount - 1);
newCellIndex = MAX(0, MIN(_cellCount, newCellIndex));
// calculate which pages are visible
NSInteger firstVisibleCell = self.firstVisibleCellIndex;
NSInteger lastVisibleCell = self.lastVisibleCellIndex;
NSInteger firstCell = MAX(0, MIN(firstVisibleCell, newCellIndex - _cellsToPreload));
NSInteger lastCell = MIN(_cellCount-1, MAX(lastVisibleCell, newCellIndex + _cellsToPreload));
// recycle no longer visible cells
NSMutableSet *cellsToRemove = [NSMutableSet set];
for (LandscapeCell *cell in _visibleCells) {
if (cell.tag < firstCell || cell.tag > lastCell) {
[self recycleCell:cell];
[cellsToRemove addObject:cell];
}
}
[_visibleCells minusSet:cellsToRemove];
// add missing cells
for (NSInteger index = firstCell; index <= lastCell; index++) {
if ([self cellForIndex:index] == nil) {
// only preload visible pages in quick mode
if (quickMode && (index < firstVisibleCell || index > lastVisibleCell))
continue;
LandscapeCell *cell = [_dataSource cellInTableView:self atIndex:index];
[self configureCell:cell forIndex:index];
[_scrollView addSubview:cell];
[_visibleCells addObject:cell];
}
}
// update loaded cells info
BOOL loadedCellsChanged = NO;
if (quickMode) {
// Delay the notification until we actually load all the promised pages.
// Also don't update _firstLoadedPageIndex and _lastLoadedPageIndex, so
// that the next time we are called with quickMode==NO, we know that a
// notification is still needed.
//loadedCellsChanged = NO;
} else {
loadedCellsChanged = (_firstLoadedCellIndex != firstCell || _lastLoadedCellIndex != lastCell);
if (loadedCellsChanged) {
_firstLoadedCellIndex = firstCell;
_lastLoadedCellIndex = lastCell;
}
}
// update current cell index
BOOL cellIndexChanged = (newCellIndex != _currentCellIndex);
if (cellIndexChanged) {
_lastCellIndex = _currentCellIndex;
_currentCellIndex = newCellIndex;
if ([(NSObject *)_delegate respondsToSelector:@selector(tableView:didChangeAtIndex:)])
[_delegate tableView:self didChangeAtIndex:_currentCellIndex];
}
}
- (void)configureCell:(LandscapeCell *)cell forIndex:(NSInteger)index
{
cell.tag = index;
cell.frame = [self frameForCellAtIndex:index];
[cell setNeedsDisplay];
}
// It's the caller's responsibility to remove this cell from _visiblePages,
// since this method is often called while traversing _visibleCells array.
- (void)recycleCell:(LandscapeCell *)cell
{
if ([cell respondsToSelector:@selector(prepareForReuse)]) {
[cell performSelector:@selector(prepareForReuse)];
}
if (_recyclingEnabled) {
[_recycledCells addObject:cell];
}
[cell removeFromSuperview];
}
- (CGRect)frameForScrollView
{
CGSize size = self.bounds.size;
return CGRectMake(-_gapBetweenCells/2, 0, size.width + _gapBetweenCells, size.height);
}
- (CGRect)frameForCellAtIndex:(NSUInteger)index
{
CGFloat cellWidthWithGap = _scrollView.frame.size.width;
CGSize cellSize = self.bounds.size;
return CGRectMake(cellWidthWithGap * index + _gapBetweenCells/2,
0, cellSize.width, cellSize.height);
}
- (void)willBeginMoving
{
if (!_scrollViewIsMoving) {
_scrollViewIsMoving = YES;
if ([(NSObject *)_delegate respondsToSelector:@selector(tableViewWillBeginMoving:)]) {
[_delegate tableViewWillBeginMoving:self];
}
}
}
- (void)didEndMoving
{
if (_scrollViewIsMoving) {
_scrollViewIsMoving = NO;
if (_cellsToPreload > 0) {
// we didn't preload invisible cells during scrolling, so now is the time
[self configureCells];
}
if ([(NSObject *)_delegate respondsToSelector:@selector(tableViewDidEndMoving:)]) {
[_delegate tableViewDidEndMoving:self];
}
if (_lastCellIndex != _currentCellIndex) {
LandscapeCell *cell = [self cellForIndex:_lastCellIndex];
cell.frame = cell.frame;
}
}
}
#pragma mark -
#pragma mark UIScrollViewDelegate methods
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (_scrollView == scrollView) {
if (_isRotationing)
return;
[self configureCells];
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
if (_scrollView == scrollView) {
[self willBeginMoving];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (!decelerate && _scrollView == scrollView) {
[self didEndMoving];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if (_scrollView == scrollView) {
[self didEndMoving];
}
}
@end
2.横向滚动单元格TableCell
新建NewsLandscapeCell
//NewsLandscapeCell.h
#import "LandscapeCell.h"
#import "NewsWidget.h"
@interface NewsLandscapeCell : LandscapeCell {
NewsWidget *_widget;
}
@end
//NewsLandscapeCell.m
#import "NewsLandscapeCell.h"
#import "ColumnInfo.h"
@implementation NewsLandscapeCell
- (void)setCellData:(ColumnInfo *)info
{
[super setCellData:info];
if (_widget == nil) {
_widget = [[NewsWidget alloc] init];
_widget.columnInfo = info;
_widget.owner = self.owner;
_widget.view.frame = self.bounds;
[self addSubview:_widget.view];
}
else {
_widget.columnInfo = info;
[_widget reloadData];
}
}
@end
3.实现二级栏目导航横向滚动切换
//NewsController.h
IBOutlet LandscapeTableView *_tableView;
//NewsController.m
#pragma mark - LandscapeViewDataSource & LandscapeViewDelegate methods
- (NSInteger)numberOfCellsInTableView:(LandscapeTableView *)tableView
{
return _barWidget.listData.count;
}
- (LandscapeCell *)cellInTableView:(LandscapeTableView *)tableView atIndex:(NSInteger)index
{
NewsLandscapeCell *cell = (NewsLandscapeCell *)[tableView dequeueReusableCell];
if (cell == nil) {
cell = [[NewsLandscapeCell alloc] initWithFrame:_tableView.bounds];
cell.owner = self;
}
ColumnInfo *info = [_barWidget.listData objectAtIndex:index];
[cell setCellData:info];
return cell;
}
- (void)tableView:(LandscapeTableView *)tableView didChangeAtIndex:(NSInteger)index
{
_barWidget.pageIndex = index;
}
1.新建新闻列表视图xib,NewsWidget
4.新建xib类,NewsWidget
//NewsWidget.h
#import "TableWidget.h"
#import "ColumnInfo.h"
@interface NewsWidget : TableWidget{
BOOL _hasNextPage;
NSInteger _pageIndex;
}
@property(nonatomic, strong) ColumnInfo *columnInfo;
@end
//NewsWidget.m
#import "NewsWidget.h"
#import "GetNews.h"
#import "BaseCell.h"
@implementation NewsWidget
- (void)viewDidLoad
{
self.cellIdentifier = @"NewsCell";
_cellHeight = 80;
_pageIndex = 0;
_hasNextPage = NO;
self.listData = [[NSMutableArray alloc] init];
[super viewDidLoad];
}
- (void)reloadData
{
// 停止网络请求
[_operation cancelOp];
_operation = nil;
_pageIndex = 0;
// 先清除上次内容
[self.listData removeAllObjects];
[super reloadData];
}
- (BOOL)isReloadLocalData
{
//NSArray *datas = [FxDBManager fetchNews:self.columnInfo.ID];
//[self.listData addObjectsFromArray:datas];
return [super isReloadLocalData];
}
- (void)requestServerOp
{
NSString *url = [NSString stringWithFormat:NewsURLFmt,
self.columnInfo.ID];
NSDictionary *dictInfo = @{@"url":url,
@"body":self.columnInfo.ID,
};
_operation = [[GetNews alloc] initWithDelegate:self opInfo:dictInfo];
[_operation executeOp];
}
- (void)requestNextPageServerOp
{
NSString *url = [NSString stringWithFormat:NewsURLFmt,
self.columnInfo.ID];
NSString *body = [NSString stringWithFormat:@"pageindex=%@",@(_pageIndex)];
NSDictionary *dictInfo = @{@"url":url,
@"body":body
};
_operation = [[GetNews alloc] initWithDelegate:self opInfo:dictInfo];
[_operation executeOp];
}
- (void)opSuccess:(NSArray *)data
{
_hasNextPage = YES;
_operation = nil;
if (_pageIndex == 0) {
[self.listData removeAllObjects];
}
_pageIndex++;
[self.listData addObjectsFromArray:data];
[self updateUI];
[self hideIndicator];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return indexPath.row < self.listData.count ? _cellHeight:44;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _hasNextPage?self.listData.count+1:self.listData.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellIdentifier = nil;
BaseInfo *info = nil;
if (indexPath.row < self.listData.count) {
cellIdentifier = self.cellIdentifier;
info = [self.listData objectAtIndex:indexPath.row];
}
else {
cellIdentifier = @"NewsMoreCell";
[self requestNextPageServerOp];
}
BaseCell *cell = (BaseCell*)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
NSArray* Objects = [[NSBundle mainBundle] loadNibNamed:cellIdentifier owner:tableView options:nil];
cell = [Objects objectAtIndex:0];
[cell initCell];
}
[cell setCellData:info];
return cell;
}
@end
5.设置NewsWidget.xib的File’s owner为NewsWidget
6.新建列表item的TableCell,NewsCell.xib
7.拖拽UIImageView和2个UILabel,分别用来显示缩略图,标题和新闻摘要
8.设置服务器新闻数据,为了方便,我们分别用news_序号来代表每个栏目返回的数据,例如,news_1.json代表第一个栏目数据,news_2.json为第二个栏目,以此类推。
//news_1.json
{
"result":"ok",
"data":[
{
"id":"AUL8RO0H00014JB6",
"name":"日众议院表决通过新安保法案",
"desc":"在反对声中通过新安保法案,将提交参议院审议。",
"iconurl":"http://img5.cache.netease.com/3g/2015/7/16/2015071609154377b2d.jpg",
"contenturl":"http://3g.163.com/news/15/0716/13/AUL8RO0H00014JB6.html"
},
{
"id":"AUKG45I500014AED",
"name":"深圳企业水源保护区建练车场",
"desc":"事发地毗邻深圳水库,工程未经批复公司入场抢建。",
"iconurl":"http://img4.cache.netease.com/3g/2015/7/16/201507160855254aa19.jpg",
"contenturl":"http://3g.163.com/news/15/0716/06/AUKG45I500014AED.html"
},
{
"id":"AUKRS19T0001124J",
"name":"曝公安部已确定恶意做空对象",
"desc":"上海个别贸易公司成调查的对象,数千家公司惶恐。",
"iconurl":"http://img6.cache.netease.com/3g/2015/7/16/201507160943494a1da.jpg",
"contenturl":"http://3g.163.com/news/15/0716/09/AUKRS19T0001124J.html"
},
{
"id":"3",
"name":"唐七《三生三世》涉嫌抄袭",
"desc":"唐七直言被黑,大风则感慨:有人叫抄袭,有人叫模仿",
"iconurl":"http://img5.cache.netease.com/3g/2015/7/7/20150707155751b64ea.jpg",
"contenturl":"http://3g.163.com/ent/15/0707/15/ATUBLOMC00031GVS.html"
},
{
"id":"4",
"name":"张晋带女儿上街 蔡少芬\"吃醋\"",
"desc":"张晋一手牵大女儿一手拖小女儿,蔡少芬吐槽:那我呢?",
"iconurl":"http://img5.cache.netease.com/3g/2015/7/7/20150707153619a6fc6.jpg",
"contenturl":"http://3g.163.com/ntes/15/0707/15/ATUBEV3400963VRR.html"
},
{
"id":"5",
"name":"唐七《三生三世》涉嫌抄袭",
"desc":"唐七直言被黑,大风则感慨:有人叫抄袭,有人叫模仿",
"iconurl":"http://img5.cache.netease.com/3g/2015/7/7/20150707155751b64ea.jpg",
"contenturl":"http://3g.163.com/ent/15/0707/15/ATUBLOMC00031GVS.html"
},
{
"id":"6",
"name":"张晋带女儿上街 蔡少芬\"吃醋\"",
"desc":"张晋一手牵大女儿一手拖小女儿,蔡少芬吐槽:那我呢?",
"iconurl":"http://img5.cache.netease.com/3g/2015/7/7/20150707153619a6fc6.jpg",
"contenturl":"http://3g.163.com/ntes/15/0707/15/ATUBEV3400963VRR.html"
},
{
"id":"7",
"name":"唐七《三生三世》涉嫌抄袭",
"desc":"唐七直言被黑,大风则感慨:有人叫抄袭,有人叫模仿",
"iconurl":"http://img5.cache.netease.com/3g/2015/7/7/20150707155751b64ea.jpg",
"contenturl":"http://3g.163.com/ent/15/0707/15/ATUBLOMC00031GVS.html"
}
]
}
9.运行程序,cmd+R,如下图,即表示我们新闻列表页开发好了。
github源码:https://github.com/tangthis/NewsReader
个人技术分享微信公众号,欢迎关注一起交流