在个人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