「iOS」高仿【少數派】客戶端 代碼+思路講解

少數派
少數派

1、寫在前面

在個人iOS開發學習過程當中,閱讀過許多同窗的高仿項目文章、源碼,對我助益頗深。可是許許多多的高仿項目在技術方面各有側重,因此我先把本項目中值得探討的技術點列出,方便正好須要的同窗。ios

本項目重點探討:git

  • UITableview的性能優化
  • UIScrollView的進階使用
  • 少數派客戶端導航欄動態效果的實現
  • UITableview的多種控件嵌套
  • 手動封裝一些經常使用的視圖控件

2、簡述

首先來看一下項目的運行效果:
github

LYSSPai運行展現
LYSSPai運行展現

對於原客戶端的一些重複性細節沒有所有實現,歡迎你們fork。

這裏是 LYSSPai項目地址web

在本文中,我會先介紹項目的總體實現思路,而後對於開發過程當中遇到的值得探討的點進一步講述。json

項目中的數據來源爲使用Charles抓包獲取,用json文件存在bundle中。
項目中的素材來源爲官方客戶端ipa包使用iOS Images Extractors解析得到。
聲明:僅用於學習交流,嚴禁用於商業用途。緩存

3、總體實現思路

在這一節,我會按照頁面來介紹總體開發思路。性能優化

1. 首頁

首頁展現-1
首頁展現-1

首頁展現-2
首頁展現-2

1.1 頁面簡述

  • 這是項目的首頁,主要結構是頂部的導航欄和下面的內容。
  • 導航欄效果:
    在頁面向上滑動時,頂部導航欄的文字、按鈕尺寸會隨之動態減少,然後總體上移,懸停在頂部,模擬系統的導航欄效果。當頁面下滑時,效果相反。
  • 內容展現部分:
    首先有一個左右滑動的相似輪播圖部分。用以展現重點推薦的專題、文章、廣告等。
    接下來是一篇文章。
    而後又是一個手動滑動的相似輪播圖。用來展現付費的欄目。
    剩餘部分全爲文章。

1.2 實現思路

1.2.1 內容展現

使用UITableview,包含三種cell。
輪播圖爲橫向的UIScrollView,爲其中的每個子cell設置tag值,點擊事件以delegate的方式交由首頁VC實現。
文章展現cell爲普通的cell。右上角的菜單按鈕點擊事件以delegate的方式交由首頁VC實現。bash

1.2.2 導航欄實現

導航欄的動態效果須要隨着內容滑動而進行,然後懸停在頂部。其中涉及導航欄的高度變化以及懸停效果
咱們很容易想到使用UITableView的tableHeadersectionHeader,那麼先來明確一下這兩種視圖的特性:
tableHeader沒有頂部懸停效果,可是能夠方便地更改視圖的高度:網絡

CGRect newFrame = headerView.frame;
newFrame.size.height = newFrame.size.height + webView.frame.size.height;
headerView.frame = newFrame;

//beginUpdates和endUpdates方法用來以動畫形式更改高度
[self.tableView beginUpdates];

//要更改tableHeader,必須顯式調用set方法
[self.tableView setTableHeaderView:headerView];

[self.tableView endUpdates];複製代碼

而sectionHeader是默認帶有懸停效果的,可是我沒有找到能夠高效更新視圖高度的方法,因此這種方法果斷放棄。
對於tableHeader的懸停效果,能夠在頁面滑到臨界點時,將tableHeader加入到與tableview同一層級的view中,手動實現懸停效果,這也是許多UIScrollView的子View想要實現頁面懸停效果的方式。
可是有一點須要知道,UITableView是一個龐大的對象,對它頻繁更新勢必會影響性能。而動態更改tableHeader時,會不停地改變整個UITableView的佈局。爲了一個小小的動態效果實在沒必要如此。因此,我使用一個單獨的view做爲頂部的導航欄,而且將它和tableview加入到同一個容器scrollview中。這樣動態效果僅僅影響這個單獨的view佈局。app

1.2.3 分類專題頁

分類專題頁
分類專題頁

點擊首頁右上角的按鈕或者在內容cell中左劃,會進入分類專題頁面。這個頁面只是簡單模擬實現了一下。

1.2.4 文章閱讀頁面

文章閱讀頁
文章閱讀頁

點擊文章cell或者輪播部分的文章類型子cell,會進入對應的文章閱讀頁面。
這個頁面底部導航欄爲手動模擬實現。文章展現使用 WKWebView。在整個頁面包含web內容部分,都可以右劃返回。

關於使用WebView展現內容的探討,在個人文章從簡書iOS客戶端,來談談Hybrid方案細節設計進行了詳細探討,歡迎你們閱讀。

2.發現

發現頁展現
發現頁展現

這個頁面和首頁相似,而且比首頁簡單,略過不表。

3.消息

消息頁展現
消息頁展現

這個頁面沒有特別複雜的部分。不過本身封裝了選擇器View,效果和原客戶端徹底一致,須要的同窗能夠閱讀這部分代碼。其中涉及到UIScrollView的一些進階特性,一會會詳述。

4、重點詳述

1. tableview性能優化

  • 優化場景
    頁面開發完成後,cell嵌套scrollview,其中還包括多個子cell,若是不加優化的話,能夠預見使用體驗不會太好。在第一次滑動到第二個輪播圖時,很明顯感覺到頁面fps降低。然後滑動流暢,fps基本保持在60。因此咱們知道,優化重點在於輪播圖的首次加載、渲染。輪播圖首次出如今屏幕範圍中以後,被加入緩存,因此再次滑到這裏時便不會卡頓。
    說到性能優化,不得不推薦一下ibireme的文章,強烈建議沒看過的同窗認真閱讀一下iOS 保持界面流暢的技巧
  • 優化思路
    滑動頁面時fps在60左右時,用戶不會感受到卡頓,這是優化的目標。也就是說,咱們須要在1s/60 = 16.7ms內,完成每一幀的渲染。而視圖渲染須要CPU運算+GPU渲染運算共同完成。因此咱們須要分析在這個場景下,CPU與GPU各自的工做量,合理調配,從而使它們的每一幀運算耗時總和低於16.7ms。
  • cell重用
    cell重用是很是基礎但又很是重要的優化手段,正確使用tableview的cell重用機制。
  • cell高度緩存
    tableview的渲染過程當中,有多少個cell,就會調用多少次- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法,從而肯定contentSize。因此,儘可能將cell的高度提早計算而且進行緩存,避免在這個代理方法中進行計算,能夠有效優化tableview的渲染。
  • 佈局計算優化

    佈局的計算是CPU的工做,當頁面層級複雜時,佈局計算就會耗費較多時間。同時,應該明確的一點是使用Masonry自動佈局是將佈局計算量交給CPU去完成,勢必會相對增長耗時。因此,在複雜cell的優化中,通常建議手動計算佈局,會稍微提高一些性能。除此以外,若是頁面佈局計算量比較大的話,將佈局計算在頁面渲染以前完成而且緩存,會有效減小視圖渲染時的16.7ms中的CPU運算時間。
    在本項目中,我爲輪播圖cell封裝了一個frameModel,在頁面數據獲取完成後,提早計算輪播圖的佈局結果,在頁面渲染時,無需計算即可以直接賦值。

    //count爲輪播圖子cell數量
    +(instancetype)PaidNewsFrameModelWithCount:(NSInteger)count
    {
      PaidNewsFrameModel *model = [[self alloc] init];
    
      float cellWidth = LYScreenWidth * 0.55;
      float cellHeight = LYScreenWidth * 0.7;
      model.cellTitleFrame = CGRectMake(25, 10, 100, 18);
      model.moreFrame = CGRectMake(LYScreenWidth - 65, 11, 40, 16);
      model.backScrollViewFrame = CGRectMake(0, 43, LYScreenWidth, cellHeight);
      model.paidNewsViewFrames = [[NSMutableArray alloc] init];
      model.paidTitleFrames = [[NSMutableArray alloc] init];
      model.avatorFrames = [[NSMutableArray alloc] init];
      model.nicknameFrames = [[NSMutableArray alloc] init];
      model.updateInfoFrames = [[NSMutableArray alloc] init];
    
      for ( int i = 0; i < count; i++)
      {
          NSValue *paidNewsViewFrame = [NSValue valueWithCGRect:CGRectMake(25 + (cellWidth + 15) * i, 0, cellWidth, cellHeight)];
          [model.paidNewsViewFrames addObject:paidNewsViewFrame];
          NSValue *avatorFrame = [NSValue valueWithCGRect:CGRectMake(15, cellHeight - 90, 20, 20)];
          [model.avatorFrames addObject:avatorFrame];
          NSValue *nicknameFrame = [NSValue valueWithCGRect:CGRectMake(45, cellHeight - 85, cellWidth - 75, 12)];
          [model.nicknameFrames addObject:nicknameFrame];
          NSValue *updateInfoFrame = [NSValue valueWithCGRect:CGRectMake(15, cellHeight - 50, cellWidth - 30, 12)];
          [model.updateInfoFrames addObject:updateInfoFrame];
      }
      return model;
    }複製代碼

    能夠看到,帶有for循環而且每個循環體都稍有計算量,將這些計算工做提早而且在子線程執行是很是明智的。咱們要讓那16.7ms「用在刀刃上」。

  • 正確選擇視圖控件,爲視圖瘦身
    UIViewCALayer的關係你們應該都有所瞭解。UIView在CALayer的基礎上,封裝了交互操做相關的部分,UIView是比CALayer更重量的。若是當前控件不須要響應用戶操做,咱們應該儘量使用CALayer替代UIView。
    在本項目中,付費內容輪播圖部分,整個子cell須要響應用戶的點擊操做。因此只須要在子cell的最底層view添加手勢識別。而背景圖片、用戶頭像等元素是不須要響應特殊操做的,因此這些控件不使用UIImageView,改用CALayer。其實文字部分,也能夠不使用UILabel,這是能夠繼續優化的部分。
    這是頭像部分的佈局代碼:
    CALayer *avator = [[CALayer alloc] init];
    [paidNewsView.layer addSublayer:avator];
    NSValue *avatorFrame = self.model.paidNewsFrame.avatorFrames[i];
    avator.frame = avatorFrame.CGRectValue;
    [avator yy_setImageWithURL:[NSURL URLWithString:self.model.PaidNewsData[i][@"avatar"]] placeholder:nil options:kNilOptions progress:nil transform:^UIImage * _Nullable(UIImage * _Nonnull image, NSURL * _Nonnull url) {
      image = [image yy_imageByRoundCornerRadius:40.0];
      return image;
    } completion:nil];複製代碼
  • 網絡內容異步加載
    待頁面顯示出來以後,網絡內容再慢慢加載,也是爲了將時間用在刀刃上。
    異步加載網絡圖片的框架,有你們都熟知的SDWebImage,也有ibiremeYYWebImage。據介紹YYWebImage的性能是要比SD好一些的,這個我沒有親自驗證。
    這裏我使用了YYWebImage:
    [avator yy_setImageWithURL:[NSURL URLWithString:self.model.PaidNewsData[i][@"avatar"]] placeholder:nil options:kNilOptions progress:nil transform:^UIImage * _Nullable(UIImage * _Nonnull image, NSURL * _Nonnull url) {
      image = [image yy_imageByRoundCornerRadius:40.0];
      return image;
    } completion:nil];複製代碼
  • 圓角設置

    又是老生常談的圓角設置。使用CALayer的相關屬性來實現圓角效果會觸發離屏渲染,增長GPU的工做量。在這一點的優化上,可使用CPU將圖片素材直接裁剪爲圓角圖片再進行顯示。固然,最優的方案固然是讓大家的美工直接提供圓角素材~
    這裏我直接使用了YYImage的圓角處理。

2. UIScrollView的進階使用

這個部分我主要講的是消息頁面的選擇器控件封裝的思路。
先看效果:

selectView效果展現
selectView效果展現

一個很是簡單的控件。 可是有一個細節須要注意:使用輕劃手勢左右滑動時,頁面必然進行滾動。而使用拖拽時,則會判斷拖拽範圍來決定是否進行滾動。
這個效果我使用了 UIScrollView的代理方法 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate來實現。
這裏是代碼:

//中止拖拽時的代理
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
//    若是是內容頁的橫向滑動
    if (scrollView == self.contentView)
    {
        NSLog(@"slowing?? %@",decelerate ? @"YES" : @"NO");
        CGFloat scrollX = scrollView.contentOffset.x;
//        若是帶有慣性(快速滑動),則內容頁必然進行對應的移動
        if (decelerate)
        {
            if (self.selectedTag == 0 && scrollView.contentOffset.x > 0)
            {
                self.selectedTag = 1;
            }
            else if (self.selectedTag == 1 && scrollView.contentOffset.x < LYScreenWidth)
            {
                self.selectedTag = 0;
            }
        }
//        若是無慣性(慢速拖拽),此時須要知足拖動的範圍纔會進行移動
        else
        {
            if (self.selectedTag == 0 && scrollX >= 0.5 * LYScreenWidth)
            {
                self.selectedTag = 1;
            }
            else if (self.selectedTag == 1 && scrollX <= 0.5 * LYScreenWidth){
                self.selectedTag = 0;
            }
        }
        [self contentViewScrollAnimation];
    }
}複製代碼

當輕劃頁面時,scrollview是有慣性的,而拖拽時是沒有慣性的,利用這個特性來進行相應的判斷。
這裏是小橫條移動的動畫:

//內容頁進行移動的封裝
- (void)contentViewScrollAnimation
{
    //根據此時選中的按鈕計算出contentView的偏移量
    CGFloat offsetX = self.selectedTag * LYScreenWidth;
    CGPoint scrPoint = self.contentView.contentOffset;
    scrPoint.x = offsetX;
    //默認滾動速度有點慢 加速了下
    [UIView animateWithDuration:0.3 animations:^{
        [self.contentView setContentOffset:scrPoint];
    }];
//    通知選擇器,進行小橫條的移動
    [self.selectView selectBtnChangedTo:self.selectedTag];
}複製代碼

3. 導航欄動態效果的實現

先從新看一下效果:

導航欄效果展現
導航欄效果展現

這裏使用scrollview的代理方法 - (void)scrollViewDidScroll:(UIScrollView *)scrollView來實現。
這是代碼的部分:

//    scrollview剛剛開始滑動,此時導航標題大小和按鈕大小進行變化
    if (Y <= -97 && Y > -130)
    {
        //            以字號爲36和20計算得出的臨界Y值爲-97和-130,根據此刻Y值計算此時的字號
        CGFloat fontSize = (-((16.0 * Y)/33.0)) - 892.0/33.0;
        self.titleLabel.font = [UIFont fontWithName:@"HelveticaNeue-Bold" size:fontSize];
        //            NSLog(@"point:: %f",self.titleLabel.font.pointSize);
        //            更新titlelabel的高度約束
        [self.titleLabel mas_updateConstraints:^(MASConstraintMaker *make) {
            make.height.mas_equalTo(self.titleLabel.font.pointSize + 0.5);
        }];
        //            計算此刻button的對應尺寸,若大於最小值(16),則更新約束
        CGFloat buttonSize = self.titleLabel.font.pointSize * (5.0/9.0);
        if (buttonSize >= 16.0)
            [self.button mas_updateConstraints:^(MASConstraintMaker *make) {
                make.width.mas_equalTo(buttonSize);
                make.height.mas_equalTo(buttonSize);
            }];
    }複製代碼

這裏計算比較繁瑣,能夠仔細看一下。

4. UITableview的多種控件嵌套

這個部份內容在前文的頁面實現部分已經簡單講過,這裏列出來是提醒初學的朋友能夠稍做留意。

5. 手動封裝一些經常使用的視圖控件

在本項目中,我封裝了頁面的導航欄視圖HeaderView,選擇器視圖SelectView以及頁面的加載loading視圖LYLoadingView。須要瞭解的同窗能夠留心看一些。
這裏簡單展現一下loading視圖的封裝。
這是頭文件部分:

@interface LYLoadingView : UIView
//隱藏傳入view中的loadingview
+ (BOOL)hideLoadingViewFromView:(UIView *)view;
//爲傳入view顯示一個loadingview
+ (BOOL)showLoadingViewToView:(UIView *)view WithFrame:(CGRect)frame;
@end複製代碼

這是實現部分:

+ (BOOL)hideLoadingViewFromView:(UIView *)view
{
    NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];
    for (UIView *subview in subviewsEnum)
    {
        if([subview isKindOfClass:self])
        {
            [subview removeFromSuperview];
            return YES;
        }
    }
    return NO;
}

+ (BOOL)showLoadingViewToView:(UIView *)view WithFrame:(CGRect)frame
{
    LYLoadingView *loadingView = [[LYLoadingView alloc] initWithFrame:frame];
    loadingView.backgroundColor = [UIColor whiteColor];
    UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
    indicator.center = CGPointMake(frame.size.width/2, frame.size.height/2 - 100);
    [indicator startAnimating];
    [loadingView addSubview:indicator];
    [view addSubview:loadingView];
    return YES;
}複製代碼

loading視圖模仿官方app的一個簡單菊花指示器。
使用時,在頁面渲染最開始在視圖上加一個loadingview:

//    初始化loadingview
CGRect loadingViewFrame = CGRectMake(0, 130, LYScreenWidth, LYScreenHeight - 130);
[LYLoadingView showLoadingViewToView:self.view WithFrame:loadingViewFrame];複製代碼

頁面數據獲取完成後,table進行reload,而後移除loading視圖:

[self.newsTableView reloadData];
//        隱藏loadingview
[LYLoadingView hideLoadingViewFromView:self.view];複製代碼

5、寫在最後

這個項目並無100%徹底復原官方客戶端,筆者閒暇時間不容許,因此算是倉促結束,而且寫了這篇文章做結尾。項目中還存在一些bug,也有未完成的功能點,歡迎你們fork。
有不足之處歡迎你們指出,也歡迎討論項目中的其餘實現方式,但願幫助到須要的同窗。

最後再貼一下 LYSSPai項目地址。若是以爲不錯,但願點個star~

halo

相關文章
相關標籤/搜索