关于高仿优雅的好奇心日报的项目优化
这篇文章主要是介绍iOS-高仿优雅的好奇心日报之前这个项目的优化更新和一些设计模式的修改问题。
源码请看:JFQDaily-Github源码
此次主要做了如下工作:
- 优化主页UITableView;
- 优化定时器NSTimer和轮播器;
- 修改数据模型;
- 之前大多使用block的部分,现改成使用代理;
- 使用Swift和OC混编实现登录、注册等注册界面;
先看新版效果:
JFQDaily效果展示.gif1、优化主页UITableView
关于如何实现丝滑般顺畅的UITableView文章有很多,这里推荐大家看下ibireme大神的iOS 保持界面流畅的技巧和他的YYKit Demo源码。
本次优化UITableView的流畅度的核心思想是不要在下面两个方法中处理不必要的业务逻辑,使其内部逻辑实现足够简洁;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
以上两个方法的调用时机是:在cell显示到屏幕之前先调用heightForRowAtIndexPath
方法,原则是有多少条数据该方法就会被调用多少次,在cell显示到屏幕时分别调用cellForRowAtIndexPath
方法和heightForRowAtIndexPath
方法,原则是屏幕上显示多少行cell它们就分别被调用多少次;且当用户滑动tableView,屏幕上每出现一个新cell时就会调用cellForRowAtIndexPath
方法和heightForRowAtIndexPath
方法。调用的如此频繁,可想而知,若这两方法中处理过多复杂业务逻辑的话是多么恶心的一件事儿!
此项目目前有三种cell,高度和样式都不同,所以我新建了一个JFNewsCellLayout
布局模式类用来返回cell的风格、高度和数据;
#import <UIKit/UIKit.h>
#import "JFResponseModel.h"
/// 风格
typedef NS_ENUM(NSUInteger, JFNewsCellLayoutStyle) {
JFNewsCellLayoutStyleAbove = 0, // cellType = 0,图片在上,文字在下
JFNewsCellLayoutStyleRight = 1, // cellType = 1,图片在右,文字在左
JFNewsCellLayoutStyleDetails = 2, // cellType = 2,图片在上,文字在下,下方有“评论”和“喜欢”数值
};
@interface JFNewsCellLayout : NSObject
@property (nonatomic, assign) JFNewsCellLayoutStyle style; // cell风格
@property (nonatomic, assign, readonly) CGFloat height; // cell高度
@property (nonatomic, strong) JFFeedsModel *model; // 数据
- (instancetype)initWithModel:(JFFeedsModel *)model style:(JFNewsCellLayoutStyle)style;
@end
在JFHomeViewController获取到数据后将数据转成JFNewsCellLayout
模型塞到一个如下数组中;
@property (nonatomic, strong) NSMutableArray <JFNewsCellLayout *> *layouts;
转换过程具体请看:
#pragma mark --- 数据管理器
- (JFHomeNewsDataManager *)manager {
// code...
}
下面咱们来看看自定义的cell头文件JFHomeNewsTableViewCell.h
:主要把cell分成两部分,底部带新闻类型、评论数等的JFBottomView部分和主要展示新闻图片和新闻标题的JFNewsView部分;然后在JFHomeNewsTableViewCell.h
中声明了- (void)setLayout:(JFNewsCellLayout *)layout
方法供cellForRowAtIndexPath
方法调用,具体实现请看源码。
#import <UIKit/UIKit.h>
#import "JFNewsCellLayout.h"
#import <UIImageView+WebCache.h>
#import "JFConfigFile.h"
@interface JFBottomView : UIView
@property (nonatomic, strong) UIImageView *commentImageView; // 评论icon
@property (nonatomic, strong) UIImageView *praiseImageView; // 喜欢icon
@property (nonatomic, strong) UILabel *newsTypeLabel; // 新闻类型(设计、智能、娱乐等)
@property (nonatomic, strong) UILabel *commentlabel; // 该条新闻的评论数
@property (nonatomic, strong) UILabel *praiseLabel; // 点赞数
@property (nonatomic, strong) UILabel *timeLabel; // 新闻发布时间
@end
@interface JFNewsView : UIView
@property (nonatomic, strong) UIImageView *newsImageView; // 新闻图片
@property (nonatomic, strong) UILabel *newsTitleLabel; // 新闻标题
@property (nonatomic, strong) UILabel *subheadLabel; // 新闻副标题
@end
@interface JFHomeNewsTableViewCell : UITableViewCell
@property (nonatomic, strong) JFNewsView *newsView;
@property (nonatomic, strong) JFBottomView *bottomView;
@property (nonatomic, strong) UIView *cellBackgroundView;
@property (nonatomic, strong) JFNewsCellLayout *layout;;
- (void)setLayout:(JFNewsCellLayout *)layout;
@end
下面是heightForRowAtIndexPath
和cellForRowAtIndexPath
两个方法的实现。
// 直接返回对应cell的高度,不需要在方法中做判断和计算
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return ((JFNewsCellLayout *)_layouts[indexPath.row]).height;
}
// 只做初始化cell的工作,然后通过setLayout:设置cell的样式,这个工作是由JFHomeNewsTableViewCell自己做
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *cellID = @"newsCell";
self.cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (!_cell) {
_cell = [[JFHomeNewsTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
}
[_cell setLayout:_layouts[indexPath.row]];
return _cell;
}
如此这般就是改善了之前版本中的UITableView滑动卡顿和加载新数据跳闪的问题,基本能保证tableView在60帧左右滑动,目前有个问题待解决,就是在有gif图片时滑动掉帧很严重。
温馨提示:上拉加载数据的时机做了小小的变动,以提供更流畅的用户体验!
在用户滑动tableView,数据还剩10行左右时开始提前加载网络数据,这样可以尽量在用户无感知的状态下加载数据,大大提升滑动流畅体验。当然滑动过快和网速较慢的情况下,该方法作用甚微!
#pragma mark --- UIScrollDelegate
/// 滚动时调用
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 其它逻辑代码略...
//提前加载数据,以提供更流畅的用户体验
NSIndexPath *indexPatch = [_homeNewsTableView indexPathForRowAtPoint:CGPointMake(40, scrollView.contentOffset.y)];
if (indexPatch.row == (_layouts.count - 10)) {
if (_row == indexPatch.row) return;//避免重复加载
_row = indexPatch.row;
[self loadData];
}
}
关于改项目的UITableView优化部分到这基本介绍完了,关于tableView的head上的轮播图优化会在下面继续讲。
2、优化定时器NSTimer和轮播器
2.1 NStimer
直接使用NSTimer的+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
方法会出现循环引用的可能,大家应该都知道;例如在需要实现定时器的类中创建一个NSTimer实例,然后再调用上面的类方法设置定时器,此时该类会强引用NSTimer的这个实例,而上面方法的target的目标对象是self,而定时器会保留目标对象self,所以当指定时间间隔重复执行任务,又没有主动的销毁定时器,就会造成了循环引用。所以切记NSTimer会保留其目标对象,直到计时器本身失效为止!
所以要想打破这个循环引用,只能改变实例变量或令定时器无效!
用block和创建NSTimer的分类打破该循环引用
直接看分类代码:
#import <Foundation/Foundation.h>
@interface NSTimer (JFBlocksTimer)
+ (NSTimer *)jf_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats;
@end
#import "NSTimer+JFBlocksTimer.h"
@implementation NSTimer (JFBlocksTimer)
+ (NSTimer *)jf_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)())block repeats:(BOOL)repeats {
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(jf_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)jf_blockInvoke:(NSTimer *)timer {
void (^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
上面这段代码将定时器所要执行的任务分装成block,在调用定时器函数时,把它作为userInfo参数传进去,只要定时器有效,就会一直保留着它。传入参数时要通过copy方法将block拷贝到“堆”上,否则等到稍后要执行它的时候,该block可能已经无效了。
下面看该方法的使用:
- (void)addTimer {
if (self.timer) return;
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer jf_scheduledTimerWithTimeInterval:4 block:^{
__strong typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf nextImage];
}
}
repeats:YES];
}
可以看到使用经典的打破block的循环引用的方法即可打破定时器的循环引用。
2.2 项目轮播器的优化
这里所指的轮播器优化,实质上是指合理的使用NSTimer的暂停和开启。也牵扯到UITableView的性能优化,即当head的轮播器滑出界面时暂停定时器,当出现轮播器出现时再开启定时器。
// 开启定时器
- (void)startTimer {
[self.timer setFireDate:[NSDate distantPast]];
}
// 暂停定时器
- (void)stopTimer {
[self.timer setFireDate:[NSDate distantFuture]];
}
在JFHomeViewController.m
中,当用户滑动时适时的开启和关闭定时器。
#pragma mark --- UIScrollDelegate
/// 滚动时调用
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.y > 400) { // 轮播图滑出界面时,关闭定时器
if (_isRuning) {
[self.loopView stopTimer];
_isBeyondBorder = YES;
_isRuning = NO;
}
}else if (scrollView.contentOffset.y < 400) { // 轮播图进入界面时,打开定时器
if (!_isRuning) {
[self.loopView startTimer];
_isRuning = YES;
_isBeyondBorder = NO;
}
}
}
3、修改数据模型
主要是将之前的多个数据模型类文件合并到一个文件中,在没有复杂的实现方法和业务逻辑时,个人觉得这种写法会更简洁、清晰,特别适合建立简单的数据模式。
#import <Foundation/Foundation.h>
//*********************************JFCategoryModel****************************//
@interface JFCategoryModel : NSObject
@property (nonatomic, copy) NSString *title; // 新闻类型(设计、娱乐、智能等)
@end
//*********************************JFPostModel****************************//
@interface JFPostModel : NSObject
@property (nonatomic, copy) NSString *title; // 新闻标题
@property (nonatomic, copy) NSString *subhead; // 副标题
@property (nonatomic, assign) NSInteger publish_time; // 出版时间
@property (nonatomic, copy) NSString *image; // 配图
@property (nonatomic, assign) NSInteger comment_count; // 评论数
@property (nonatomic, assign) NSInteger praise_count; // 点赞数
@property (nonatomic, copy) NSString *appview; // 新闻文章链接(html格式)
@property (nonatomic, strong) JFCategoryModel *category;
@end
//*********************************JFFeedsModel****************************//
@interface JFFeedsModel : NSObject
@property (nonatomic, copy) NSString *type; // 文章类型(以此来判断cell(文章显示)的样式)
@property (nonatomic, copy) NSString *image; // 文章配图
@property (nonatomic, strong) JFPostModel *post;
@end
//*********************************JFResponseModel****************************//
@interface JFResponseModel : NSObject
@property (nonatomic, copy) NSString *has_more; // 下拉加载时判断是否还有更多文章 false:没有 true:有
@property (nonatomic, copy) NSString *last_key; // 下拉加载时需要拼接到URL中的key
@property (nonatomic, strong) JFFeedsModel *feeds;
@end
//*********************************JFBannersModel****************************//
@interface JFBannersModel : JFFeedsModel
@end
4、之前大多使用block的部分,现改成使用代理
这个问题牵扯到经典的面试问题:“block、代理和通知的区别”,其实我将项目中之前使用block的部分改成代理的原因主要是代理看着比blcok条理更清晰,更能把控整个项目流程,使代码看起来更整洁清晰且不用担心循环引用问题。其它的就不再赘述!
5、使用Swift和OC混编实现登录、注册等注册界面;
使用Swift和OC混编的目的是想转战Swift,所以借这个项目练练手,准备将之前写的iOS-(仿美团)城市选择器+自动定位+字母索引用Swift再实现一遍。
下面是实现效果:
关于Swift和OC混编。Swift如何调用OC方法,OC如何调用Swift的方法就不细说了,网上有很多教程和博客。这里就简单提一下此次的项目更新和改动,具体请看源码和我的上一篇iOS-高仿优雅的好奇心日报
总结:
本篇博客主要是介绍仿好奇心日报该项目的优化和修改点,可能具体知识细节内容没有详细说,不过相信大家在网上一搜一堆。项目源码请看:JFQDaily-Github源码喜欢的话可以Star、Fork,后面会坚持完善和更新。
最后感谢您耐心的看完本篇博客,如有错误和不妥的地方欢迎留言指正!