2018-06-21 項目2:實現(xiàn)UICollectionView以及視頻播放器

一.項目需求

二.實現(xiàn)列表

本次列表展示參考博客為ios - 用UICollectionView實現(xiàn)瀑布流詳解
具體分為Cell震肮、Layout和Controller三個層面的實現(xiàn),實現(xiàn)邏輯如下:

1.Cell

在Cell層府怯,我們需要對其進(jìn)行布局(用代碼實現(xiàn))孽文,類似于Android里面設(shè)置weight一樣黑滴,只不過我通過手動設(shè)置比例來設(shè)置它們布局的相對大小赂乐。

- (instancetype) initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame: frame]) {
        //設(shè)置imageView的布局
        _imageView  = [[UIImageView alloc] init];
        [self.contentView addSubview:_imageView];
        [_imageView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.contentView).offset(0);
            make.size.mas_equalTo(CGSizeMake(self.contentView.bounds.size.width, self.contentView.bounds.size.height * 0.85));
        }];
        //標(biāo)記為需要重新布局
        [_imageView setNeedsLayout];
        
        //設(shè)置label的布局
        _titleLabel = [[UILabel alloc] init];
        [self.contentView addSubview:_titleLabel];
        _titleLabel.textAlignment = NSTextAlignmentCenter;
        _titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
        _titleLabel.font = [UIFont systemFontOfSize: 13];
        [_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.imageView.mas_bottom).offset(self.contentView.bounds.size.height * 0.04);
            make.bottom.equalTo(self.contentView.mas_bottom);
            make.size.mas_equalTo(CGSizeMake(self.contentView.bounds.size.width, self.contentView.bounds.size.height * 0.11));
        }];
    }
    return self;
}

有關(guān)setNeedsLayout的可以參考setNeedsLayout與layoutIfNeeded的區(qū)別

為了設(shè)置圓角,我們需要在layoutSublayersOfLayer中設(shè)置圓角涕烧,再用setNeedsLayout對UIImageView進(jìn)行布局更新

- (void) layoutSublayersOfLayer:(CALayer *)layer
{
    //設(shè)置圓角
    UIBezierPath *maskPath;
    maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds
                                     byRoundingCorners:(UIRectCornerTopLeft | UIRectCornerTopRight)
                                           cornerRadii:CGSizeMake(5.0f, 5.0f)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.CGPath;
    self.layer.mask = maskLayer;
}

2.Layout

在Layout層月而,我們則需要對Cell的放置、高度等屬性進(jìn)行相應(yīng)的設(shè)置议纯,由于原demo是可以實現(xiàn)瀑布流的父款,其思路是:每次都將cell插入到瀑布流中最短的那一列,然后實時更新每一列的高度瞻凤,直到cell放置結(jié)束憨攒。

但是我實現(xiàn)瀑布流之后發(fā)現(xiàn)效果慘不忍睹(因為給我的封面的高度和寬度參差不齊,不好看)阀参,因此套用了demo的模板對布局進(jìn)行展示:

首先肝集,通過Controller實現(xiàn)的委托,獲得Cell對應(yīng)的屬性蛛壳,將其放入屬性數(shù)組當(dāng)中:

//返回indexPath對應(yīng)cell的布局屬性
- (UICollectionViewLayoutAttributes *) layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    //創(chuàng)建布局屬性
    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
    
    //collectionView的寬度
    CGFloat collectionViewWidth = self.collectionView.frame.size.width;
    
    //設(shè)置布局屬性的frame
    CGFloat cellWidth = (collectionViewWidth - self.edgeInsets.left - self.edgeInsets.right - (self.columnCount - 1) * self.columnMargin) / self.columnCount;
    CGFloat cellHeight = cellWidth * 0.8;
    
    //找出最短那一列
    NSInteger destColumn = 0;
    CGFloat minColumnHeight = [self.columnHeights[0] doubleValue];
    
    for (int i = 1; i < self.columnCount; i++) {
        //取得第i列的高度
        CGFloat columnHeight = [self.columnHeights[i] doubleValue];
        
        if (minColumnHeight > columnHeight) {
            minColumnHeight = columnHeight;
            destColumn = I;
        }
    }
    
    //設(shè)置cell的坐標(biāo)
    CGFloat cellX = self.edgeInsets.left + destColumn * (cellWidth + self.columnMargin);
    CGFloat cellY = minColumnHeight;
    if (cellY != self.edgeInsets.top)
        cellY += self.rowMargin;
    
    attrs.frame = CGRectMake(cellX, cellY, cellWidth, cellHeight);
    
    //更新最短那一列的高度
    self.columnHeights[destColumn] = @(CGRectGetMaxY(attrs.frame));
    
    //記錄內(nèi)容的高度(即最長那一列的高度)
    CGFloat maxColumnHeight = [self.columnHeights[destColumn] doubleValue];
    if (self.contentHeight < maxColumnHeight)
        self.contentHeight = maxColumnHeight;
    
    return attrs;
}

獲得屬性數(shù)組之后包晰,在prepareLayout中,對所有的屬性(高寬炕吸、位置、頁邊距)對應(yīng)的數(shù)組進(jìn)行初始化

//初始化
- (void) prepareLayout
{
    [super prepareLayout];
    
    self.contentHeight = 0;
    
    //清除之前計算的所有高度
    [self.columnHeights removeAllObjects];
    
    // 設(shè)置每一列默認(rèn)的高度
    for (NSInteger i = 0; i < HobenDefaultColumnCount ; i ++) {
        [self.columnHeights addObject:@(HobenDefaultEdgeInsets.top)];
    }
    
    // 清除之前所有的布局屬性
    [self.attrsArr removeAllObjects];
    
    // 開始創(chuàng)建每一個cell對應(yīng)的布局屬性
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    
    for (int i = 0; i < count; i++) {
        // 創(chuàng)建位置
        NSIndexPath * indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        
        // 獲取indexPath位置上cell對應(yīng)的布局屬性
        UICollectionViewLayoutAttributes * attrs = [self layoutAttributesForItemAtIndexPath:indexPath];
        
        [self.attrsArr addObject:attrs];
    }
}

設(shè)置Cell的大忻愠铡:

//cell的大小
- (NSArray<UICollectionViewLayoutAttributes *> *) layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attrsArr;
}

注意cell和cell之間是有間距的赫模,需要通過計算,獲得Layout大姓裘:

//內(nèi)容的大小
- (CGSize) collectionViewContentSize
{
    return CGSizeMake(0, self.contentHeight + self.edgeInsets.bottom);
}

3.Controller

Controller是對每一個Cell的內(nèi)容屬性(封面圖片等)進(jìn)行設(shè)置瀑罗,并且獲得每一個Cell的布局屬性胸嘴,實現(xiàn)委托,傳遞給Layout:

首先斩祭,一定要記得劣像,有個ID需要注冊:

static NSString * const HobenCoverId = @"HobenCoverId";

/**
 * 創(chuàng)建布局和collectionView
 */
- (void)setupLayoutAndCollectionView
{
    
    // 創(chuàng)建布局
    HobenWaterFallLayout * waterFallLayout = [[HobenWaterFallLayout alloc] init];
    waterFallLayout.delegate = self;
    
    // 創(chuàng)建collectionView
    UICollectionView * collectionView = [[UICollectionView alloc] initWithFrame: self.view.bounds
                                                           collectionViewLayout: waterFallLayout];
    collectionView.backgroundColor = [UIColor whiteColor];
    
    //設(shè)置DataSource和Delegate
    collectionView.dataSource = self;
    collectionView.delegate = self;
    [self.view addSubview:collectionView];
    
    // 注冊
    [collectionView registerClass: [HobenCoverCell class]
       forCellWithReuseIdentifier: HobenCoverId];
    
    self.collectionView = collectionView;
}

解析json:

- (void) refreshCover
{
   _isEnd = NO;
   [self.collectionView.mj_footer resetNoMoreData];
   NSURL *url = [NSURL URLWithString: @"http://*******"];
   
   NSString *jsonString;
   jsonString = [NSString stringWithContentsOfURL: url
                                         encoding: NSUTF8StringEncoding
                                            error: nil];
   
   NSData* jsonData;
   jsonData = [jsonString dataUsingEncoding: NSUTF8StringEncoding];
   
   //獲得解析的json
   _dict = [NSJSONSerialization JSONObjectWithData: jsonData
                                           options: NSJSONReadingMutableContainers
                                             error: nil];
   
   //獲得需要的json數(shù)組
   _totalCovers = _dict[@"data"][@"info_list"];
}

在本次項目中,我實現(xiàn)了加載摧玫,因此需要對獲得的數(shù)據(jù)進(jìn)行分頁耳奕,在這里,我設(shè)置每頁有10個數(shù)據(jù):

/**
 * 初始化
 */
- (void)initialize
{
    [self refreshCover];
    //設(shè)置每頁大小和當(dāng)前頁數(shù)
    _sectionNum = 10;
    _currentPageNum = 0;
    self.title = @"視頻列表";
    self.view.backgroundColor = [UIColor whiteColor];
}

刷新控件相應(yīng)的邏輯如下:

/**
* 刷新控件
*/
- (void)setupRefresh
{
    self.collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewCovers)];
    self.collectionView.mj_header.backgroundColor = [UIColor whiteColor];
    [self.collectionView.mj_header beginRefreshing];
    
    self.collectionView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadMoreShops)];
    self.collectionView.mj_footer.backgroundColor = [UIColor whiteColor];
    self.collectionView.mj_footer.hidden = YES;
}

下拉刷新诬像,將會加載新的數(shù)據(jù)屋群,其實現(xiàn)邏輯如下:

/**
 * 加載新的視頻
 */
- (void) loadNewCovers
{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self refreshCover];
        
        //刷新,當(dāng)前頁數(shù)置0
        [_covers removeAllObjects];
        _currentPageNum = 0;
        int endValue = 0;
        
        //如果到底了
        if ((_currentPageNum + 1)* _sectionNum >= [_totalCovers count]) {
            endValue = (int)[_totalCovers count] - _currentPageNum * _sectionNum;
        }
        
        else {
            endValue = _sectionNum;
        }
        
        NSArray * cover = [_totalCovers subarrayWithRange: NSMakeRange(_currentPageNum * _sectionNum, endValue)];
        [_covers addObjectsFromArray:cover];
        
        // 刷新表格
        [self.collectionView reloadData];
        
        [self.collectionView.mj_header endRefreshing];
    });
}

上拉加載則需要判斷是否到底坏挠,如果到底了芍躏,則需要顯示no more data:

//加載更多視頻
- (void) loadMoreCovers
{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (_isEnd)
            return;
        
        //刷新
        _currentPageNum++;
        int endValue = 0;
        
        //如果到底了
        if ((_currentPageNum + 1)* _sectionNum >= [_totalCovers count]) {
            endValue = (int)[_totalCovers count] - _currentPageNum * _sectionNum;
            _isEnd = YES;
        }
        
        else {
            endValue = _sectionNum;
        }
        
        NSArray * cover = [_totalCovers subarrayWithRange: NSMakeRange(_currentPageNum * _sectionNum, endValue)];
        [_covers addObjectsFromArray:cover];
        
        // 刷新表格
        [self.collectionView reloadData];
        
        [self.collectionView.mj_footer endRefreshing];
        
        if (_isEnd)
            [self.collectionView.mj_footer endRefreshingWithNoMoreData];
    });
}

對每一個cell與數(shù)據(jù)一一對應(yīng),并設(shè)置進(jìn)cell里面:

- (UICollectionViewCell *) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    HobenCoverCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier: HobenCoverId
                                                                     forIndexPath: indexPath];
    
    NSDictionary *cover = [[NSDictionary alloc] init];
    cover = self.covers[indexPath.item];
    
    /** 圖片  */
    NSString *img = cover[@"cover"];
    
    /** 視頻標(biāo)題  */
    NSString *title = cover[@"title"];
    
    /** 視頻字段  */
    NSString *flv = cover[@"flv"];
    /** 視頻時長  */
    NSString *duration = cover[@"duration"];
    
    /** 列表到底  */
    NSString *end = cover[@"end"];
    
    /** 寬高  */
    NSNumber *height = cover[@"height"];
    NSNumber *width = cover[@"width"];
    
    HobenCover *cellCover = [[HobenCover alloc] init];
    
    //設(shè)置內(nèi)容
    [cellCover setImg: img];
    [cellCover setTitle: title];
    [cellCover setFlv: flv];
    [cellCover setDuration: duration];
    [cellCover setEnd: end];
    [cellCover setW: [width floatValue]];
    [cellCover setH: [height floatValue]];
    
    cell.cover = cellCover;
    return cell;
}

設(shè)置點擊跳轉(zhuǎn)事件:

- (void) collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    HobenCoverCell *cell = (HobenCoverCell *)[collectionView cellForItemAtIndexPath: indexPath];
    
    HobenCover *cover = cell.cover;
    
    
    HobenVideoController *videoController = [[HobenVideoController alloc] init];
    
    [videoController setCover: cover];
    
    //點擊跳轉(zhuǎn)
    [self.navigationController pushViewController: videoController
                                         animated: YES];
    
}

其他的就不用說了降狠,和之前學(xué)習(xí)的TableView差不多对竣,需要設(shè)置section數(shù)量和section里面的item的數(shù)量:

- (NSInteger) numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    return 1;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    self.collectionView.mj_footer.hidden = self.covers.count == 0;
    return self.covers.count;
}

最后實現(xiàn)委托,傳遞給Layout:

- (CGFloat) waterFallLayout:(HobenWaterFallLayout *)waterFallLayout heightForItemAtIndexPath:(NSUInteger)indexPath itemWidth:(CGFloat)itemWidth
{
    
    NSDictionary *cover = _covers[indexPath];
    NSNumber *height = cover[@"height"];
    NSNumber *width = cover[@"width"];
    return itemWidth / [width floatValue] * [height floatValue];
}

- (CGFloat) rowMarginInWaterFallLayout:(HobenWaterFallLayout *)waterFallLayout
{
    return 10;
}

- (NSUInteger) columnCountInWaterFallLayout:(HobenWaterFallLayout *)waterFallLayout
{
    return 2;
}

- (UIEdgeInsets) edgeInsetdInWaterFallLayout:(HobenWaterFallLayout *)waterFallLayout
{
    return UIEdgeInsetsMake(10, 10, 10, 10);
}

三.實現(xiàn)視頻播放

這次的視頻播放器使用的是基于AVPlayer封裝的ZFPlayer榜配,不得不說這個作者真的很強大:
具體參考他的GitHub博客文檔否纬,配置的話GitHub有說得很完整了。

當(dāng)控制器的view將要布局子控件時芥牌,就會調(diào)用viewWillLayoutSubviews烦味,因此我們首先對視頻的布局進(jìn)行配置:

- (void) viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    
    CGFloat w = CGRectGetWidth(self.view.frame);
    CGFloat h = w * 9 / 16;
    
    //視頻布局
    CGFloat x = 0;
    CGFloat y = (CGRectGetHeight(self.view.frame) - h) / 2;
    self.containerView.frame = CGRectMake(x, y, w, h);
    
    w = 44;
    h = w;
    
    //按鈕居中
    x = (CGRectGetWidth(self.containerView.frame) - w) / 2;
    y = (CGRectGetHeight(self.containerView.frame) - h) / 2;
    self.playBtn.frame = CGRectMake(x, y, w, h);
}

對于布局加載的各個函數(shù)的調(diào)用順序,可以看這篇文章
而在控制器的view布局子控件完成時壁拉,將調(diào)用viewDidLayoutSubviews谬俄,在下面進(jìn)行播放器的初始化:

- (void) initialize
{
    //將flv轉(zhuǎn)換成mp4
    NSMutableString *flv = [[NSMutableString alloc] initWithString: self.cover.flv];
    if (flv == nil)
        return;
    NSRange range = [flv rangeOfString: @".flv"];
    [flv replaceCharactersInRange: range withString: @".mp4"];
    _flvReadOnly = [[NSString alloc] initWithString: flv];
    
    //設(shè)置相應(yīng)布局
    self.view.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:self.containerView];
    [self.containerView addSubview:self.playBtn];
    
    ZFAVPlayerManager *playerManager = [[ZFAVPlayerManager alloc] init];
    /// 播放器相關(guān)
    self.player = [ZFPlayerController playerWithPlayerManager: playerManager
                                                containerView: self.containerView];
    self.player.controlView = self.controlView;

    //全屏之后,頂部欄隱藏
    @weakify(self)
    self.player.orientationWillChange = ^(ZFPlayerController * _Nonnull player, BOOL isFullScreen) {
        @strongify(self)
        [self setNeedsStatusBarAppearanceUpdate];
    };
    
    //結(jié)束播放之后停止
    self.player.playerDidToEnd = ^(id  _Nonnull asset) {
        @strongify(self)
        if (self.player.isFullScreen) {
            [self.player enterFullScreen:NO animated:YES];
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.player.orientationObserver.duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [self.player stop];
            });
        } else {
            [self.player stop];
        }
    };
    
}

在播放鍵點擊之后進(jìn)行播放:

- (void) playClick:(UIButton *)sender
{
    self.player.assetURL = [NSURL URLWithString: _flvReadOnly];
    [self.player playTheIndex: 0];
    [self.controlView showTitle: self.cover.title
                 coverURLString: self.cover.img
                 fullScreenMode: ZFFullScreenModeLandscape];
}

在加載視頻之前顯示封面圖弃理,注意要先將封面圖進(jìn)行比例壓縮:

- (UIImage*)imageCompressWithSimple:(UIImage*)image scaledToSize:(CGSize)size
{
    UIGraphicsBeginImageContext(size);
    [image drawInRect:CGRectMake(0,0,size.width,size.height)];
    UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}

獲得壓縮完畢的封面圖后溃论,我們可以用BackgroundColor的方法,讓視頻未加載前顯示封面圖:

- (UIView *) containerView
{
    if (!_containerView) {
        _containerView = [UIView new];
        
        NSURL *imageURL = [NSURL URLWithString: self.cover.img];
        NSData *data = [NSData dataWithContentsOfURL: imageURL];
        UIImage *image = [[UIImage alloc] initWithData: data];
        
        CGFloat w = CGRectGetWidth(self.view.frame);
        CGFloat h = w * 9 / 16;
        
        //設(shè)置封面圖的大小
        image = [self imageCompressWithSimple: image scaledToSize: CGSizeMake(w, h)];
        UIColor *bgColor = [UIColor colorWithPatternImage: image];

        [_containerView setBackgroundColor: bgColor];
    }
    return _containerView;
}

對播放按鈕進(jìn)行初始化:

- (UIButton *) playBtn
{
    if (!_playBtn) {
        _playBtn = [UIButton buttonWithType:UIButtonTypeCustom];
        [_playBtn setImage:[UIImage imageNamed:@"播放"] forState:UIControlStateNormal];
        [_playBtn addTarget:self action:@selector(playClick:) forControlEvents:UIControlEventTouchUpInside];
    }
    return _playBtn;
}

大功告成痘昌,這個ZFPlayer支持手勢滑動調(diào)節(jié)亮度钥勋、音量、進(jìn)度辆苔、重力感應(yīng)等功能算灸,可以說是非常強大了。

還有一點就是驻啤,使用視頻播放器的時候不要加斷點菲驴!否則會出現(xiàn)一些很奇怪的錯誤,我是看了這篇博客才知道的骑冗,真的太坑了赊瞬。

四.問題反饋與糾正

1.關(guān)于ViewDidLoad先煎、viewDidLayoutSubviews、layoutSubviews的調(diào)用問題

參考文章:UI篇-VC的生命周期以及UIView的layoutSubviews和drawRect方法
首先看看單個viewController的生命周期:

  1. loadView:加載view 會多次調(diào)用并且會使viewWillLayoutSubviews巧涧、viewDidLayoutSubviews不再執(zhí)行
  2. viewDidLoad:view加載完畢
  3. viewWillAppear:控制器的view將要顯示
  4. viewWillLayoutSubviews:控制器的view將要布局子控件
  5. viewDidLayoutSubviews:控制器的view布局子控件完成
    這期間系統(tǒng)可能會多次調(diào)用viewWillLayoutSubviews 薯蝎、 viewDidLayoutSubviews 倆個方法
  6. viewDidAppear:控制器的view完全顯示
  7. viewWillDisappear:控制器的view即將消失的時候
  8. viewDidDisappear:控制器的view完全消失的時候

整個控制器生命周期: viewDidLoad -> viewWillAppear -> viewWillLayoutSubviews -> viewDidLayoutSubviews -> viewDidAppear -> viewWillDisappear -> viewDidDisappear

viewWillLayoutSubviews 在 viewWillAppear 之后 viewDidAppear 之前執(zhí)行,這個方法會被調(diào)用多次谤绳,如果在此創(chuàng)建視圖占锯,可能會創(chuàng)建多個,而且這個方法中執(zhí)行耗時操作依然會造成跳轉(zhuǎn)卡頓的問題闷供。

viewDidLoad是當(dāng)程序第一次加載view時調(diào)用烟央,以后都不會用到,而viewDidAppear是每當(dāng)切換到view時就調(diào)用歪脏。

科普完以上知識之后疑俭,再看看我的代碼:

1) viewDidLoad和viewDidLayoutSubviews
//不正確的方法
- (void)viewDidLoad
{
    [super viewDidLoad];
}


- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    
    [self initialize];
}

可以看到,如果創(chuàng)建多個視圖的話婿失,就會不斷加載钞艇,可以說會非常消耗了!

2) viewDidLayoutSubviews和viewWillLayoutSubviews

再來看看我的布局代碼放哪了:

- (void) viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];
    
    CGFloat w = CGRectGetWidth(self.view.frame);
    CGFloat h = w * 9 / 16;
    
    //視頻布局
    CGFloat x = 0;
    CGFloat y = (CGRectGetHeight(self.view.frame) - h) / 2;
    self.containerView.frame = CGRectMake(x, y, w, h);
    
    w = 44;
    h = w;
    
    //按鈕居中
    x = (CGRectGetWidth(self.containerView.frame) - w) / 2;
    y = (CGRectGetHeight(self.containerView.frame) - h) / 2;
    self.playBtn.frame = CGRectMake(x, y, w, h);
}

是的豪硅,放在了控制器的view將要布局子控件里面哩照,還用到了view的frame!如果我view大小改變了的話,這個布局就會不準(zhǔn)確了!

2.關(guān)于使用frame進(jìn)行布局的問題

在Cell的initWithFrame里面拱礁,我的布局是這樣的:

[_imageView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.contentView).offset(0);
    make.size.mas_equalTo(CGSizeMake(self.contentView.bounds.size.width, self.contentView.bounds.size.height * 0.85));
}];

這樣做的問題在于蝴猪,我是直接獲得了contentView的大凶延(即寫死了),當(dāng)其布局大小發(fā)生改變的時候,用mas_equal的方法可不會隨之而改變。怎么辦冠王?改成這樣:

[_imageView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.contentView).offset(0);
    make.width.equalTo(self.contentView);
    make.height.equalTo(self.contentView).multipliedBy(0.85);
}];

但因為offset的問題(無法設(shè)置成weight形式),imageView和Label之間還是會有約束沖突舌镶,這時候我們可以定義一個空白的View來解決:

//添加空隙
UIView *space = [[UIView alloc] init];
[self.contentView addSubview: space];
[space mas_makeConstraints:^(MASConstraintMaker *make) {
    make.width.equalTo(self.contentView);
    make.height.equalTo(self.contentView).multipliedBy(0.04);
}];

這樣就可以按照0.85 : 0.04 : 0.11的比例來控制這個Cell布局了柱彻。

3.關(guān)于網(wǎng)絡(luò)請求與阻塞主線程的問題

在請求數(shù)據(jù)的時候,我曾經(jīng)是這樣請求的:

- (void) refreshCover
{
   _isEnd = NO;
   [self.collectionView.mj_footer resetNoMoreData];
   NSURL *url = [NSURL URLWithString: @"http://*******"];
   
   NSString *jsonString;
   jsonString = [NSString stringWithContentsOfURL: url
                                         encoding: NSUTF8StringEncoding
                                            error: nil];
   
   NSData* jsonData;
   jsonData = [jsonString dataUsingEncoding: NSUTF8StringEncoding];
   
   //獲得解析的json
   _dict = [NSJSONSerialization JSONObjectWithData: jsonData
                                           options: NSJSONReadingMutableContainers
                                             error: nil];
   
   //獲得需要的json數(shù)組
   _totalCovers = _dict[@"data"][@"info_list"];
}

這個有什么問題呢餐胀?問題在于哟楷,這個函數(shù)是寫在了主線程里面,如果加載很久的話否灾,就會造成阻塞UI的后果卖擅。幸運的是,AFNetWorking提供了異步請求方法,參考iOS9之后AFNetWorking的使用(詳細(xì))磨镶,就可以將代碼修改成這樣:

NSString *URLString = @"http://*******";

//使用AFNetWorking請求
AFHTTPSessionManager *session = [AFHTTPSessionManager manager];

//這一行是解決bug的
session.responseSerializer.acceptableContentTypes=[NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript",@"text/html", @"application/javascript", nil];

//get請求
[session GET: URLString
  parameters: nil
    progress: nil
     success: ^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"請求成功");
         
         //獲得字典
        _dict = responseObject;
         
        //獲得需要的json數(shù)組
        _totalCovers = _dict[@"data"][@"info_list"];
     }
     failure: ^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
         NSLog(@"請求失敗:%@", error);
     }];

看到那行解決bug沒有,那行好像是因為這個框架本身的問題導(dǎo)致報錯"Request failed: unacceptable content-type: application/javascript"健提,參考了解決方案琳猫。

同樣地,在cell里面私痹,設(shè)置加載中的圖片時脐嫂,我也直接暴力地請求了網(wǎng)絡(luò):

NSURL *url = [NSURL URLWithString: @"https://image.baidu.com/search/detail?ct=503316480&z=0&ipn=d&word=正在加載圖片gif&hs=2&pn=9&spn=0&di=122397146850&pi=0&rn=1&tn=baiduimagedetail&is=0%2C0&ie=utf-8&oe=utf-8&cl=2&lm=-1&cs=4034065388%2C2568359934&os=124701788%2C3310077975&simid=0%2C0&adpicid=0&lpn=0&ln=30&fr=ala&fm=&sme=&cg=&bdtype=0&oriquery=正在加載圖片gif&objurl=http%3A%2F%2Fimgsrc.baidu.com%2Fforum%2Fw%3D580%2Fsign%3D4fc40444dec451daf6f60ce386fd52a5%2Faef6d933c895d143f95a783970f082025aaf0749.jpg&fromurl=ippr_z2C%24qAzdH3FAzdH3Fptjkwv_z%26e3Bkwt17_z%26e3Bv54AzdH3FrAzdH3Fndb8c999ad%3Frt1%3Dc0al99dn8n0%26fjj_sz%3D8&gsm=0&islist=&querylist="];
NSData *data = [NSData dataWithContentsOfURL: url];
    
 UIImage *image = [UIImage sd_animatedGIFWithData:data];

事實上,只需要將這個Gif下載下來紊遵,放入Assets文件里面账千,再這樣加載就OK了:

UIImage *image = [UIImage imageNamed: @"loading"];

(解決失敗,不知道怎么加載gif類型的Placeholder暗膜,算了匀奏。。)

4.使用官方的UICollectionViewFlowLayout

鑒于自定義Layout太復(fù)雜学搜,在這里還是嘗試一下使用官方的UICollectionViewFlowLayout娃善,好像真的免去了委托等繁雜的工作(不過還是需要計算,因為UICollectionViewFlowLayout好像沒有提供列數(shù)這個接口)

//collectionView的寬度
CGFloat collectionViewWidth = self.view.frame.size.width;

//內(nèi)邊距
UIEdgeInsets edgeInsets = UIEdgeInsetsMake(10, 10, 10, 10);

//列數(shù)
int columnCount = 2;

//列間距
CGFloat columnMargin = 10;

//設(shè)置布局屬性的frame
CGFloat cellWidth = (collectionViewWidth - edgeInsets.left - edgeInsets.right - (columnCount - 1) * columnMargin) / columnCount;
CGFloat cellHeight = cellWidth * 0.8;

UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc]init];

//設(shè)置UICollectionViewFlowLayout的屬性
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
layout.sectionInset = edgeInsets;
layout.itemSize = CGSizeMake(cellWidth, cellHeight);

// 創(chuàng)建collectionView
UICollectionView * collectionView = [[UICollectionView alloc] initWithFrame: self.view.bounds
                                                       collectionViewLayout: layout];

5.分頁加載與超時功能的完善

項目給出的HTTP的地址里面有page=和size=瑞佩,這其實是用于分頁加載的(之前一直理解錯了= =)
所以我們要用占位符來得出加載出來的地址聚磺。(這里略)
同時,使用了AFNetworking進(jìn)行了異步加載炬丸,則需要處理好MJRefresh控件AFNetworking邏輯瘫寝,MJRefresh控件的加載完成必須是在AFNetworking請求完成并且讀取完畢之后才能隱藏:

//get請求
    [session GET: URLString
      parameters: nil
        progress: nil
         success: ^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            NSLog(@"請求成功");
             
             //獲得字典
            _dict = responseObject;
             
            //獲得需要的json數(shù)組
            _totalCovers = _dict[@"data"][@"info_list"];
             //獲得該數(shù)據(jù)請求是否結(jié)束
             if ([_dict[@"data"][@"end"] intValue] == 1)
                 _isEnd = YES;
             else
                 _isEnd = NO;
             if ([load isEqualToString: @"loadNewCovers"])  {
                 //加載成功,更新列表
                 [_covers addObjectsFromArray: _totalCovers];
                 [self.collectionView reloadData];
                 [self.collectionView.mj_header endRefreshing];
             }
             if ([load isEqualToString: @"loadMoreCovers"])  {
                 if (_isEnd) {
                     //加載到底稠炬,結(jié)束加載
                     [self.collectionView.mj_footer endRefreshingWithNoMoreData];
                     return;
                 }
                 else {
                     //加載成功焕阿,更新列表
                     [_covers addObjectsFromArray: _totalCovers];
                     [self.collectionView reloadData];
                     [self.collectionView.mj_footer endRefreshing];
                     
                 }
             }
         }

設(shè)置請求的超時時間:

[session.requestSerializer willChangeValueForKey: @"timeoutInterval"];
session.requestSerializer.timeoutInterval = 10.0f;
[session.requestSerializer didChangeValueForKey: @"timeoutInterval"];

在網(wǎng)絡(luò)請求失敗的同時,也需要拋出警告視圖酸纲。

 failure: ^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
             //加載失敗捣鲸,拋出異常
             NSLog(@"請求失敗:%@", error);
             
             UIAlertController *alert = [UIAlertController alertControllerWithTitle: @"加載超時"
                                                                            message: @"請檢查你的網(wǎng)絡(luò)"
                                                                     preferredStyle: UIAlertControllerStyleAlert];
             UIAlertAction *action = [UIAlertAction actionWithTitle: @"了解"
                                                              style: UIAlertActionStyleDefault
                                                            handler: nil];
             [alert addAction: action];
             [self presentViewController: alert
                                animated: YES
                              completion: nil];
             
             [self.collectionView.mj_header endRefreshing];
             [self.collectionView.mj_footer endRefreshing];
         }];
}

為防止下拉加載的時候多次誤觸控件,需要設(shè)置一個延時:

//加載更多視頻
- (void) loadMoreCovers
{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //刷新
        _currentPageNum++;
        
        //開始加載
        [self refreshCover: @"loadMoreCovers"];
    });
}

自此闽坡,分頁加載即可完成了栽惶!

6.模擬差網(wǎng)絡(luò)環(huán)境下的刷新

這里下載網(wǎng)絡(luò)環(huán)境模擬器Network Link Conditioner

一開始我的footer在差網(wǎng)絡(luò)環(huán)境下不斷上拉就會不斷刷新currentPageNum,導(dǎo)致UI操作和刷新操作不同步疾嗅,在這里需要添加一個邏輯:即在進(jìn)行UI上拉操作后外厂,footer應(yīng)該在UICollectionView加載完成之后,才能繼續(xù)上拉代承,由此汁蝶,我加上了這樣一個操作:dispatch_async(dispatch_get_main_queue())

 if ([load isEqualToString: @"loadNewCovers"])  {
     //加載成功,更新列表
     [_covers addObjectsFromArray: _totalCovers];
     [self.collectionView reloadData];
     dispatch_async(dispatch_get_main_queue(), ^{
         [self.collectionView.mj_header endRefreshing];
     });
 }
 if ([load isEqualToString: @"loadMoreCovers"])  {
     if (_isEnd) {
         //加載到底,結(jié)束加載
         [self.collectionView.mj_footer endRefreshingWithNoMoreData];
         return;
     }
     else {
         //加載成功掖棉,更新列表
         [_covers addObjectsFromArray: _totalCovers];
         [self.collectionView reloadData];
         
         dispatch_async(dispatch_get_main_queue(), ^{
             [self.collectionView.mj_footer endRefreshing];
         });
         
     }
 }

加上以后墓律,即可實現(xiàn)我想要的邏輯。同時記得下拉刷新后resetNoMoreData:

if ([load isEqualToString: @"loadNewCovers"])  {
    [_covers removeAllObjects];
    [self.collectionView.mj_footer resetNoMoreData];
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末幔亥,一起剝皮案震驚了整個濱河市耻讽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌帕棉,老刑警劉巖针肥,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異香伴,居然都是意外死亡慰枕,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門即纲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來具帮,“玉大人,你說我怎么就攤上這事崇裁∝芭鳎” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵拔稳,是天一觀的道長葛峻。 經(jīng)常有香客問我,道長巴比,這世上最難降的妖魔是什么术奖? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮轻绞,結(jié)果婚禮上采记,老公的妹妹穿的比我還像新娘。我一直安慰自己政勃,他們只是感情好唧龄,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奸远,像睡著了一般既棺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上懒叛,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天丸冕,我揣著相機與錄音,去河邊找鬼薛窥。 笑死胖烛,一個胖子當(dāng)著我的面吹牛眼姐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播佩番,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼众旗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了趟畏?” 一聲冷哼從身側(cè)響起逝钥,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拱镐,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體持际,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡沃琅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜘欲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片益眉。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖姥份,靈堂內(nèi)的尸體忽然破棺而出郭脂,到底是詐尸還是另有隱情,我是刑警寧澤澈歉,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布展鸡,位于F島的核電站,受9級特大地震影響埃难,放射性物質(zhì)發(fā)生泄漏莹弊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一涡尘、第九天 我趴在偏房一處隱蔽的房頂上張望忍弛。 院中可真熱鬧,春花似錦考抄、人聲如沸细疚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疯兼。三九已至,卻和暖如春挑势,著一層夾襖步出監(jiān)牢的瞬間镇防,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工潮饱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留来氧,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像啦扬,于是被迫代替她去往敵國和親中狂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 1扑毡、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_x閱讀 15,968評論 3 119
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,089評論 1 32
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,321評論 8 265
  • 你聽 雨 像不像 你來時 我的 心跳 你看 風(fēng) 像不像 你走近時 我的 微笑 風(fēng)雨都在 你什么時候 才能 如期歸來
    遠(yuǎn)川閱讀 340評論 0 0
  • 前文再續(xù)胃榕,書接上一回!在昨天的收盤點評《生蠔說:沉悶一天有玄機》中瞄摊,我們講到:“ 然后中午午評提前告訴大家這個...
    果園生蠔閱讀 1,163評論 1 3