iOS 小demoiOS DeveloperiOS && Android

iOS项目-网易新闻项目笔记

2016-02-24  本文已影响2422人  si1ence

在项目之前,最好下载该App或者GitHub源码跑一下看一下效果,该项目旨在练习UI及网络数据的处理,推荐初学者边参考笔记边进行代码的编写

分析项目,确定 UI 框架

一. 新闻频道

1. 创建WPFChannel

    + (instancetype)channalWithDict:(NSDictionary *)dict {

        WPFChannal *channal = [[WPFChannal alloc] init];

        [channal setValuesForKeysWithDictionary:dict];

        return channal;
    }

    #warning 当只使用字典中部分键值对的时候,最好加上这个方法
    // kvc,防止找不到对应的key值而崩溃
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
        // 什么都不用写
    }

2. 创建 WPFChannalCell 类,继承UICollectionCell

    @property (nonatomic, strong) WPFChannal *channal;
    - (instancetype)initWithFrame:(CGRect)frame {
        if (self = [super initWithFrame:frame]) {

            // 初始化label
            self.lblName = [[UILabel alloc] init];

            // 设置文本框字号
            self.lblName.font = [UIFont systemFontOfSize:16];

            // 设置文本框文字居中
            self.lblName.textAlignment = NSTextAlignmentCenter;

            // 将label 添加到cell中
            [self.contentView addSubview:self.lblName];
        }
        return self;
    }
    - (void)setChannal:(WPFChannal *)channal {
        _channal = channal;
        // 进行名称的赋值
        self.lblName.text = channal.tname;

        // 如果当前cell 处于被选状态,放大字号(20),红色
        if (self.isSelected) {

            // 获取当前view 的父view
            UICollectionView *collectionView = (UICollectionView *)self.superview;

            self.lblName.font = [UIFont systemFontOfSize:20];
            self.lblName.textColor = [UIColor redColor];

        // 如果不是被选状态,正常字号(16),黑色
        } else {
            self.lblName.font = [UIFont systemFontOfSize:16];
            self.lblName.textColor = [UIColor blackColor];
        }

        // 在这句代码之后,lblName 才有frame
        [self.lblName sizeToFit];

        // 改变文本框中心点
        self.lblName.center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
    }

sizeToFit方法快速计算label 的长度(也可以通过字数及字号确定,但是略麻烦)

3. 创建 WPFChannalView 类,继承UICollectionView

    - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(nonnull UICollectionViewLayout *)layout {

        // 首先要执行父类的构造方法
        if (self = [super initWithFrame:frame collectionViewLayout:layout]) {

            // 设置导航栏背景颜色
            self.backgroundColor = [UIColor grayColor];

            // 设置数据源对象和代理对象
            self.dataSource = self;
            self.delegate = self;

            // 取消横向滚动条
            self.showsHorizontalScrollIndicator = NO;

            // 注册cell
            [self registerClass:[WPFChannalCell class] forCellWithReuseIdentifier:kIdentifier];

            // 异步+主队列:保证执行顺序,在加载完毕UI界面后再加载数据
            dispatch_async(dispatch_get_main_queue(), ^{
                [self loadServerDataWithUrlString:@"http://localhost/topic_news.json"]; // 自定义方法
            });
        }
        return self;
    }
    - (void)loadServerDataWithUrlString:(NSString *)urlString {

        // 利用第三方框架请求服务器数据
        [[AFHTTPSessionManager manager] GET:urlString parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
            // 不需要写东西
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

        #wanring 在这里需要打印一下,确定数据具体形式!!!!
    //        NSLog(@"responseObject-->%@", responseObject);
            /*
             打印结果--》是一个字典,里面包含一个名为tList的数组,数组内部是一个个字典,打印头部分如下:
             {
             tList = (
             {
             */

            // 接受获取的网络数据
            NSDictionary *channalDict = responseObject;
            NSArray *channalArray = channalDict[@"tList"];

            // 遍历数组
            [channalArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

                // 数组元素是字典类型
                NSDictionary *dict = obj;

                // 进行字典转模型
                WPFChannal *channal = [WPFChannal channalWithDict:dict];

                // 将模型对象添加到模型数组中
                // 注意该数组的懒加载
                [self.channals addObject:channal];
            }];

            // 刷新数据,先加载UICollectionViewDelegate,再加载viewDidLoad
            [self reloadData];

            // 必须有数据之后,选中第一个cell 才有意义
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];

            [self selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];

        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

            NSLog(@"channalView: error-->%@", error);
        }];
    }

注意先打印一遍数据,再根据数据类型的层级关系转化为具体对象

    // 一旦实现了下面的代理方法, layout.itemSize 就是失效.
    - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(nonnull NSIndexPath *)indexPath {

        // 1. 获取当前行的模型对象的文字
        WPFChannal *channal = self.channals[indexPath.row];
        NSString *name = channal.tname;

        // 2. 返回对应文字的label的尺寸
        return [self getLabelSizeWithTname:name];
    }

    // 获取对应文字的label的尺寸
    - (CGSize)getLabelSizeWithTname:(NSString *)name {

        UILabel *label = [[UILabel alloc] init];

        label.text = name;

        label.font = [UIFont systemFontOfSize:16];

        [label sizeToFit];

        return label.frame.size;
    }

二. 具体新闻信息

1. 创建 WPFMainData,每个新闻信息对象

    + (instancetype)mainDataWithDict:(NSDictionary *)dict {

        WPFMainData *data = [[WPFMainData alloc] init];

        [data setValuesForKeysWithDictionary:dict];

        return data;
    }

    // 有些变量名没有定义,防止崩溃
    - (void)setValue:(id)value forUndefinedKey:(NSString *)key {
        // 什么都不用写
    }

2. 创建 WPFMainTableViewCell

重写构造方法中实例化UI控件

重写绑定对象的set方法中进行数据的添加

layoutSubViews 中进行控件frame的布局

    @property (nonatomic, strong) WPFMainData *data;
    /** 图片框 */
    @property (nonatomic, strong) UIImageView *imgViewIcon;

    /** 标题label */
    @property (nonatomic, strong) UILabel *lblTitle;

    /** 副标题(摘要)label */
    @property (nonatomic, strong) UILabel *lblDigest;

    /** 跟帖数label */
    @property (nonatomic, strong) UILabel *lblReplyCount;
    - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {

    #warning 重写cell 的构造方法一定要用这个!!
        if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {

            // 实例化UI控件
            self.imgViewIcon = [[UIImageView alloc] init];
            self.imgViewIcon.backgroundColor = [UIColor orangeColor];

            self.lblDigest = [[UILabel alloc] init];
            self.lblDigest.font = [UIFont systemFontOfSize:13];
            self.lblDigest.numberOfLines = 2;

            self.lblReplyCount = [[UILabel alloc] init];
            self.lblReplyCount.backgroundColor = [UIColor lightGrayColor];
            self.lblReplyCount.font = [UIFont systemFontOfSize:12];

            self.lblTitle = [[UILabel alloc] init];
            self.lblTitle.font = [UIFont systemFontOfSize:16];

            // 将UI控件添加到当前cell 中
            [self.contentView addSubview:self.imgViewIcon];
            [self.contentView addSubview:self.lblTitle];
            [self.contentView addSubview:self.lblReplyCount];
            [self.contentView addSubview:self.lblDigest];

            // cell 分割线
            UIView *separateLine = [[UIView alloc] initWithFrame:CGRectMake(0, 79, self.frame.size.width, 1)];

            separateLine.backgroundColor = [UIColor blackColor];

            [self.contentView addSubview:separateLine];
        }
        return self;
    }
    - (void)layoutSubviews {
        // 一定要记得调用父类的该方法
        [super layoutSubviews];

        CGFloat imgX = 8;
        CGFloat imgY = 8;
        CGFloat imgW = 100;
        CGFloat imgH = 64;

        self.imgViewIcon.frame = CGRectMake(imgX, imgY, imgW, imgH);

        self.lblTitle.frame = CGRectMake(imgW + 2*imgX, imgY, self.frame.size.width - 3*imgX - imgW, 15);

        self.lblDigest.frame = CGRectMake(self.lblTitle.frame.origin.x, CGRectGetMaxY(self.lblTitle.frame) + 3, self.lblTitle.frame.size.width, 40);

        // sizeToFit快速得出label实际大小
        [self.lblReplyCount sizeToFit];
        // 即 label 右下角位置不变
        CGFloat replyX = self.frame.size.width - self.lblReplyCount.frame.size.width - imgX;
        CGFloat replyY = self.frame.size.height - self.lblReplyCount.frame.size.height - imgY;

        self.lblReplyCount.frame = CGRectMake(replyX, replyY, self.lblReplyCount.bounds.size.width, self.lblReplyCount.bounds.size.height);
    }
    - (void)setData:(WPFMainData *)data {
        _data = data;

        // 设置数据
        self.lblTitle.text = data.title;
        self.lblDigest.text = data.digest;
        self.lblReplyCount.text = [NSString stringWithFormat:@"回帖数:%@", data.replyCount];

        // 自动下载并显示图片
        [self.imgViewIcon sd_setImageWithURL:[NSURL URLWithString:data.imgsrc]];
    }

3. 创建 WPFMainTableView

    - (instancetype)initWithFrame:(CGRect)frame {
        if (self = [super initWithFrame:frame]) {

            // 设置数据源和代理对象
            self.delegate = self;
            self.dataSource = self;

            // 注册cell
            [self registerClass:[WPFMainTableViewCell class] forCellReuseIdentifier:kMainTableViewCell];

            // 隐藏cell 分割线
            self.separatorStyle = UITableViewCellSeparatorStyleNone;
        }
        return self;
    }
    - (void)setChannal:(WPFChannal *)channal {
        _channal = channal;

        // 初始值为10,表示滚动到第十条新闻的时候,开始加载第二十条信息
        self.index = 10;

        // 清空数据源
        [self.newsData removeAllObjects];

        // 刷新数据
        [self reloadData];

        // 根据tid 值获取当前页面的数据
        [self getMainDataWithTid:channal.tid];
    }
    - (void)getMainDataWithTid:(NSString *)tid {
        // 数据加载原则:
        // 1. 单词加载的数据量能够铺满一个屏幕
        // 2. 给用户预留一个屏幕的数据量作为滚动使用

        // 小菊花妈妈课堂开课了!
        [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

        // 1. 拼接网址字符串
        NSString *urlString = [NSString stringWithFormat:@"http://c.m.163.com/nc/article/headline/%@/0-20.html", tid];

        // 2. 发送请求
        [[AFHTTPSessionManager manager] GET:urlString parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
            //
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

    //        NSLog(@"responseObject-->%@", responseObject);
            /*
             打印结果:返回的整体是一个字典,下面是以tid值为名称的数组,数组内部是一个个字典,则根据其类型进行字典转模型

             tid-->T1370583240249
             responseObject-->{
             T1370583240249 = (
             {
             */

            // 1. 获取整体的字典
            NSDictionary *mainDict = responseObject;

            // 2. 获取字典下面的数组
            NSArray *mainArray = mainDict[tid];

            // 3. 遍历数组元素
            [mainArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

                // 3.0 转化为正确的类型
                NSDictionary *dict = obj;

                // 3.1 进行字典转模型
                WPFMainData *data = [WPFMainData mainDataWithDict:dict];

                // 3.2 将模型添加到模型数组中
                [self.newsData addObject:data];
            }];

            dispatch_async(dispatch_get_main_queue(), ^{

                // 小菊花隐藏
                [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

                [self reloadData];
            });

        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

            NSLog(@"mainTableView: error-->%@", error);
        }];
    }

3. 创建 WPFMainCollectionViewCell

    - (instancetype)initWithFrame:(CGRect)frame {
        if (self = [super initWithFrame:frame]) {

            WPFMainTableView *tableView = [[WPFMainTableView alloc] initWithFrame:self.bounds];

            self.tableView = tableView;
            [self.contentView addSubview:tableView];
        }
        return self;
    }

3. 创建 WPFMainCollectionView

    - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout {

        if (self = [super initWithFrame:frame collectionViewLayout:layout]) {

            // 注册cell
            [self registerClass:[WPFMainCollectionViewCell class] forCellWithReuseIdentifier:kMainCollectionViewCell];

            // 设置其代理对象和数据源对象
            self.delegate = self;
            self.dataSource = self;

            // 去向横向滚动条
            self.showsHorizontalScrollIndicator = NO;

            // 设置翻页效果
            self.pagingEnabled = YES;

            // 取消弹簧效果
            self.bounces = NO;
        }
        return self;
    }

三. 数据的传递

不同类之间进行信息的传递最好用通知

注意添加监听者的代码执行越早越好,一般都是重写类的创建方法的时候就添加了,还有不要忘记在dealloc 方法中移除监听者

1. 显示数据:将channelView 中的channels数组传递给mainCollectionView

    // 将加载到的新闻数据传递给主界面
    [[NSNotificationCenter defaultCenter] postNotificationName:@"NewsChannelDataLoadSuccess" object:self.channals];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(loadChannalDataWithNoti:) name:@"NewsChannelDataLoadSuccess" object:nil];
    - (void)loadChannalDataWithNoti:(NSNotification *)noti {

        self.channals = noti.object;

        [self reloadData];
    }

2. 实现点击新闻频道,就会切换到对应的新闻信息板块

点击上面的小collectionView,自动切换下面的大collectionView

    - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
        // 1. 获取被选中的cell
        WPFChannalCell *cell = (WPFChannalCell *)[collectionView cellForItemAtIndexPath:indexPath];

        // 2. 重新设置模型对象,在其set 方法会自动调整文字格式
        cell.channal = self.channals[indexPath.row];

        // 3. 发送通知,改变新闻控制器
        [[NSNotificationCenter defaultCenter] postNotificationName:@"MainCollectionViewChangeToIndexPath" object:indexPath];
    }
    - (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {

        // 获取被选中的cell
        WPFChannalCell *cell = (WPFChannalCell *)[collectionView cellForItemAtIndexPath:indexPath];

        // 重新设置模型对象,在其set 方法会自动调整文字格式
        cell.channal = self.channals[indexPath.row];
    }
    // 接受改变新闻频道的方法
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeToIndexPathWithNoti:) name:@"MainCollectionViewChangeToIndexPath" object:nil];
    - (void)changeToIndexPathWithNoti:(NSNotification *)noti {

        // animated: 表面上是是否以动画方式显现
        // YES: 滚动经过的所有界面都会被加载
        // NO: 只加载最后停留的界面
        // 一般为了节省客户的流量,都使用 NO
        [self scrollToItemAtIndexPath:noti.object atScrollPosition:UICollectionViewScrollPositionNone animated:NO];
    }

3. 拖动新闻信息板块,上面的新闻频道索引也会自动切换

滑动下面的大collectionView,自动切换上面的小collectionView

    // 减速结束的代理方法
    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {

        // 算出页数:注意这里需要手动计算,不能直接获取,因为当前页面大多情况下只被拖动没被选择
        // 道理同新闻频道的collectionView,拖动但是没有选择
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.contentOffset.x / self.frame.size.width inSection:0];

        [[NSNotificationCenter defaultCenter] postNotificationName:@"changeChannalToIndexPath" object:indexPath];
    }
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeToIndexWithNoti:) name:@"changeChannalToIndexPath" object:nil];
    - (void)changeToIndexWithNoti:(NSNotification *)noti {
        [self reloadData];

        // 获取当前页数并选择
        [self selectItemAtIndexPath:noti.object animated:YES scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
    }

补充,如果想联系编程数学,加深对collectionView理解的同仁,可以参考下面的代码

    // 中间部分
    if (self.center.x < collectionView.contentSize.width - kWidth/2  &&  self.center.x > kWidth / 2) {

        offsetX = self.center.x - kWidth / 2;

    // 前半屏幕
    } else if(self.center.x < kWidth/2) {

        offsetX = 0;

    // 最后半屏幕
    } else {
        offsetX = collectionView.contentSize.width - kWidth;
    }

    [UIView animateWithDuration:0.5 animations:^{
        collectionView.contentOffset = CGPointMake(offsetX, 0);
    }];

4. 加载更多信息

    @property (nonatomic, assign) NSInteger index;
    // 初始值为10,表示滚动到第十条新闻的时候,开始加载第二十条信息
    self.index = 10;
    // 如果当前加载的行 = 需要加载数据的行索引,就加载更多数据
    if (indexPath.row == self.index) {

        // 表示再往下拖十条数据,再次加载
        self.index += 10;

        [self loadMoreDataWithTid:self.channal.tid startIndex:self.index];
    }
    - (void)loadMoreDataWithTid:(NSString *)tid startIndex:(NSInteger)index {

        // 小菊花转起来
        [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;

        // 1. 拼接网址字符串,%ld-10表示从index 开始往后加载10条数据
        NSString *urlString = [NSString stringWithFormat:@"http://c.m.163.com/nc/article/headline/%@/%ld-10.html", tid, index];

        // 2. 发送请求
        [[AFHTTPSessionManager manager] GET:urlString parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
            //
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

            // 1. 获取整体的字典
            NSDictionary *mainDict = responseObject;

            // 2. 获取字典下面的数组
            NSArray *mainArray = mainDict[tid];

            // 3. 遍历数组元素
            [mainArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

                // 3.0 转化为正确的类型
                NSDictionary *dict = obj;

                // 3.1 进行字典转模型
                WPFMainData *data = [WPFMainData mainDataWithDict:dict];

                // 3.2 将模型添加到模型数组中
                [self.newsData addObject:data];
            }];

            dispatch_async(dispatch_get_main_queue(), ^{

                // 小菊花不转
                [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;

                [self reloadData];
            });
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

            NSLog(@"mainTableView: error-->%@", error);
        }];
    }
上一篇 下一篇

猜你喜欢

热点阅读