关于高仿优雅的好奇心日报的项目优化

这篇文章主要是介绍iOS-高仿优雅的好奇心日报之前这个项目的优化更新和一些设计模式的修改问题。
源码请看:JFQDaily-Github源码

此次主要做了如下工作:

  • 优化主页UITableView;
  • 优化定时器NSTimer和轮播器;
  • 修改数据模型;
  • 之前大多使用block的部分,现改成使用代理;
  • 使用Swift和OC混编实现登录、注册等注册界面;

先看新版效果:

JFQDaily效果展示.gif

1、优化主页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

下面是heightForRowAtIndexPathcellForRowAtIndexPath两个方法的实现。

// 直接返回对应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再实现一遍。
下面是实现效果:

注册界面.png

登录界面.png

关于Swift和OC混编。Swift如何调用OC方法,OC如何调用Swift的方法就不细说了,网上有很多教程和博客。这里就简单提一下此次的项目更新和改动,具体请看源码和我的上一篇iOS-高仿优雅的好奇心日报

总结:

本篇博客主要是介绍仿好奇心日报该项目的优化和修改点,可能具体知识细节内容没有详细说,不过相信大家在网上一搜一堆。项目源码请看:JFQDaily-Github源码喜欢的话可以Star、Fork,后面会坚持完善和更新。
最后感谢您耐心的看完本篇博客,如有错误和不妥的地方欢迎留言指正!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容