在上教程中,我们介绍了二级导航栏的开发,今天我们来讲解iOS开发中非常常用和重要的组件:“列表”,即UITableView。本节课程将会介绍横向滚动列表和竖向滚动列表,分别来实现二级栏目滑动切换和新闻内容列表的功能。
-
-
-
- UITableView介绍
- 横向滚动列表-二级栏目滑动切换
- 新闻内容列表
-
-
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;// 单元格之间的间隔,缺省20CGFloat _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 cellsfor (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 yetif (_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 temporarilyBOOL 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 visibleNSInteger 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 cellsNSMutableSet *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 cellsfor (NSInteger index = firstCell; index <= lastCell; index++) {if ([self cellForIndex:index] == nil) {// only preload visible pages in quick modeif (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 infoBOOL 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 indexBOOL 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
2.拖拽UITableView控件
3.设置约束,自适应父视图
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
个人技术分享微信公众号,欢迎关注一起交流