[置顶] iOS新闻客户端开发教程7-新闻列表

在上教程中,我们介绍了二级导航栏的开发,今天我们来讲解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;

    // 单元格之间的间隔,缺省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

2.拖拽UITableView控件
[置顶] iOS新闻客户端开发教程7-新闻列表_第1张图片

3.设置约束,自适应父视图
[置顶] iOS新闻客户端开发教程7-新闻列表_第2张图片

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,分别用来显示缩略图,标题和新闻摘要
[置顶] iOS新闻客户端开发教程7-新闻列表_第3张图片
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,如下图,即表示我们新闻列表页开发好了。
[置顶] iOS新闻客户端开发教程7-新闻列表_第4张图片
[置顶] iOS新闻客户端开发教程7-新闻列表_第5张图片

github源码:https://github.com/tangthis/NewsReader
个人技术分享微信公众号,欢迎关注一起交流
[置顶] iOS新闻客户端开发教程7-新闻列表_第6张图片

你可能感兴趣的:(ios开发,iOS教程,新闻App,iOS-App)