iOS-高仿優雅的好奇心日報
好奇心日報 關于好奇心有這樣一句話:
之所以叫好奇心日報,是因為我們認為好奇心是人類最美好的品質之一,我們篩選最有價值的信息,你能看到全球最有想法,最有關注的各界動態,以及他們背后的故事
正是出于好奇心偶然間下載安裝了這個 好奇心日報 ,打開這個APP首先吸引我的就是它簡潔干凈的界面和優雅的配色,整個首頁只有一個帶有logo的懸浮按鈕,然后就是帶有高清配圖不同與其他新聞平臺的新聞文章,圖標基本是簡單的黑、黃、白三種顏色加單線條圖形的設計。最后讓我下定決心仿它的是它那簡單到自然的動畫效果。說這么多當然不是給好奇心日報打廣告,只是想說我為什么寫這個項目;額,就這么簡單!
先看看 JFQDaily 效果:
如果你是一個iOS開發入門級的猿,有興趣話可以下載 JFQDaily源碼 然后結合本篇博客來看,相信你會有所收獲的,代碼寫的接地氣,注釋詳細!
一、準備工作
1、高仿,原生圖片圖標自然必不可少,利用 iOS images Extractor 抓取 好奇心日報 的圖片,如何使用iOS images Extractor抓取APP圖片,我的 iOS直播APP-點贊動畫的實現 這篇文章下面有介紹。
2、用青花瓷( Charles )提取密碼: kgya抓取 好奇心日報 數據。
3、篩選數據:
- GET請求,拼接url路徑,第一次獲取數據url是:
http://app3.qdaily.com/app3/homes/index/0.json? - 獲取last_key后上拉加載時GET的URL
http://app3.qdaily.com/app3/homes/index/1478819276_1478777270.json?
Paste_Image.png
上面是用chalers所抓取到的數據,分析并拿到你需要的數據就行,上面標注的都是項目中需要用到的數據,具體篩選過程就不再分析了,無非是打開 好奇心日報 看APP所展示的信息和抓到的數據做對比,找到相應的映射關系。如果你想走的更遠獨立行走是必要的。具體數據分析里的細節問題可以認真看源碼 Models 文件夾類的 數據模型 文件。好啦準備工作完成啦,開始動手建工程碼代碼吧!
二、項目文件結構
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搭建。
6、views:界面
- JFSuspensionView: 懸浮按鈕View
- JFHomeNewsTableViewCell: 首頁cell,繼承自UITableViewCell
- JFMenuView: 菜單界面
- JFNewsClassificationView 新聞分類界面繼承自UIView
7、pods:項目所用到的第三方框架
- Masonry :Masonry是目前最流行的AutoLayout框架。
- AFNetworking :是一個非常方便的網絡請求庫,可以輕松實現各種網絡請求,比如經常使用的GET請求、POST請求等。
- MJRefresh :李明杰老師寫的下拉刷新框架,使用方法很簡單。
- MJExtension :json數據轉模型的框架,也是李明杰老師寫的,用法都很簡單。
- SDWebImage :目前最受歡迎的圖片下載第三方框架,使用率很高。
- MBProgressHUD :是一個顯示HUD窗口的第三方類庫,用法簡單。
各框架的具體用法網上很多資料,不再贅述!
三、代碼
1、搭建數據模型
這是我們用chalers抓取的json格式的數據,根據 MJExtension 的使用方法建立數據模型,先分別創建JFResponseModel、JFFeedsModel、JFPostModel、JFCategoryModel四個類,繼承自NSObject。
然后我們慢慢從json數據的最里層向外層聲明你所 需要 的對應參數,切記參數可以自行選擇創建,但是模型的類型一個不能少,參數名一定要一樣,如果命名有沖突 MJExtension 提供的有相應的方法進行映射。例如:將沖突的參數 description 名映射到 subhead
/** 設置模型屬性名和字典key之間的映射關系 */
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
/* 返回的字典,key為模型屬性名,value為轉化的字典的多級key */
return @{@"subhead":@"description"};
}
1.1 JFCategoryModel數據模型
#import
@interface JFCategoryModel : NSObject
/** 新聞類型(設計、娛樂、智能等)*/
@property (nonatomic, copy) NSString *title;
@end
1.2 JFPostModel數據模型
#import
@class JFCategoryModel;
@interface JFPostModel : NSObject
/** 新聞標題*/
@property (nonatomic, copy) NSString *title;
/** 副標題*/
@property (nonatomic, copy) NSString *subhead;
/** 出版時間*/
@property (nonatomic, assign) NSIntegerpublish_time;
/** 配圖*/
@property (nonatomic, copy) NSString *image;
/** 評論數*/
@property (nonatomic, assign) NSIntegercomment_count;
/** 點贊數*/
@property (nonatomic, assign) NSIntegerpraise_count;
/** 新聞文章鏈接(html格式)*/
@property (nonatomic, copy) NSString *appview;
@property (nonatomic, strong) JFCategoryModel *category;
@end
1.3 JFFeedsModel數據模型
#import
@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"
@implementationJFPostModel
/** 設置模型屬性名和字典key之間的映射關系 */
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
/* 返回的字典,key為模型屬性名,value為轉化的字典的多級key */
return @{@"subhead":@"description"};
}
@end
1.4 JFResponseModel數據模型
#import
@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
typedef void(^JFHomeNewsDataManagerBlock)(iddata);
@interface JFHomeNewsDataManager : NSObject
// 請求數據成功后返回新聞數據回調的block
@property (nonatomic, copy) JFHomeNewsDataManagerBlocknewsDataBlock;
// 請求新聞數據
- (void)requestHomeNewsDataWithLastKey:(NSString *)lastKey;
- (void)newsDataBlock:(JFHomeNewsDataManagerBlock)block;
@end
JFHomeNewsDataManager.m
#import "JFHomeNewsDataManager.h"
#import
#import "JFConfigFile.h"
#define kTimeOutInterval 10
@implementationJFHomeNewsDataManager
#pragma mark - 創建請求者
- (AFHTTPSessionManager *)manager {
AFHTTPSessionManager *manager = [AFHTTPSessionManagermanager];
manager.requestSerializer.timeoutInterval = kTimeOutInterval;
manager.responseSerializer = [AFHTTPResponseSerializerserializer];
//設置相應內容類(這里根據所請求的數據類型可自行選擇)
manager.responseSerializer.acceptableContentTypes = [NSSetsetWithObjects:@"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 = [NSStringstringWithFormat:@"http://app3.qdaily.com/app3/homes/index/%@.json?",lastKey];
[managerGET:urlStringparameters:nilprogress:^(NSProgress * _NonnulldownloadProgress) {
} success:^(NSURLSessionDataTask * _Nonnulltask, id _NullableresponseObject) {
// JSON數據轉字典
NSDictionary *dataDictionary = [NSJSONSerializationJSONObjectWithData:responseObjectoptions:NSJSONReadingMutableContainerserror:nil];
if (self.newsDataBlock) {
self.newsDataBlock([dataDictionaryvalueForKey:@"response"]);
}
} failure:^(NSURLSessionDataTask * _Nullabletask, NSError * _Nonnullerror) {
}];
}
- (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里的內容:
response里的數據是我們需要處理的,所以用 valueForKey: 方法拿到其數據返回給JFHomeViewController:
[dataDictionaryvalueForKey:@"response"]
數據處理部分到此算是告一段落,接下來就是要把數據展示到界面上,然后實現好奇心日報的交互動畫。
3、UI布局
3.1 項目中實現的三種不同的cell樣式
cellType對應的就是數據模型JFFeedsModel中的type
- cellType = 0,UITableViewCell的樣式是上方新聞配圖、然后是新聞標題,最下面是副標題。
- cellType = 1,UITableViewCell的樣式是新聞配圖在右側,左側是新聞標題,在其下面是新聞種類、評論數和點贊數。
cellType = 2,UITableViewCell的樣式和cellType = 0時基本一致,就多了最下面的新聞種類、評論數和點贊數。
顯然用蘋果提供的cell是不行的,所以創建JFHomeNewsTableViewCell繼承自UITableViewCell,然后我們來自定義cell。
JFHomeNewsTableViewCell.h文件中聲明屬性:
#import
@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) NSIntegertime;
@end
動態的設置cell的樣式(frame)是在 - (void)layoutSubviews; 方法中,這里我把相關代碼都放在了 - (void)customUI; 方法中,這里使用了 Masonry 自動布局。
- (void)layoutSubviews {
[super layoutSubviews];
[self customUI];
}
使用 Masonry 時有一點一定要 注意 ,必須先把子控件添加到父控件上才能用 Masonry 去自動布局,否則父控件上沒有相應的子控件何談布局呢。
在JFHomeViewController.m文件中重寫UITableView的 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; 代理方法來動態設置cell的高度;同樣是根據類型判斷。
/// 根據cell類型返回cell高度
- (CGFloat)tableView:(UITableView *)tableViewheightForRowAtIndexPath:(NSIndexPath *)indexPath {
JFHomeNewsTableViewCell *cell = self.cell;
if ([cell.cellTypeisEqualToString:@"0"]) {
return 330;
}else if ([cell.cellTypeisEqualToString:@"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.viewaddSubview:self.homeNewsTableView];
[self.viewaddSubview:self.jfSuspensionView];
}
3.2.2 在JFSuspension.h文件中用枚舉定義了懸浮按鈕的四種Tag類型和四種block回調方法,其實四種Tag類型一一對應四個回調函數,通過判斷Tag類型來執行相應的block函數。
#import
/// 懸浮按鈕種類(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) NSIntegerJFSuspensionButtonStyle;
/** 彈出菜單界面*/
@property (nonatomic, copy) JFSuspensionViewBlockpopupMenuBlock;
/** 關閉菜單界面*/
@property (nonatomic, copy) JFSuspensionViewBlockcloseMenuBlock;
/** 返回到homeNewsViewController*/
@property (nonatomic, copy) JFSuspensionViewBlockbackBlock;
/** 返回到JFMenuView*/
@property (nonatomic, copy) JFSuspensionViewBlockbackToMenuViewBlock;
- (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
3.3 菜單(JFMenuView)界面布局
下圖層級關系對照源碼看,清晰明了!(JFNewsClassificationView層級關系和JFMenuView一樣)
懶加載模糊層:
- (UIVisualEffectView *)blurEffectView {
if (!_blurEffectView) {
UIBlurEffect *blurEffect = [UIBlurEffecteffectWithStyle:UIBlurEffectStyleDark];
_blurEffectView = [[UIVisualEffectViewalloc] initWithEffect:blurEffect];
_blurEffectView.frame = self.frame;
}
return _blurEffectView;
}
四、彈簧動畫效果
彈簧動畫效果是用 非死book 開源的 pop動畫引擎 ,簡單使用的話推薦看: POP介紹與使用實踐(快速上手動畫) ,下面這段代碼就是實例化了一個彈簧動畫(POPPropertyAnimation),用來實現菜單界面的彈出效果。
/** pop動畫
* POPPropertyAnimation 動畫屬性
* view 動畫對象
* offset 偏移量
* speed 動畫速度
*/
- (void)popAnimationWithView:(UIView *)viewOffset:(CGFloat)offsetspeed:(CGFloat)speed {
POPSpringAnimation *popSpring = [POPSpringAnimationanimationWithPropertyNamed:kPOPLayerPositionY];
popSpring.toValue = @(view.center.y + offset);
popSpring.beginTime = CACurrentMediaTime();
popSpring.springBounciness = 11.0f;
popSpring.springSpeed = speed;
[viewpop_addAnimation:popSpringforKey:@"positionY"];
}
菜單的隱藏動畫使用的是蘋果提供的UIView的動畫,如下隱藏菜單的頂部View和底部View。
/// 動畫隱藏headerView和footerView
- (void)hideMenuViewAnimation {
[UIViewanimateWithDuration:0.1 animations:^{
[self headerViewOffsetY:-KHeaderViewH];
[self footerViewOffsetY:JFSCREENH_HEIGHT];
} completion:^(BOOL finished) {
//隱藏JFMenuView
[self setHidden:YES];
}];
}
如上用原生的UIView動畫加 pop動畫引擎 就可以實現懸浮按鈕和菜單view的彈簧效果,若想動畫效果更加自然,是需要耐心的調整 pop動畫引擎 屬性。
總結:這篇文章不是一個細致講解這個項目的文檔,結合這篇文章去看 JFQDaily源碼 相信你會有所收獲的,畢竟是三天寫出來的東西,代碼可能有不規范的地方,歡迎指出糾正,出于好奇心和喜歡,一氣呵成的寫完了主要功能。
來自:http://ios.jobbole.com/90974/