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

少數(shù)派

一、寫在前面

在我的iOS開發(fā)學(xué)習(xí)過程中,閱讀過許多同學(xué)的高仿項目文章、源碼,對我助益頗深。但是許許多多的高仿項目在技術(shù)方面各有側(cè)重,所以我先把本項目中值得探討的技術(shù)點列出,方便正好需要的同學(xué)。

本項目重點探討:

  • UITableview的性能優(yōu)化
  • UIScrollView的進(jìn)階使用
  • 少數(shù)派客戶端導(dǎo)航欄動態(tài)效果的實現(xiàn)
  • UITableview的多種控件嵌套
  • 手動封裝一些常用的視圖控件

二、簡述

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


LYSSPai運行展示

對于原客戶端的一些重復(fù)性細(xì)節(jié)沒有全部實現(xiàn),歡迎大家fork。

這里是 LYSSPai項目地址。

在本文中,我會先介紹項目的整體實現(xiàn)思路,然后對于開發(fā)過程中遇到的值得探討的點進(jìn)一步講述。

項目中的數(shù)據(jù)來源為使用Charles抓包獲取,用json文件存在bundle中。
項目中的素材來源為官方客戶端ipa包使用iOS Images Extractors解析獲得。
聲明:僅用于學(xué)習(xí)交流,嚴(yán)禁用于商業(yè)用途。

三、整體實現(xiàn)思路

在這一節(jié),我會按照頁面來介紹整體開發(fā)思路。

1. 首頁

首頁展示-1

首頁展示-2

1.1 頁面簡述

  • 這是項目的首頁,主要結(jié)構(gòu)是頂部的導(dǎo)航欄和下面的內(nèi)容。
  • 導(dǎo)航欄效果:
    在頁面向上滑動時,頂部導(dǎo)航欄的文字、按鈕尺寸會隨之動態(tài)減小,而后整體上移,懸停在頂部,模擬系統(tǒng)的導(dǎo)航欄效果。當(dāng)頁面下滑時,效果相反。
  • 內(nèi)容展示部分:
    首先有一個左右滑動的類似輪播圖部分。用以展示重點推薦的專題、文章、廣告等。
    接下來是一篇文章。
    然后又是一個手動滑動的類似輪播圖。用來展示付費的欄目。
    剩余部分全為文章。

1.2 實現(xiàn)思路

1.2.1 內(nèi)容展示

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

1.2.2 導(dǎo)航欄實現(xiàn)

導(dǎo)航欄的動態(tài)效果需要隨著內(nèi)容滑動而進(jìn)行,而后懸停在頂部。其中涉及導(dǎo)航欄的高度變化以及懸停效果
我們很容易想到使用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,必須顯式調(diào)用set方法
[self.tableView setTableHeaderView:headerView];

[self.tableView endUpdates];

而sectionHeader是默認(rèn)帶有懸停效果的,但是我沒有找到可以高效更新視圖高度的方法,所以這種方法果斷放棄。
對于tableHeader的懸停效果,可以在頁面滑到臨界點時,將tableHeader加入到與tableview同一層級的view中,手動實現(xiàn)懸停效果,這也是許多UIScrollView的子View想要實現(xiàn)頁面懸停效果的方式。
但是有一點需要知道,UITableView是一個龐大的對象,對它頻繁更新勢必會影響性能。而動態(tài)更改tableHeader時,會不停地改變整個UITableView的布局。為了一個小小的動態(tài)效果實在不必如此。所以,我使用一個單獨的view作為頂部的導(dǎo)航欄,并且將它和tableview加入到同一個容器scrollview中。這樣動態(tài)效果僅僅影響這個單獨的view布局。

1.2.3 分類專題頁
分類專題頁

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

1.2.4 文章閱讀頁面

文章閱讀頁

點擊文章cell或者輪播部分的文章類型子cell,會進(jìn)入對應(yīng)的文章閱讀頁面。
這個頁面底部導(dǎo)航欄為手動模擬實現(xiàn)。文章展示使用WKWebView。在整個頁面包含web內(nèi)容部分,均可以右劃返回。

關(guān)于使用WebView展示內(nèi)容的探討,在我的簡書文章從簡書iOS客戶端,來談?wù)凥ybrid方案細(xì)節(jié)設(shè)計進(jìn)行了詳細(xì)探討,歡迎大家閱讀。

2.發(fā)現(xiàn)

發(fā)現(xiàn)頁展示

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

3.消息

消息頁展示

這個頁面沒有特別復(fù)雜的部分。不過自己封裝了選擇器View,效果和原客戶端完全一致,需要的同學(xué)可以閱讀這部分代碼。其中涉及到UIScrollView的一些進(jìn)階特性,一會會詳述。

四、重點詳述

1. tableview性能優(yōu)化

  • 優(yōu)化場景

頁面開發(fā)完成后,cell嵌套scrollview,其中還包括多個子cell,如果不加優(yōu)化的話,可以預(yù)見使用體驗不會太好。在第一次滑動到第二個輪播圖時,很明顯感受到頁面fps下降。而后滑動流暢,fps基本保持在60。所以我們知道,優(yōu)化重點在于輪播圖的首次加載、渲染。輪播圖首次出現(xiàn)在屏幕范圍中之后,被加入緩存,所以再次滑到這里時便不會卡頓。
說到性能優(yōu)化,不得不推薦一下ibireme的文章,強烈建議沒看過的同學(xué)認(rèn)真閱讀一下iOS 保持界面流暢的技巧。

  • 優(yōu)化思路

滑動頁面時fps在60左右時,用戶不會感覺到卡頓,這是優(yōu)化的目標(biāo)。也就是說,我們需要在1s/60 = 16.7ms內(nèi),完成每一幀的渲染。而視圖渲染需要CPU運算+GPU渲染運算共同完成。所以我們需要分析在這個場景下,CPU與GPU各自的工作量,合理調(diào)配,從而使它們的每一幀運算耗時總和低于16.7ms。

  • cell重用

cell重用是非?;A(chǔ)但又非常重要的優(yōu)化手段,正確使用tableview的cell重用機制。

  • cell高度緩存

tableview的渲染過程中,有多少個cell,就會調(diào)用多少次- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法,從而確定contentSize。所以,盡量將cell的高度提前計算并且進(jìn)行緩存,避免在這個代理方法中進(jìn)行計算,可以有效優(yōu)化tableview的渲染。

  • 布局計算優(yōu)化

布局的計算是CPU的工作,當(dāng)頁面層級復(fù)雜時,布局計算就會耗費較多時間。同時,應(yīng)該明確的一點是使用Masonry自動布局是將布局計算量交給CPU去完成,勢必會相對增加耗時。所以,在復(fù)雜cell的優(yōu)化中,一般建議手動計算布局,會稍微提升一些性能。除此之外,如果頁面布局計算量比較大的話,將布局計算在頁面渲染之前完成并且緩存,會有效減少視圖渲染時的16.7ms中的CPU運算時間。
在本項目中,我為輪播圖cell封裝了一個frameModel,在頁面數(shù)據(jù)獲取完成后,提前計算輪播圖的布局結(jié)果,在頁面渲染時,無需計算便可以直接賦值。

//count為輪播圖子cell數(shù)量
+(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循環(huán)并且每一個循環(huán)體都稍有計算量,將這些計算工作提前并且在子線程執(zhí)行是非常明智的。我們要讓那16.7ms“用在刀刃上”。

  • 正確選擇視圖控件,為視圖瘦身

UIViewCALayer的關(guān)系大家應(yīng)該都有所了解。UIView在CALayer的基礎(chǔ)上,封裝了交互操作相關(guān)的部分,UIView是比CALayer更重量的。如果當(dāng)前控件不需要響應(yīng)用戶操作,我們應(yīng)該盡可能使用CALayer替代UIView。
在本項目中,付費內(nèi)容輪播圖部分,整個子cell需要響應(yīng)用戶的點擊操作。所以只需要在子cell的最底層view添加手勢識別。而背景圖片、用戶頭像等元素是不需要響應(yīng)特殊操作的,所以這些控件不使用UIImageView,改用CALayer。其實文字部分,也可以不使用UILabel,這是可以繼續(xù)優(yōu)化的部分。
這是頭像部分的布局代碼:

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];
  • 網(wǎng)絡(luò)內(nèi)容異步加載

待頁面顯示出來之后,網(wǎng)絡(luò)內(nèi)容再慢慢加載,也是為了將時間用在刀刃上。
異步加載網(wǎng)絡(luò)圖片的框架,有大家都熟知的SDWebImage,也有ibiremeYYWebImage。據(jù)介紹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];
  • 圓角設(shè)置

又是老生常談的圓角設(shè)置。使用CALayer的相關(guān)屬性來實現(xiàn)圓角效果會觸發(fā)離屏渲染,增加GPU的工作量。在這一點的優(yōu)化上,可以使用CPU將圖片素材直接裁剪為圓角圖片再進(jìn)行顯示。當(dāng)然,最優(yōu)的方案當(dāng)然是讓你們的美工直接提供圓角素材~
這里我直接使用了YYImage的圓角處理。

2. UIScrollView的進(jìn)階使用

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

selectView效果展示

一個非常簡單的控件。但是有一個細(xì)節(jié)需要注意:使用輕劃手勢左右滑動時,頁面必然進(jìn)行滾動。而使用拖拽時,則會判斷拖拽范圍來決定是否進(jìn)行滾動。
這個效果我使用了UIScrollView的代理方法- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate來實現(xiàn)。
這里是代碼:

//停止拖拽時的代理
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
//    如果是內(nèi)容頁的橫向滑動
    if (scrollView == self.contentView)
    {
        NSLog(@"slowing?? %@",decelerate ? @"YES" : @"NO");
        CGFloat scrollX = scrollView.contentOffset.x;
//        如果帶有慣性(快速滑動),則內(nèi)容頁必然進(jìn)行對應(yīng)的移動
        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;
            }
        }
//        如果無慣性(慢速拖拽),此時需要滿足拖動的范圍才會進(jìn)行移動
        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];
    }
}

當(dāng)輕劃頁面時,scrollview是有慣性的,而拖拽時是沒有慣性的,利用這個特性來進(jìn)行相應(yīng)的判斷。
這里是小橫條移動的動畫:

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

3. 導(dǎo)航欄動態(tài)效果的實現(xiàn)

先重新看一下效果:

導(dǎo)航欄效果展示

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

 //    scrollview剛剛開始滑動,此時導(dǎo)航標(biāo)題大小和按鈕大小進(jìn)行變化
    if (Y <= -97 && Y > -130)
    {
        //            以字號為36和20計算得出的臨界Y值為-97和-130,根據(jù)此刻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的對應(yīng)尺寸,若大于最小值(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);
            }];
    }

這里計算比較繁瑣,可以仔細(xì)看一下。

4. UITableview的多種控件嵌套

這個部分內(nèi)容在前文的頁面實現(xiàn)部分已經(jīng)簡單講過,這里列出來是提醒初學(xué)的朋友可以稍作留意。

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

在本項目中,我封裝了頁面的導(dǎo)航欄視圖HeaderView,選擇器視圖SelectView以及頁面的加載loading視圖LYLoadingView。需要了解的同學(xué)可以留心看一些。
這里簡單展示一下loading視圖的封裝。
這是頭文件部分:

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

這是實現(xiàn)部分:

+ (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];

頁面數(shù)據(jù)獲取完成后,table進(jìn)行reload,然后移除loading視圖:

[self.newsTableView reloadData];
//        隱藏loadingview
[LYLoadingView hideLoadingViewFromView:self.view];

五、寫在最后

這個項目并沒有100%完全復(fù)原官方客戶端,筆者閑暇時間不允許,所以算是倉促結(jié)束,并且寫了這篇文章作結(jié)尾。項目中還存在一些bug,也有未完成的功能點,歡迎大家fork。
有不足之處歡迎大家指出,也歡迎討論項目中的其他實現(xiàn)方式,希望幫助到需要的同學(xué)。

最后再貼一下 LYSSPai項目地址。如果覺得不錯,希望點個star~

halo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 少數(shù)派 一、寫在前面 在我的iOS開發(fā)學(xué)習(xí)過程中,閱讀過許多同學(xué)的高仿項目文章、源碼,對我助益頗深。但是許許多多的...
    軟件iOS開發(fā)閱讀 341評論 0 0
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,323評論 25 708
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,631評論 4 61
  • 今天出乎意料地,我們居然雙休!但是貓爸帶高三,每個月只有月底的一周才有雙休,所以打算下午貓爸上完課我們回庫爾勒。 ...
    星酉林夕閱讀 188評論 0 0
  • 2016年12月29日,91年出生的25歲李靖(公眾號:李叫獸)被任命百度副總裁,引起好多驚嘆。其實在職場環(huán)境中,...
    xindong_ying閱讀 269評論 0 0

友情鏈接更多精彩內(nèi)容