在個人iOS開發學習過程當中,閱讀過許多同窗的高仿項目文章、源碼,對我助益頗深。可是許許多多的高仿項目在技術方面各有側重,因此我先把本項目中值得探討的技術點列出,方便正好須要的同窗。ios
本項目重點探討:git
首先來看一下項目的運行效果:
github
這裏是 LYSSPai項目地址。web
在本文中,我會先介紹項目的總體實現思路,而後對於開發過程當中遇到的值得探討的點進一步講述。json
項目中的數據來源爲使用
Charles
抓包獲取,用json文件存在bundle中。
項目中的素材來源爲官方客戶端ipa包使用iOS Images Extractors
解析得到。
聲明:僅用於學習交流,嚴禁用於商業用途。緩存
在這一節,我會按照頁面來介紹總體開發思路。性能優化
使用UITableview,包含三種cell。
輪播圖爲橫向的UIScrollView,爲其中的每個子cell設置tag值,點擊事件以delegate的方式交由首頁VC實現。
文章展現cell爲普通的cell。右上角的菜單按鈕點擊事件以delegate的方式交由首頁VC實現。bash
導航欄的動態效果須要隨着內容滑動而進行,然後懸停在頂部。其中涉及導航欄的高度變化以及懸停效果。
咱們很容易想到使用UITableView的tableHeader
和sectionHeader
,那麼先來明確一下這兩種視圖的特性:
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
WKWebView
。在整個頁面包含web內容部分,都可以右劃返回。
關於使用WebView
展現內容的探討,在個人文章從簡書iOS客戶端,來談談Hybrid方案細節設計進行了詳細探討,歡迎你們閱讀。
- (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「用在刀刃上」。
UIView
和CALayer
的關係你們應該都有所瞭解。UIView在CALayer的基礎上,封裝了交互操做相關的部分,UIView是比CALayer更重量的。若是當前控件不須要響應用戶操做,咱們應該儘量使用CALayer替代UIView。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
,也有ibireme的YYWebImage
。據介紹YYWebImage的性能是要比SD好一些的,這個我沒有親自驗證。[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];複製代碼
YYImage
的圓角處理。這個部分我主要講的是消息頁面的選擇器控件封裝的思路。
先看效果:
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];
}複製代碼
先從新看一下效果:
- (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);
}];
}複製代碼
這裏計算比較繁瑣,能夠仔細看一下。
這個部份內容在前文的頁面實現部分已經簡單講過,這裏列出來是提醒初學的朋友能夠稍做留意。
在本項目中,我封裝了頁面的導航欄視圖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];複製代碼
這個項目並無100%徹底復原官方客戶端,筆者閒暇時間不容許,因此算是倉促結束,而且寫了這篇文章做結尾。項目中還存在一些bug,也有未完成的功能點,歡迎你們fork。
有不足之處歡迎你們指出,也歡迎討論項目中的其餘實現方式,但願幫助到須要的同窗。
最後再貼一下 LYSSPai項目地址。若是以爲不錯,但願點個star~
halo