前言


        打开这个APP首先吸引我的就是它简洁干净的界面和优雅的配色,整个首页只有一个带有logo的悬浮按钮,然后就是带有高清配图不同与其他新闻平台的新闻文章,图标基本是简单的黑、黄、白三种颜色加单线条图形的设计。最后让我下定决心仿它的是它那简单到自然的动画效果。说这么多当然不是给好奇心日报打广告,只是想说我为什么写这个项目。额,就这么简单!


先看看JFQDaily效果:

iOS-高仿优雅的好奇心日报项目详解_悬浮按钮


        如果你是一个iOS开发入门级的猿,有兴趣话可以下载JFQDaily源码然后结合本篇博客来看,相信你会有所收获的,代码写的接地气,注释详细!


准备工作


        1、高仿,原生图片图标自然必不可少,利用iOS images Extractor抓取好奇心日报的图片,如何使用iOS images Extractor抓取APP图片,iOS直播APP-点赞动画的实现这篇文章下面有介绍。

        2、用青花瓷(Charles)抓取好奇心日报数据,Charles的用法网上有很多文章,可以看看Charles常用的十大功能。

        3、筛选数据:

  • GET请求,拼接url路径,第一次获取数据url是:​
  • 获取last_key后上拉加载时GET的URL​


iOS-高仿优雅的好奇心日报项目详解_悬浮按钮_02


iOS-高仿优雅的好奇心日报项目详解_悬浮按钮_03


iOS-高仿优雅的好奇心日报项目详解_悬浮按钮_04

        上面是用chalers所抓取到的数据,分析并拿到你需要的数据就行,上面标注的都是项目中需要用到的数据,具体筛选过程就不再分析了,无非是打开好奇心日报看APP所展示的信息和抓到的数据做对比,找到相应的映射关系。如果你想走的更远独立行走是必要的。具体数据分析里的细节问题可以认真看源码​​Models​​文件夹类的​​数据模型​​文件。好啦准备工作完成啦,开始动手建工程码代码吧!


项目文件结构


iOS-高仿优雅的好奇心日报项目详解_#import_05

1、 AppDeleteggate文件夹

  • 放着AppDelegate .h和.m文件

2、Tools:工具类

  • NSString+JFMessage:NSString的类扩展,添加了计算文本高度和将毫秒转换成日期的类方法
  • JFLoopView:无限循环图片轮播器,关于JFLoopView可以看一行代码实现图片无限轮播器
  • MBProgressHUD+JFProgressHUD:MBProgressHUD的类扩展,添加了一个创建MBProgressHUD类方法,方便调用。
  • JFTimer:定时器,在一行代码实现图片无限轮播器中有讲到。
  • JFConfigFile:这个里面是一些高频的宏定义

3、Models:数据模型

  • 这里是根据之前使用chalers筛选的数据建立的数据模型

4、DataManager:数据管理器

  • 使用第三方框架AFNetworking创建的数据管理器,使用GET请求相关数据。

5、ViewControllers:控制器

  • JFHomeViewController:首页控制器
  • JFReaderViewController:文章阅读器控制器,使用WXWebView搭建。关于WXWebView推荐iOS (一) - UIWebView 与 WKWebView . 基本使用

6、views:界面

  • JFSuspensionView:悬浮按钮View
  • JFHomeNewsTableViewCell:首页cell,继承自UITableViewCell
  • JFMenuView:菜单界面
  • JFNewsClassificationView新闻分类界面继承自UIView

7、pods:项目所用到的第三方框架

  • Masonry :Masonry是目前最流行的AutoLayout框架。推荐追求Masonry
  • AFNetworking:是一个非常方便的网络请求库,可以轻松实现各种网络请求,比如经常使用的GET请求、POST请求等。
  • MJRefresh:李明杰老师写的下拉刷新框架,使用方法很简单。
  • MJExtension:json数据转模型的框架,也是李明杰老师写的,用法都很简单。相关用法可以看:iOS的学习笔记38 MJExtension使用
  • SDWebImage:目前最受欢迎的图片下载第三方框架,使用率很高。
  • MBProgressHUD:是一个显示HUD窗口的第三方类库,用法简单。

各框架的具体用法网上很多资料,不再赘述!


代码


1、搭建数据模型

iOS-高仿优雅的好奇心日报项目详解_悬浮按钮_06

        这是我们用chalers抓取的json格式的数据,根据MJExtension的使用方法建立数据模型,先分别创建JFResponseModel、JFFeedsModel、JFPostModel、JFCategoryModel四个类,继承自NSObject。

        然后我们慢慢从json数据的最里层向外层声明你所需要的对应参数(如果你还不了解MJExtension,建议你先看看他的官网说明文档,或者iOS的学习笔记38 MJExtension使用),切记参数可以自行选择创建,但是模型的类型一个不能少,参数名一定要一样,如果命名有冲突MJExtension提供的有相应的方法进行映射。例如:将冲突的参数​​description​​名映射到​​subhead​

/** 设置模型属性名和字典key之间的映射关系 */
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
/* 返回的字典,key为模型属性名,value为转化的字典的多级key */
return @{@"subhead":@"description"};
}


1.1 JFCategoryModel数据模型

#import <Foundation/Foundation.h>
@interface JFCategoryModel : NSObject
/** 新闻类型(设计、娱乐、智能等)*/
@property (nonatomic, copy) NSString *title;
@end


1.2 JFPostModel数据模型

#import <Foundation/Foundation.h>
@class JFCategoryModel;
@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;
/** 新闻文章链接(html格式)*/
@property (nonatomic, copy) NSString *appview;
@property (nonatomic, strong) JFCategoryModel *category;
@end


1.3 JFFeedsModel数据模型

#import <Foundation/Foundation.h>
@class JFPostModel;
@interface JFFeedsModel : NSObject
/** 文章类型(以此来判断cell(文章显示)的样式)*/
@property (nonatomic, copy) NSString *type;
/** 文章配图 */
@property (nonatomic, copy) NSString *image;
@property (nonatomic, strong) JFPostModel *post;
@end

        上面所提到的参数名冲突导致的参数名不一致问题的解决方法:

#import "JFPostModel.h"
@implementation JFPostModel
/** 设置模型属性名和字典key之间的映射关系 */
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
/* 返回的字典,key为模型属性名,value为转化的字典的多级key */
return @{@"subhead":@"description"};
}
@end


1.4 JFResponseModel数据模型

#import <Foundation/Foundation.h>
@class JFFeedsModel;
@interface JFResponseModel : NSObject
/** 下拉加载时判断是否还有更多文章 false:没有 true:有*/
@property (nonatomic, copy) NSString *has_more;
/** 下拉加载时需要拼接到URL中的key*/
@property (nonatomic, copy) NSString *last_key;
@property (nonatomic, strong) JFFeedsModel *feeds;
@end


1.5 JFBannersModel数据模型

        JFBannersModel模型与JFFeedsModel参数一样,所以继承自JFFeedsModel就好

#import "JFFeedsModel.h"
@interface JFBannersModel : JFFeedsModel
@end


        看起来模型很麻烦,只要你理清思路,掌握MJExtension用法,建立模型是很简单的,最主要是模型建好后,在后面使用数据时会非常方便。

2、DataManager数据管理器

        JFHomeNewsDataManager.h文件中添加一个请求新闻数据的方法和一个请求数据成功后回调的block方法。

#import <Foundation/Foundation.h>
typedef void(^JFHomeNewsDataManagerBlock)(id data);
@interface JFHomeNewsDataManager : NSObject
// 请求数据成功后返回新闻数据回调的block
@property (nonatomic, copy) JFHomeNewsDataManagerBlock newsDataBlock;
// 请求新闻数据
- (void)requestHomeNewsDataWithLastKey:(NSString *)lastKey;
- (void)newsDataBlock:(JFHomeNewsDataManagerBlock)block;
@end

        JFHomeNewsDataManager.m

#import "JFHomeNewsDataManager.h"
#import <AFNetworking.h>
#import "JFConfigFile.h"
#define kTimeOutInterval 10
@implementation JFHomeNewsDataManager
#pragma mark - 创建请求者
- (AFHTTPSessionManager *)manager {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer.timeoutInterval = kTimeOutInterval;
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
//设置相应内容类(这里根据所请求的数据类型可自行选择)
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json",
@"text/html",
@"image/jpeg",
@"image/png",
@"application/octet-stream",
@"text/json",
nil];
return manager;
}
#pragma mark - GET方式请求新闻数据
- (void)requestHomeNewsDataWithLastKey:(NSString *)lastKey {
AFHTTPSessionManager *manager = [self manager];
//拼接URL
NSString *urlString = [NSString stringWithFormat:@"http://app3.qdaily.com/app3/homes/index/%@.json?",lastKey];

[manager GET:urlString parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {

} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
// JSON数据转字典
NSDictionary *dataDictionary = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:nil];

if (self.newsDataBlock) {
self.newsDataBlock([dataDictionary valueForKey:@"response"]);
}

} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

}];
}
- (void)newsDataBlock:(JFHomeNewsDataManagerBlock)block {
self.newsDataBlock = block;
}
@end


        使用GET方法请求数据,将last_key参数拼接到URL中:

//拼接URL NSString *urlString = [NSString stringWithFormat:@"http://app3.qdaily.com/app3/homes/index/%@.json?",lastKey];


        打断点看我们请求到的JSON数据转换成字典dataDictionary里的内容:

iOS-高仿优雅的好奇心日报项目详解_数据_07


​valueForKey:​​方法拿到其数据返回给JFHomeViewController:

[dataDictionary valueForKey:@"response"]

        数据处理部分到此算是告一段落,接下来就是要把数据展示到界面上,然后实现好奇心日报的交互动画。


UI布局


3.1 项目中实现的三种不同的cell样式

cellType对应的就是数据模型JFFeedsModel中的type

  • cellType = 0,UITableViewCell的样式是上方新闻配图、然后是新闻标题,最下面是副标题。

iOS-高仿优雅的好奇心日报项目详解_#import_08


  • cellType = 1,UITableViewCell的样式是新闻配图在右侧,左侧是新闻标题,在其下面是新闻种类、评论数和点赞数。

iOS-高仿优雅的好奇心日报项目详解_数据_09


cellType = 2,UITableViewCell的样式和cellType = 0时基本一致,就多了最下面的新闻种类、评论数和点赞数。


        显然用苹果提供的cell是不行的,所以创建JFHomeNewsTableViewCell继承自UITableViewCell,然后我们来自定义cell。

        JFHomeNewsTableViewCell.h文件中声明属性:

#import <UIKit/UIKit.h>
@interface JFHomeNewsTableViewCell : UITableViewCell
/** cell的类型(0、1、2)*/
@property (nonatomic, copy) NSString *cellType;
/** 配图*/
@property (nonatomic, copy) NSString *newsImageName;
/** 标题*/
@property (nonatomic, copy) NSString *newsTitle;
/** 副标题*/
@property (nonatomic, copy) NSString *subhead;
/**
* 新闻类型(设计、智能、娱乐等)
*/
@property (nonatomic, copy) NSString *newsType;
/** 该条新闻的评论数*/
@property (nonatomic, copy) NSString *commentCount;
/** 点赞数*/
@property (nonatomic, copy) NSString *praiseCount;
/** 新闻发布时间*/
@property (nonatomic, assign) NSInteger time;
@end


​- (void)layoutSubviews;​​方法中,这里我把相关代码都放在了​​- (void)customUI;​​方法中,这里使用了Masonry自动布局,具体代码不在上了,可以下载JFQDaily源码看​​JFHomeNewsTableViewCell​​类文件。

- (void)layoutSubviews {
[super layoutSubviews];
[self customUI];
}


        使用Masonry时有一点一定要注意,必须先把子控件添加到父控件上才能用Masonry去自动布局,否则父控件上没有相应的子控件何谈布局呢。

​- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;​​代理方法来动态设置cell的高度;同样是根据类型判断。

/// 根据cell类型返回cell高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
JFHomeNewsTableViewCell *cell = self.cell;
if ([cell.cellType isEqualToString:@"0"]) {
return 330;
}else if ([cell.cellType isEqualToString:@"2"]) {
return 360;
}else {
return 130;
}
}


3.2 悬浮按钮(JFSuspensionView)

3.2.1 悬浮按钮实现的原理:

  • 其实很简单,就是在JFHomeViewController控制器的View上添加一个JFSuspensionView,只是要在homeNewsTableView之上,使用下面方法或者使用- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview;
    方法,此时当你滑动UITableView的时候按钮就是悬浮不动的。
- (void)loadView {
[super loadView];

[self.view addSubview:self.homeNewsTableView];
[self.view addSubview:self.jfSuspensionView];
}


        3.2.2 在JFSuspension.h文件中用枚举定义了悬浮按钮的四种Tag类型和四种block回调方法,其实四种Tag类型一一对应四个回调函数,通过判断Tag类型来执行相应的block函数。

#import <UIKit/UIKit.h>
/// 悬浮按钮种类(tag)枚举
typedef NS_ENUM(NSInteger, JFSuspensionButtonStyle) {
JFSuspensionButtonStyleQType = 1, // Qlogo样式 (弹出JFMenuView)
JFSuspensionButtonStyleCloseType, // 关闭样式(关闭JFMenuView)
JFSuspensionButtonStyleBackType, // 返回样式(返回到JFHomeViewController根View)
JFSuspensionButtonStyleBackType2 // 返回样式2(返回到JFMenuView)
};
typedef void(^JFSuspensionViewBlock)();
@interface JFSuspensionView : UIView
/** 悬浮按钮,设置按钮样式(tag)*/
@property (nonatomic, assign) NSInteger JFSuspensionButtonStyle;
/** 弹出菜单界面*/
@property (nonatomic, copy) JFSuspensionViewBlock popupMenuBlock;
/** 关闭菜单界面*/
@property (nonatomic, copy) JFSuspensionViewBlock closeMenuBlock;
/** 返回到homeNewsViewController*/
@property (nonatomic, copy) JFSuspensionViewBlock backBlock;
/** 返回到JFMenuView*/
@property (nonatomic, copy) JFSuspensionViewBlock backToMenuViewBlock;
- (void)popupMenuBlock:(JFSuspensionViewBlock)block;
- (void)closeMenuBlock:(JFSuspensionViewBlock)block;
- (void)backBlock:(JFSuspensionViewBlock)block;
- (void)backToMenuViewBlock:(JFSuspensionViewBlock)block;
@end


​- (void)clickSuspensionButton:(UIButton *)sender​​点击事件处理方法,通过tag判断需执行的事件。

- (void)clickSuspensionButton:(UIButton *)sender {
if (_suspensionButton.tag == JFSuspensionButtonStyleQType || _suspensionButton.tag == JFSuspensionButtonStyleCloseType) {
//需要做的事情...
}

//弹出菜单界面
if (_suspensionButton.tag == JFSuspensionButtonStyleQType) {
//需要做的事情...
}

//关闭菜单界面
if (_suspensionButton.tag == JFSuspensionButtonStyleCloseType) {
//需要做的事情...
}

//返回到homeNewsViewController
if (_suspensionButton.tag == JFSuspensionButtonStyleBackType) {
//需要做的事情...
}

//返回到JFMenuView
if (_suspensionButton.tag == JFSuspensionButtonStyleBackType2) {
//需要做的事情...
}
}


3.3.3 上滑隐藏悬浮按钮,下滑显示悬浮按钮。

        在JFHomeViewController中实现UIScrollDelegate代理方法

#pragma mark --- UIScrollDelegate
/// 滚动时调用
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.y > _contentOffset_Y + 80) {
[self suspensionWithAlpha:0];
} else if (scrollView.contentOffset.y < _contentOffset_Y) {
[self suspensionWithAlpha:1];
}
}
/// 停止滚动时调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
_contentOffset_Y = scrollView.contentOffset.y;
}
/// 设置悬浮按钮view透明度,以此显示和隐藏悬浮按钮
- (void)suspensionWithAlpha:(CGFloat)alpha {
[UIView animateWithDuration:0.3 animations:^{
[self.jfSuspensionView setAlpha:alpha];
}];
}


3.3 菜单(JFMenuView)界面布局

        下图层级关系对照源码看,清晰明了!(JFNewsClassificationView层级关系和JFMenuView一样)

iOS-高仿优雅的好奇心日报项目详解_数据_10


        懒加载模糊层:

- (UIVisualEffectView *)blurEffectView {
if (!_blurEffectView) {
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
_blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
_blurEffectView.frame = self.frame;
}
return _blurEffectView;
}


弹簧动画效果


        弹簧动画效果是用Facebook开源的pop动画引擎,简单使用的话推荐看:POP介绍与使用实践(快速上手动画),下面这段代码就是实例化了一个弹簧动画(POPPropertyAnimation),用来实现菜单界面的弹出效果。

/** pop动画
* POPPropertyAnimation 动画属性
* view 动画对象
* offset 偏移量
* speed 动画速度
*/
- (void)popAnimationWithView:(UIView *)view Offset:(CGFloat)offset speed:(CGFloat)speed {
POPSpringAnimation *popSpring = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionY];
popSpring.toValue = @(view.center.y + offset);
popSpring.beginTime = CACurrentMediaTime();
popSpring.springBounciness = 11.0f;
popSpring.springSpeed = speed;
[view pop_addAnimation:popSpring forKey:@"positionY"];
}


        菜单的隐藏动画使用的是苹果提供的UIView的动画,如下隐藏菜单的顶部View和底部View。

/// 动画隐藏headerView和footerView
- (void)hideMenuViewAnimation {
[UIView animateWithDuration:0.1 animations:^{
[self headerViewOffsetY:-KHeaderViewH];
[self footerViewOffsetY:JFSCREENH_HEIGHT];
} completion:^(BOOL finished) {
//隐藏JFMenuView
[self setHidden:YES];
}];
}


        如上用原生的UIView动画加pop动画引擎就可以实现悬浮按钮和菜单view的弹簧效果,若想动画效果更加自然,是需要耐心的调整pop动画引擎属性。




        


iOS-高仿优雅的好奇心日报项目详解_数据_11