項目地址github: https://github.com/tianliangyihou/headlineNews
如果您覺得不錯莱睁,記得給一個star??
采用了MVVM + RAC的方式,對微頭條界面 使用YYCache 進行了本地緩存
效果跟目前最新的今日頭條有些地方不一樣,因為今日頭條最近更新了新版本
下面對項目中的一些效果和實現(xiàn)思路做下介紹
如果您有什么問題或者建議,歡迎在簡書下面留言或者在github上issue me
網(wǎng)絡(luò)請求
以首頁的頂部的菜單欄為例
開始網(wǎng)絡(luò)請求
- (void)viewDidLoad {
[super viewDidLoad];
HNNavigationBar *bar = [self showCustomNavBar];
[bar.searchSubjuct subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
}];
[self configUI];
@weakify(self)
[[self.titleViewModel.titlesCommand execute:@13] subscribeNext:^(id _Nullable x) {
@strongify(self);
self.models = x;
[self reloadData];
[self configPageVC];
}];
}
HNHomeTitleViewModel 中網(wǎng)絡(luò)請求的處理
這里對網(wǎng)絡(luò)請求的處理,相對于正常對AFNetWorking的封裝,又進行了進一步的封裝.
可以參考鏈接 http://www.reibang.com/p/1f5cd52981a1
_titlesCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
HNHomeTitleRequest *request = [HNHomeTitleRequest netWorkModelWithURLString:HNURLManager.homeTitleURLString isPost:NO];
request.iid = HN_IID;
request.device_id = HN_DEVICE_ID;
request.aid = [input intValue];
[request sendRequestWithSuccess:^(id response) {
NSDictionary *responseDic = (NSDictionary *)response;
responseDic = responseDic[@"data"];
NSMutableArray *models = [NSMutableArray array];
if (responseDic.count > 0) {
NSArray *dicArr = responseDic[@"data"];
for (int i = 0; i < [dicArr count]; i++) {
HNHomeTitleModel *model = [[HNHomeTitleModel new] mj_setKeyValues:dicArr[i]];
[models addObject:model];
}
[subscriber sendNext:models];
[subscriber sendCompleted];
}else {
[MBProgressHUD showError: HN_ERROR_SERVER toView:nil];
}
} failure:^(NSError *error) {
// do something
}];
return nil;
}];
}];
首頁圖片的展示
這個界面上展示的圖片都是webp格式的. SDWebImage需要下載一個依賴庫才能支持
SDWebImage 文檔上關(guān)于如何加載webp格式圖片的介紹很簡單 :
pod 'SDWebImage/WebP'
但是當(dāng)你實際操作起來,這個東西是一直下載不下來的,即使你開了vpn
你可以看下這個連接的內(nèi)容: http://www.reibang.com/p/4468f03cf606
如果還是下載不下來,就還需要調(diào)整一些東西,可以在簡書下面給我留言.
編輯頻道界面
編輯頻道界面主要是frame的計算,這個是一個消耗cpu的行為,可以在異步線程完成
故把消耗性能的frame計算等 都放在了后臺線程,等后臺線程計算完畢 在主線程更新UI
創(chuàng)建一個同步隊列,用來專門處理frame相關(guān)的計算
_queue = dispatch_queue_create("com.headlineNews.queue", DISPATCH_QUEUE_SERIAL);
比如 長按后 交換兩個按鈕的位置
#pragma mark - 交換兩個按鈕的位置
- (void)adjustCenterForBtn:(HNButton *)btn withGes:(UILongPressGestureRecognizer *)ges{
CGPoint newPoint = [ges locationInView:self];
btn.center = newPoint;
__weak typeof(self) wself = self;
[self newLocationTagForBtn:btn locationBlock:^(HNChannelModel* targetModel) {
if (wself.divisionModel == btn.model) {
HNChannelModel *divisionModel = self.datas[btn.model.tag - 1];
_divisionModel = divisionModel;
}else if (wself.divisionModel == targetModel){
_divisionModel = btn.model;
}
[wself.datas removeObject:btn.model];
[wself.datas insertObject:btn.model atIndex:targetModel.tag];
for (int i = 0 ; i < wself.datas.count; i++) {
HNChannelModel *model = wself.datas[i];
model.tag = i;
if (model.isMyChannel && model != btn.model) {
model.frame = MYCHANNEL_FRAME(i);
}
}
dispatch_async(dispatch_get_main_queue(), ^{
for (int i = 0 ; i < wself.datas.count; i++) {
HNChannelModel *model = wself.datas[i];
if (model.isMyChannel && model != btn.model) {
[UIView animateWithDuration:0.25 animations:^{
model.btn.frame = model.frame;
}];
}
}
});
}];
}
- (void)newLocationTagForBtn:(HNButton *)moveBtn locationBlock:(void(^)(HNChannelModel* targetModel))locationBlock {
HNChannelModel *moveBtnModel = moveBtn.model;
CGPoint moveBtnCenter = moveBtn.center;
dispatch_async(_queue, ^{
NSMutableArray *models = [[NSMutableArray alloc]initWithArray:self.datas];
for (HNChannelModel *model in models) {
if (model == moveBtnModel) {
continue;
}
if (!model.isMyChannel) {
continue;
}
if (CGRectContainsPoint(model.frame,moveBtnCenter)) {
locationBlock(model);
}
}
});
}
首頁tabber圖標的切換和動畫效果
- (void)addAnnimation {
// 這里使用了 私有API 但是審核仍可以通過 有現(xiàn)成的案例
UIControl *tabBarButton = [_homeNav.tabBarItem valueForKey:@"view"];
UIImageView *tabBarSwappableImageView = [tabBarButton valueForKey:@"info"];
[tabBarSwappableImageView rotationAnimation];
_swappableImageView = tabBarSwappableImageView;
[self.tabBar hideBadgeOnItemIndex:0];
}
首頁頂部菜單欄的效果
這里采用了WMPageController,但是并不能完全滿足需求,對其源碼做了一些修改
1 邊角的+號按鈕的半透明效果
2 當(dāng)某個菜單欄處于選中狀態(tài)下,再次點擊的刷新效果
詳情可參考項目: https://github.com/tianliangyihou/headlineNews
視頻的播放
這里采用了ZFPlayer,github上一個有4000star的開源庫
ZFPlayer github地址 https://github.com/renzifeng/ZFPlayer
微頭條的實現(xiàn)頂部的隱藏效果
這里采用了rac 監(jiān)控tableView的滑動
CGFloat tableViewHeight = HN_SCREEN_HEIGHT - HN_NAVIGATION_BAR_HEIGHT - HN_TABBER_BAR_HEIGHT - 40;
@weakify(self);
[RACObserve(self.tableView, contentOffset) subscribeNext:^(id x) {
@strongify(self);
CGPoint contentOffset = [x CGPointValue];
if (contentOffset.y > 0) {
optionView.top = contentOffset.y <= 40 ? -contentOffset.y : -40;
self.tableView.top = floorf(contentOffset.y <= 40 ? 40 - contentOffset.y : 0);
self.tableView.height = floorf(contentOffset.y <= 40 ? tableViewHeight + contentOffset.y : tableViewHeight + 40);
}else {
optionView.top = 0;
self.tableView.top = 40;
self.tableView.height = tableViewHeight;
}
}];
微頭條的圖片瀏覽效果
這里采用了LBPhotoBrowser,這個是本人開發(fā)的一個圖片瀏覽器.
1 支持gif圖片播放(2種方式)
2 對圖片進行預(yù)加載
關(guān)于LBPhotoBrowser可查看: https://github.com/tianliangyihou/LBPhotoBrowser
微頭條的文字的內(nèi)容中 @
#
鏈接
內(nèi)容的識別, 以及文字過長添加全文
按鈕
這里采用了正則匹配與YYText結(jié)合的方式,例如對于 #話題#
// #話題#的規(guī)則
NSError *topicError;
NSString *topicPattern = @"#[0-9a-zA-Z\\u4e00-\\u9fa5]+#";
NSRegularExpression *topicRegex = [NSRegularExpression regularExpressionWithPattern:topicPattern
options:NSRegularExpressionCaseInsensitive
error:&topicError];
NSArray *topicMatches = [topicRegex matchesInString:_hn_content.string options:0 range:NSMakeRange(0, [_hn_content.string length])];
for (NSTextCheckingResult *match in topicMatches)
{
if (match.range.location == NSNotFound ) continue;
[_hn_content yy_setColor:hn_cell_link_nomalColor range:match.range];
// 高亮狀態(tài)
YYTextHighlight *highlight = [YYTextHighlight new];
[highlight setBackgroundBorder:highlightBorder];
[highlight setColor:hn_cell_link_hightlightColor];
// 數(shù)據(jù)信息遂蛀,用于稍后用戶點擊
highlight.userInfo = @{hn_topic : [_hn_content.string substringWithRange:NSMakeRange(match.range.location + 1, match.range.length - 1)]};
[_hn_content yy_setTextHighlight:highlight range:match.range];
}
保證微頭條界面流暢性
當(dāng)網(wǎng)絡(luò)請求結(jié)束后,解析數(shù)據(jù)為多個model
cell的上內(nèi)容怎么顯示,都應(yīng)該是由model決定的,對應(yīng)每個cell創(chuàng)建一個HNMicroLayout, 在后臺線程中完成 #xxxx # @ 鏈接的匹配
以及各個控件在cell中的布局信息.計算完成后 在主線程更新UI.
- (instancetype)init
{
self = [super init];
if (self) {
@weakify(self);
[[HNDiskCacheHelper defaultHelper] setMaxArrayCount:9 forKey:cacheKey];
_microHeadlineCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
@strongify(self)
[self requestWithSubscriber:subscriber input:input];
return nil;
}];
}];
}
return self;
}
- (void)requestWithSubscriber:(id<RACSubscriber>)subscriber input:(id)input{
HNMicroHeadlineRequest *request = [HNMicroHeadlineRequest netWorkModelWithURLString:HNURLManager.microHeadlineURLString isPost:NO];
request.iid = HN_IID;
request.device_id = HN_DEVICE_ID;
request.count = @15;
request.category = @"weitoutiao";
@weakify(self);
[request sendRequestWithSuccess:^(id response) {
@strongify(self);
// --> 復(fù)雜的模型處理應(yīng)該放在異步
dispatch_async(dispatch_get_global_queue(0, 0), ^{
HNMicroHeadlineModel *model = [[HNMicroHeadlineModel alloc]init];
[model mj_setKeyValues:response];
[model.data makeObjectsPerformSelector:@selector(detialModel)];
NSMutableArray *layouts = [[NSMutableArray alloc]init];
[model.data enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
HNMicroHeadlineSummaryModel *model = (HNMicroHeadlineSummaryModel *)obj;
HNMicroLayout *layout = [[HNMicroLayout alloc]initWithMicroHeadlineModel:model];
[layouts addObject:layout];
}];
HN_ASYN_GET_MAIN(
if ([model.message isEqualToString:@"success"]) {
[self setCacheLayouts:layouts withRefresh:input];
[subscriber sendNext:layouts];
[subscriber sendCompleted];
}else {
[MBProgressHUD showError:HN_ERROR_SERVER toView:nil];
}
);
});
} failure:^(NSError *error) {
// do something
[subscriber sendError:error];
}];
}
YYKit 作者寫的關(guān)于界面流暢性的技巧:
https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/
微頭條的點贊動畫
采用了粒子動畫
- (void)setUp {
for (int i = 0; i < sendCountEveryTime; i++) {
CAEmitterCell *cell = [CAEmitterCell emitterCell];
cell.name = [NSString stringWithFormat:@"explosion_%d",i];
cell.alphaRange = 0.5;
cell.alphaSpeed = -0.5;
cell.lifetime = 4;
cell.lifetimeRange = 2;
cell.velocity = 600;
cell.velocityRange = 200.00;
cell.scale = 0.5;
cell.yAcceleration = 600;
cell.emissionLongitude = 2 *M_PI - M_PI /4.0;
cell.emissionRange = M_PI / 2.0;
[self.cells addObject:cell];
}
}
- (void)showEmitterCellsWithImages:(NSArray<UIImage *>*)images withShock:(BOOL)shouldShock onView:(UIView *)view{
for (int i = 0; i< images.count; i++) {
CAEmitterCell *cell = self.cells[i];
cell.contents = (__bridge id _Nullable)(images[i].CGImage);
}
CAEmitterLayer *layer = [CAEmitterLayer layer];
layer.name = @"emitterLayer";
layer.position = CGPointMake(view.frame.size.width/2.0, view.frame.size.height/2.0);
layer.emitterCells = self.cells;
[view.layer addSublayer:layer];
[self explodeWithView:view andLayer:layer];
}
全屏pop 動畫
- (void)addCustomGesPop {
id target = self.interactivePopGestureRecognizer.delegate;
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:target action:@selector(handleNavigationTransition:)];
_pan = pan;
pan.delegate = self;
[self.view addGestureRecognizer:pan];
self.interactivePopGestureRecognizer.enabled = NO;
}
以及在特定界面關(guān)掉這個效果
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
HNNavigationController *nav = (HNNavigationController *)self.navigationController;
[nav stopPopGestureRecognizer];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
HNNavigationController *nav = (HNNavigationController *)self.navigationController;
[nav startPopGestureRecognizer];
}
以上是項目中部分效果的效果圖. 但是感覺已經(jīng)夠長了
完整效果及效果圖請查看: https://github.com/tianliangyihou/headlineNews