前言
很早前就想和大家分享一些真正項目的開發(fā)思路和流程,一直沒能鼓起勇氣寫,畢竟這不是一件輕松的事情,一個月前,公司的項目上線,鼓起勇氣利用每天的休息時間寫了這個半成品的項目,基本的UI與邏輯都打通了,剩下的細節(jié)需要時間修改,由于下周工作有新需求需要開發(fā),可能很長一段時間沒精力來寫了,本想把所有的功能都實現(xiàn),并且修改掉發(fā)現(xiàn)的bug后再給大家學習的,由于工作的情況,可能會延緩2個月左右的日子(后續(xù)我會把功能陸續(xù)實現(xiàn)),代碼我傳到github上了,相信讀者會發(fā)現(xiàn)很多的不足之處,希望大家可以自行嘗試修改一些bug和實現(xiàn)剩余的部分功能,說實話一直是每天晚上8點寫到凌晨3點左右,因為有些朋友不希望看見過多的三方庫,所以基本非常耗時的需求我都盡力自己封裝的,當然我寫的有很多不足之處,希望大家可以指出不足之處,共同進步,由于時間比較匆忙,我的code一次review都沒,望大家包涵,項目使用OC寫的,算是對OC的一個紀念吧。等忙完這段時間,我會再用swift來寫一個新的項目分享給大家,喜歡的朋友可以繼續(xù)關(guān)注我的博客,我會第一時間將新項目到博客上
這篇博客是配合代碼來寫的,大部分的圖片都是我自己扣下來的,數(shù)據(jù)也是我直接寫的假數(shù)據(jù),在代碼中基本每一步我都有詳細的注釋,嘮叨的有點多,下面就先展示下我仿的這個項目吧,有興趣的朋友可以下一下原版的app參照一下(寫的過程中app更新了一點新功能)
項目展示,由于沒有數(shù)據(jù),所以所有的cell顯示的都是我自己寫的數(shù)據(jù)











還有很多細節(jié)就不一一展示了,大家將代碼運行下自己查看即可
由于內(nèi)容比較多,我就按功能模塊來介紹給大家了
首先是左邊抽屜的效果以及點擊按鈕切換控制器
- 這里值得注意的是根據(jù)蘋果推薦的使用方法是,當一個控制器的View是另一控制器view的子控件,那么這個控制器也最好也是另一個控制器的子控制器例如:
UIViewController *testVC = [UIViewController new];
[self.view addSubview:testVC.view];
[self addChildViewController:testVC];
- 這里左邊的view實際上相當于自己定義一個和系統(tǒng)UITabBarController差不多功能的控件,在最底層有一個控制器(后面稱之為主控制器),將左邊的按鈕view添加到主控制器的view上,創(chuàng)建好右邊有所的控制器(首頁,發(fā)現(xiàn),消息,設(shè)置...)并且將每個右邊控制器包裝一個導航控制器,將導航控制器按序添加給主控制器做子控制器,默認情況下將首頁的導航控制器的view添加給主控制器的view子控件(
這樣就會默認顯示首頁),根據(jù)左邊按鈕的點擊事件通過代理方法通知主控制器哪個按鈕被選中.將舊控制器的view從父視圖中移除,將新的view添加到主視圖的view,并且用一個臨時屬性記錄之前選中的控制器,這樣就完成了點擊按鈕切換不同的控制器,具體實現(xiàn)代碼如下
//暫時先做沒有登陸的情況的點擊
WNXNavigationController *newNC = self.childViewControllers[toIndex];
if (toIndex == WNXleftButtonTypeIcon) {
newNC = self.childViewControllers[fromIndex];
}
//移除舊的控制器view
WNXNavigationController *oldNC = self.childViewControllers[fromIndex];
[oldNC.view removeFromSuperview];
//添加新的控制器view
[self.view addSubview:newNC.view];
newNC.view.transform = oldNC.view.transform;
self.showViewController = newNC.childViewControllers[0];
這樣就完成了切換控制器
抽屜的效果是通過給導航控制器的view做形變動畫完成的
transform的X軸的位移以及整體的縮放效果,這里由于每個導航控制器的功能一樣,這里抽取了共同的特點封裝了一個基類導航控制器,基類擁有點擊左邊的按鈕完成變形和恢復的功能拖動手勢是給主控制器添加一個UIPanGestureRecognizer手勢(
稱為pan),當pan開始拖拽時,計算拖動的距離來按比例執(zhí)行動畫,根據(jù)手勢的狀態(tài)(pan.status ==UIGestureRecognizerStateEnded)停止時拖動的距離計算出該停留在哪里的位置,需要注意的是這里得記錄下當前導航控制器是處于哪種狀態(tài),給導航控制器自定義個屬性isScale來記錄,這里判斷很多,具體實現(xiàn)我在代碼中每一步都有注釋,參照代碼即可
首頁
- 首頁就是一個tableView就可以搞定,tableView的headView顏色和數(shù)據(jù)服務(wù)器會給返回,給每個headView添加一個點擊手勢,點擊push到下一個控制器,這里需要注意tableViewdelegate中headView的復用
headView的復用使用方法和cell差不多,也是系統(tǒng)自帶緩存池子,和cell一樣注冊一下根據(jù)identifier來拿取即可,導航條的顏色會和前一個headView的顏色一樣,這里由于我之前設(shè)置了導航控制器的主題
[UINavigationBar appearanceWhenContainedIn:self, nil]
- 所以不可以直接設(shè)置導航條的顏色了 ,然后我嘗試了設(shè)置navigationBar的背景色,設(shè)置navigationBar的setTintColor:
設(shè)置navigationBar.layer的背景色 以及根據(jù)顏色畫出navigationBar的背景圖片4種辦法都無法達到原生的效果
最后采用將navigationBar隱藏,自己放一個View了充當了導航條來解決這個問題(需要注意當切換控制器是對導航條隱藏屬性的設(shè)置)
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
//隱藏系統(tǒng)的導航條,由于需要自定義的動畫,自定義一個view來代替導航條
[self.navigationController setNavigationBarHidden:YES animated:YES];
}
發(fā)現(xiàn)
- 這個頁面是一個UICollectionView,里面有兩組數(shù)據(jù),每一組都一個一個headView,需要注意的就是cell的點擊事件,注意了下官方的做法是不論點擊了cell的哪個位置,都會使cell內(nèi)部的button進入高亮狀態(tài),這就需要用到事件的響應(yīng)鏈,在cell的內(nèi)部攔截整個cell的點擊事件都交給按鈕來做,具體代碼如下
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
/* 攔截事件響應(yīng)者,不論觸發(fā)了cell中的哪個控件都交給iconButton來響應(yīng) */
// 1.判斷當前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判斷點在不在當前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
return self.iconButton;
}
- 但這里需要注意這樣攔截cell的點擊事件,在collectionView的cell被點擊后didDeselectItemAtIndexPath:就不會被觸發(fā)了,我的解決方法是在點擊button時通過自定義代理方法通知collectionView所在的控制器,這樣外部就知道點擊了那個cell,便可以拿到cell的模型push到下一個控制器,并將cell的模型賦值給下一個控制器
-(void)iconButtonClick:(UIButton *)sender
{
//點擊button通知代理
if ([self.delegate respondsToSelector:@selector(foundCollectionViewCell:)]) {
[self.delegate foundCollectionViewCell:self];
}
}
登陸
- 登陸只用了微信登陸和新浪登陸,不涉及到注冊就非常簡單,好多公司都會要求要用原生的登陸,只需要去新浪和微信的官網(wǎng)將SDK下載下來,并且按照官方給的幫助文檔操作即可,如果公司沒用硬性要求的話,我一般使用友盟平臺的登陸,包括崩潰統(tǒng)計,三方登陸,分享,用戶分析等等(
這里提一嘴貌似QQ原生登陸必須寫在appDelegate中,別的都寫在哪里都行,遇到一次這個情況,有解決辦法的朋友可以告訴我)
消息
一樣這里也是tabelView,這里我個人的邏輯是將所有的未讀的消息存放到本地數(shù)據(jù)庫,每次點擊刪除一條,將本地的數(shù)據(jù)刪除一條,有新的消息時直接寫入到數(shù)據(jù)庫
當點擊刪除全部的時候,就清空本地的數(shù)據(jù)清空,下次接受的服務(wù)器的數(shù)據(jù)在重新寫入到數(shù)據(jù)庫,每次點擊消息頁面時去數(shù)據(jù)庫查看是否有未讀消息,如果有未讀數(shù)據(jù),變顯示編輯按鈕和cell
因為是模擬的數(shù)據(jù),為了保障每次進來都有數(shù)據(jù),就沒有實現(xiàn)歸檔解檔的操作,所以每次刪除后重新進入會再次有數(shù)據(jù)
這里記錄編輯按鈕的狀態(tài),讀取本地是否有未讀消息
數(shù)組的個數(shù),如果有就顯示編輯按鈕,記錄編輯按鈕的狀態(tài),如果是選中狀態(tài)就隱藏cell中箭頭圖片,顯示刪除按鈕,并且將刪除按鈕的狀態(tài)設(shè)為選中,這樣就可以切換按鈕的文字了,將本地的數(shù)據(jù)數(shù)組刪除掉并且刷新tableView,這里用的是刪除動畫,需要注意刪除的順序
[self.datas removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationRight];
//加入延時調(diào)用是防止刪除后過快的就刷新tableView
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
- 底部的刪除完全交給編輯按鈕來控制,選中狀態(tài)時出現(xiàn),非選中狀態(tài)時消失,點擊刪除全部,直接將數(shù)組中的模型全部移除,然后清空數(shù)據(jù)庫里的數(shù)據(jù),刷新tableView
搜索

這個也需要持久化存儲來記錄用戶的操作
由于數(shù)據(jù)量比較小直接用plist即可,每次頁面彈出后,先從本地讀取用戶歷史搜索的數(shù)據(jù),用戶每次刪除或者新輸入搜索框內(nèi)容時也直接寫入到本地的caches文件中設(shè)置textFiled的代理,監(jiān)聽用戶的完成輸入,判斷textFiled的text是否有長度,如果有的話發(fā)送給服務(wù)器響應(yīng)的請求,并且將用戶數(shù)據(jù)的string保存到本地的plist中
這里需要提一下關(guān)于熱門按鈕的布局,因為熱門的文字長度不一樣,但每次只有4個按鈕,在xib中先將按鈕的位置約束好,不過寬度的約束需要倆個,一個是>= 和<= 然后根據(jù)服務(wù)器返回的實際長度在設(shè)置按鈕title時,計算出每個按鈕的真實寬度,根據(jù)真實寬度算出間距是多少,重新布局一次按鈕的位置
(void)setHotDatas:(NSMutableArray *)hotDatas
{
_hotDatas = hotDatas;
//判斷是長度是否是4,開發(fā)中可以這樣寫 應(yīng)該服務(wù)器返回幾條數(shù)據(jù)就賦值多少,而不是固定的寫死數(shù)據(jù),
//萬一服務(wù)器返回的數(shù)據(jù)有錯誤,會造成用戶直接閃退的,有
//時在某些不是很重要的東西無法確定返回的是否正確,建議用
//@try @catch來處理,
//即便返回的數(shù)據(jù)有誤,也可以讓用戶繼續(xù)別的操作,
//而不會在無關(guān)緊要的小細節(jié)上造成閃退
if (hotDatas.count == 4) {
[self.hotButton1 setTitle:hotDatas[1] forState:UIControlStateNormal];
[self.hotButton2 setTitle:hotDatas[0] forState:UIControlStateNormal];
[self.hotButton3 setTitle:hotDatas[2] forState:UIControlStateNormal];
[self.hotButton4 setTitle:hotDatas[3] forState:UIControlStateNormal];
}
[self layoutIfNeeded];
//算出間距
CGFloat margin = (WNXAppWidth - 40 -
self.hotButton1.bounds.size.width -
self.hotButton2.bounds.size.width -
self.hotButton3.bounds.size.width -
self.hotButton3.bounds.size.width) / 3;
//更新約束
[self.hotButton2 updateConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.hotButton1.right).offset(margin);
}];
[self.hotButton3 updateConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.hotButton2.right).offset(margin);
}];
[self.hotButton4 updateConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.hotButton3.right).offset(margin);
}];
}
模糊效果

- 這里由于圖片的質(zhì)量被壓縮的太厲害,實際的效果還不錯,這個功能是iOS8新開放的新接口,有倆個圖片特效可以使用,一個是模糊blur,一個是顏色疊加(
類似于PS中圖片疊加的效果,不過就3種效果) - 封裝一個blurView,blurView的子控件有一個collectionView做用戶選擇用,一個imageView做模糊背景,以及一個取消按鈕,將blurView添加給控制器的view做子控件,并且默認alpha值為0, 每次點擊分類,地區(qū),或者排序時,渲染當前tableView的layer的圖片,并且將渲染好的圖片賦值給blurView中的imageView,給imageView添加模糊效果,在配合改變blurView的alpha完成出現(xiàn)和消失即可
詳情頁

這個頁面坑很多,需要注意的細節(jié)太多,也是我耗時最久的頁面,誠然目前bug依舊不少
這個頁面的層級關(guān)系很重要,需要重點注意
首先是導航條,這個咋一看好像是導航條有個漸隱漸現(xiàn)的動畫,我的做法是在頂部放了一個高度為64的view,根據(jù)tableView的偏移量計算出view的透明度,但是透明度只是1或者0,頂部的scrollView里面裝的imageView,根據(jù)服務(wù)器返回的圖片地址個數(shù),設(shè)置他的展示內(nèi)容大小,并且在整一個scrollView最上面添加一個和導航條一樣顏色的view,用它來做出向上推慢慢出現(xiàn)綠色的效果,并且根據(jù)底部scrollview的偏移計算拉伸的大小,這里拉伸的大小我算的不是很準確,感覺需要將錨點釘在最頂端,這應(yīng)就可以做到只拉伸底部
然后是中間切換tableView的view(
后面就叫它選擇view),要實現(xiàn)能像headView一樣,卡在導航條下面的效果,這里因為沒有導航條,并且在切換tableView時候不會帶走選擇view,所以只能將他放到和頂部的view在同一個層級中,同樣根據(jù)底部scrollView的contentOffset.y計算他的位置,當偏移量超過頂部的64時,就停留在那,不超過時就回到頂部view的下面,這里的計算我加了很多的注釋,怕計算的朋友也會看的懂的,大概是這樣
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == self.rmdTableView || scrollView == self.infoTableView) {//說明是tableView在滾動
//記錄當前展示的是那個tableView
self.showingTableView = (UITableView *)scrollView;
//記錄出上一次滑動的距離,因為是在tableView的contentInset中偏移的ScrollHeadViewHeight,所以都得加回來
CGFloat offsetY = scrollView.contentOffset.y;
CGFloat seleOffsetY = offsetY - self.scrollY;
self.scrollY = offsetY;
//修改頂部的scrollHeadView位置 并且通知scrollHeadView內(nèi)的控件也修改位置
CGRect headRect = self.topView.frame;
headRect.origin.y -= seleOffsetY;
self.topView.frame = headRect;
//根據(jù)偏移量算出alpha的值,漸隱,當偏移量大于-180開始計算消失的值
CGFloat startF = -180;
//初始的偏移量Y值為 頂部倆個控件的高度
CGFloat initY = SelectViewHeight + ScrollHeadViewHeight;
//缺少的那一段漸變Y值
CGFloat lackY = initY + startF;
//自定義導航條高度
CGFloat naviH = 64;
//漸隱alpha值
CGFloat alphaScaleHide = 1 - (offsetY + initY- lackY) / (initY- naviH - SelectViewHeight - lackY);
//漸現(xiàn)alph值
CGFloat alphaScaleShow = (offsetY + initY - lackY) / (initY - naviH - SelectViewHeight - lackY) ;
if (alphaScaleShow >= 0.98) {
//顯示導航條
[UIView animateWithDuration:0.04 animations:^{
self.naviView.alpha = 1;
}];
} else {
self.naviView.alpha = 0;
}
self.topScrollView.naviView.alpha = alphaScaleShow;
self.subTitleLabel.alpha = alphaScaleHide;
self.smallImageView.alpha = alphaScaleHide;
/* 這段代碼很有深意啊。。最開始是直接用偏移量算的,但是回來的時候速度比較快時偏移量會偏度很大
然后就悲劇了。換了好多方法。。最后才開竅T——T,這一段我會在blog里面詳細描述我用的各種錯誤的方法
用了KVO監(jiān)聽偏移量的值,切換了selectView的父控件,切換tableview的headView。。。
*/
if (offsetY >= -(naviH + SelectViewHeight)) {
self.selectView.frame = CGRectMake(0, naviH, WNXAppWidth, SelectViewHeight);
} else {
self.selectView.frame = CGRectMake(0, CGRectGetMaxY(self.topView.frame), WNXAppWidth, SelectViewHeight);
}
CGFloat scaleTopView = 1 - (offsetY + SelectViewHeight + ScrollHeadViewHeight) / 100;
scaleTopView = scaleTopView > 1 ? scaleTopView : 1;
//算出頭部的變形 這里的動畫不是很準確,好的動畫是一點一點試出來了 這里可能還需要配合錨點來進行動畫,關(guān)于這種動畫我會在以后單開一個項目配合blog來講解的 這里這就不細調(diào)了
CGAffineTransform transform = CGAffineTransformMakeScale(scaleTopView, scaleTopView );
CGFloat ty = (scaleTopView - 1) * ScrollHeadViewHeight;
self.topView.transform = CGAffineTransformTranslate(transform, 0, -ty * 0.2);
//記錄selectViewY軸的偏移量,這個是用來計算每次切換tableView,讓新出來的tableView總是在頭部用的,
//現(xiàn)在腦子有點迷糊 算不出來了。。凌晨2.57分~
CGFloat selectViewOffsetY = self.selectView.frame.origin.y - ScrollHeadViewHeight;
if (selectViewOffsetY != -ScrollHeadViewHeight && selectViewOffsetY <= 0) {
if (scrollView == self.rmdTableView) {
self.infoTableView.contentOffset = CGPointMake(0, -245 - selectViewOffsetY);
} else {
self.rmdTableView.contentOffset = CGPointMake(0, -245 - selectViewOffsetY);
}
}
} else {
//說明是backgroundScrollView在滾動
CGFloat selectViewOffsetY = self.selectView.frame.origin.y - ScrollHeadViewHeight;
//讓新出來的tableView的contentOffset正好卡在selectView的頭上,還是有bug
if (selectViewOffsetY != -ScrollHeadViewHeight && selectViewOffsetY <= 0) {
if (self.showingTableView == self.rmdTableView) {
self.infoTableView.contentOffset = CGPointMake(0, -245 - selectViewOffsetY);
} else {
self.rmdTableView.contentOffset = CGPointMake(0, -245 - selectViewOffsetY);
}
}
CGFloat offsetX = self.backgroundScrollView.contentOffset.x;
NSInteger index = offsetX / WNXAppWidth;
CGFloat seleOffsetX = offsetX - self.scrollX;
self.scrollX = offsetX;
//根據(jù)scrollViewX偏移量算出頂部selectViewline的位置
if (seleOffsetX > 0 && offsetX / WNXAppWidth >= (0.5 + index)) {
[self.selectView lineToIndex:index + 1];
} else if (seleOffsetX < 0 && offsetX / WNXAppWidth <= (0.5 + index)) {
[self.selectView lineToIndex:index];
}
}
}
- 下面是一個scrollView上添加了3個tabelView,根據(jù)服務(wù)器返回的數(shù)據(jù)判斷顯示多少個,這里就只顯示了倆個tableView演示一下,如果有第三個的話直接添加到底部的scrollView即可,我的代碼中因為只是演示兩個tableView就將兩個tableView交給了一個控制器管理,如果多個tableView最好將每個tableView交給獨立的一個控制器來管理,各司其職,這樣邏輯會清晰很多
這些就是這個項目的大體思路,當然還有很多很多的細節(jié)都在代碼中,感覺自己有很多想要表達的但是沒法說出來,所以我在代碼中加的很詳細的注釋,第一次嘗試將思路寫出來,感覺有很多不足,本應(yīng)該每完成一個功能就總結(jié)一下,而我是在發(fā)布的晚上回頭總結(jié)的,有很多當時的思路不是很清晰了...以后我會改善的,大家有什么意見可以直接留言,我看到會一一回復的!
注意下載完工程請直接打開
點擊運行

附上代碼的下載地址
點擊下載代碼
- 希望大家可以點一下右上角star??,大家的支持與鼓勵是我繼續(xù)分享的動力_,歡迎大家多提意見和交流
我的微博鏈接
- 我會在以后更新學習Swift的心得以及發(fā)布一些Swift的小效果,歡迎朋友繼續(xù)關(guān)注