高仿今日頭條App

項目地址github: https://github.com/tianliangyihou/headlineNews
如果您覺得不錯莱睁,記得給一個star??

采用了MVVM + RAC的方式,對微頭條界面 使用YYCache 進行了本地緩存
效果跟目前最新的今日頭條有些地方不一樣,因為今日頭條最近更新了新版本

  下面對項目中的一些效果和實現(xiàn)思路做下介紹

  如果您有什么問題或者建議,歡迎在簡書下面留言或者在github上issue me

網(wǎng)絡(luò)請求

以首頁的頂部的菜單欄為例


WX20180123.png

開始網(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;
           }];
       }];

首頁圖片的展示

WX20180123.png
 這個界面上展示的圖片都是webp格式的. SDWebImage需要下載一個依賴庫才能支持
SDWebImage 文檔上關(guān)于如何加載webp格式圖片的介紹很簡單 : 
 pod 'SDWebImage/WebP'
但是當(dāng)你實際操作起來,這個東西是一直下載不下來的,即使你開了vpn
你可以看下這個連接的內(nèi)容: http://www.reibang.com/p/4468f03cf606
如果還是下載不下來,就還需要調(diào)整一些東西,可以在簡書下面給我留言.

編輯頻道界面

effect2.gif

編輯頻道界面主要是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圖標的切換和動畫效果

effect3.gif
- (void)addAnnimation {
    // 這里使用了 私有API 但是審核仍可以通過 有現(xiàn)成的案例
    UIControl *tabBarButton = [_homeNav.tabBarItem valueForKey:@"view"];
    UIImageView *tabBarSwappableImageView = [tabBarButton valueForKey:@"info"];
    [tabBarSwappableImageView rotationAnimation];
    _swappableImageView = tabBarSwappableImageView;
    [self.tabBar hideBadgeOnItemIndex:0];
}

首頁頂部菜單欄的效果

effect4.gif
這里采用了WMPageController,但是并不能完全滿足需求,對其源碼做了一些修改
1 邊角的+號按鈕的半透明效果
2  當(dāng)某個菜單欄處于選中狀態(tài)下,再次點擊的刷新效果

詳情可參考項目: https://github.com/tianliangyihou/headlineNews

視頻的播放

effect5.gif
這里采用了ZFPlayer,github上一個有4000star的開源庫

ZFPlayer github地址 https://github.com/renzifeng/ZFPlayer

微頭條的實現(xiàn)頂部的隱藏效果

effect6.gif
 這里采用了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;
        }
    }];

微頭條的圖片瀏覽效果

effect7.gif
這里采用了LBPhotoBrowser,這個是本人開發(fā)的一個圖片瀏覽器.
1 支持gif圖片播放(2種方式)
2 對圖片進行預(yù)加載

關(guān)于LBPhotoBrowser可查看: https://github.com/tianliangyihou/LBPhotoBrowser

微頭條的文字的內(nèi)容中 @ # 鏈接 內(nèi)容的識別, 以及文字過長添加全文按鈕

effect8.gif
  這里采用了正則匹配與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/

微頭條的點贊動畫

effect9.gif
采用了粒子動畫

- (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 動畫

effect10.gif
- (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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市祥款,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖蛋逾,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凝垛,死亡現(xiàn)場離奇詭異懊悯,居然都是意外死亡,警方通過查閱死者的電腦和手機梦皮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門炭分,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人剑肯,你說我怎么就攤上這事捧毛。” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵呀忧,是天一觀的道長型将。 經(jīng)常有香客問我,道長荐虐,這世上最難降的妖魔是什么七兜? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮福扬,結(jié)果婚禮上腕铸,老公的妹妹穿的比我還像新娘。我一直安慰自己铛碑,他們只是感情好狠裹,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著汽烦,像睡著了一般涛菠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撇吞,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天俗冻,我揣著相機與錄音,去河邊找鬼牍颈。 笑死迄薄,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的煮岁。 我是一名探鬼主播讥蔽,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼画机!你這毒婦竟也來了冶伞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤步氏,失蹤者是張志新(化名)和其女友劉穎响禽,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體戳护,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡金抡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了腌且。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梗肝。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖铺董,靈堂內(nèi)的尸體忽然破棺而出巫击,到底是詐尸還是另有隱情禀晓,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布坝锰,位于F島的核電站粹懒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏顷级。R本人自食惡果不足惜凫乖,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望弓颈。 院中可真熱鬧帽芽,春花似錦、人聲如沸翔冀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纤子。三九已至搬瑰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間控硼,已是汗流浹背泽论。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留象颖,地道東北人佩厚。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像说订,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子潮瓶,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容

  • afinalAfinal是一個android的ioc陶冷,orm框架 https://github.com/yangf...
    passiontim閱讀 15,399評論 2 45
  • 現(xiàn)在是2016年12月31號,上午八點半毯辅。 今年最后一天獻給了xdf埂伦,回頭一想時間過得還真快,這是倒數(shù)第二次課了思恐。...
    阿橘很芒閱讀 425評論 0 0
  • 國足勝利了沾谜,雖然聽上去不太真實,但他還是贏了胀莹。 一直以來對足球都不感興趣基跑,能想到的最早和足球的交集也就是:上小學(xué)時...
    喵喵咪呀maria閱讀 109評論 0 0
  • 最近媳否,我頻繁看房,選房。 牛哥總是問我篱竭,你弄房子干嘛力图? 我說,當(dāng)辦公室呀掺逼。 牛哥說吃媒,你就在我辦公室待著就行了,咱倆...
    牛哥語錄閱讀 509評論 0 0
  • 6.BOM 1.window window.innerWidth window.innerHeight 頁面可視區(qū)...
    3hours閱讀 231評論 0 0