第六天任務
- 推薦標簽頁面的完成
- 圓形頭像的設置和封裝
- 評論界面的完成
- 新帖界面的完成
- 發布界面的完成
推薦標簽頁面的完成
點擊精華頁面左上角按鈕來到推薦標簽界面。
推薦標簽的實現有了之前的經驗就非常簡單了,根據MVC原則創建文件,同樣在cell中添加模型屬性,根據模型為cell內控件賦值。
唯一有一個注意點:當點擊進入推薦標簽頁面,如果此時數據還沒有獲取到,點擊返回,SVP的提醒還在,block會對控制器產生強引用,如果block還沒有執行完,控制器是不會死的,block執行完畢之后,強引用才會被放開,控制器才會被銷毀,所以block中需要使用弱引用
__weak typeof(self) weakSelf = self;
,但是雖然使用弱引用,控制器在該被銷毀的時候就會被銷毀,但是block內的代碼還是會繼續執行的,只不過weakSelf會被置為nil,所以我們需要在一點擊返回的時候將請求取消,在-(void)viewWillDisappear:(BOOL)animated
當控制器view即將消失的時候 隱藏SVP 并且取消請求,但是AFN中如果正在發送請求當請求還沒有返回的時候,取消請求會來到failure方法中,所以需要在failure方法中進行判斷if (error.code == NSURLErrorCancelled)
,如果是需要請求的那么直接返回即可,如果是請求失敗,則提醒用戶。
但是如果是進入下一個界面,則不需要取消請求
圓形頭像的設置
圓形頭像使用Quartz2D來實現,實現思路:開啟圖形上下文,在圖形上下文上添加一個圓,裁剪,然后將圖片繪制到圓形區域,然后獲得圖片即是圓形圖片。
這里對圓形頭像進行了封裝,給image添加分類,傳入一張圖片,返回一張圓形圖片
UIImage+CLExtension.m
#import "UIImage+CLExtension.h"
@implementation UIImage (CLExtension)
/** 返回圓形圖片 */
-(instancetype)circleImage
{
// 開啟圖形上下文
UIGraphicsBeginImageContext(self.size);
// 上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 添加一個圓
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextAddEllipseInRect(ctx, rect);
// 裁剪
CGContextClip(ctx);
// 繪制圖片
[self drawInRect:rect];
// 獲得圖片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 關閉圖形上下文
UIGraphicsEndImageContext();
return image;
}
/** 直接根據image name設置圓角 */
+(instancetype)circleImageNamed:(NSString *)name
{
return [[UIImage imageNamed:name] circleImage];
}
@end
傳入圖片或者直接傳入圖片name,返回一張圓形圖片。
因為一個項目中的頭像一般是統一的,如果是方形的則項目中所有頭像都是方形的,而如果要修改為圓形的則每一處頭像設置都需要更改,為了能夠統一控制項目中所有頭像的形狀,我們給imageView添加設置頭像的分類
#import "UIImageView+CLExtension.h"
#import <UIImageView+WebCache.h>
@implementation UIImageView (CLExtension)
/** 默認為圓形頭像 */
- (void)setHeader:(NSString *)url
{
[self setCircleHeader:url];
}
/** 設置圓形頭像 */
- (void)setCircleHeader:(NSString *)url
{
// 將占位圖片也轉化為圓形 其實占位圖片本來就是圓形
UIImage *placeholder = [UIImage circleImageNamed:@"defaultUserIcon"];
[self sd_setImageWithURL:[NSURL URLWithString:url] placeholderImage:placeholder completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
// 如果image為空則返回占位圖片
if (image == nil) return;
self.image = [image circleImage];
}];
}
/** 設置方形頭像 */
- (void)setRectHeader:(NSString *)url
{
UIImage *placeholder = [UIImage imageNamed:@"defaultUserIcon"];
[self sd_setImageWithURL:[NSURL URLWithString:url] placeholderImage:placeholder];
}
@end
而項目中設置頭像也變得非常簡單,直接[imageView setHeader:url]即可,這個時候全世界的頭像都變成圓的啦。
而當需要將項目中所有頭像由方形轉變為圓形的時候,只需要在分類方法中將[self setCircleHeader:url];
修改為[self setRectHeader:url];
即可,這個時候全世界的頭像又都會變成方的。
評論界面的完成。
先來看一下評論界面的內容
點擊cell會進入到評論界面,評論界面使用xib進行描述,分為上面tableView和底部工具條。
需要注意的還是約束的添加,因為這里需要底部工具條隨著鍵盤的彈出上移,所以底部工具條的底部與SuperView的底部間距為零,如圖
然后我們拿到這個約束,監控鍵盤的彈出,當鍵盤彈出的時候,將約束間距修改為鍵盤的高度,同時也可以拿到鍵盤彈出的時間,使底部工具條在相同時間內上移即可。
// 添加監聽
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
- (void)keyboardWillChangeFrame:(NSNotification *)note
{
// 修改約束 = 屏幕的高度 - 鍵盤的y值
CGFloat keyboardY = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].origin.y;
CGFloat screenH = [UIScreen mainScreen].bounds.size.height;
self.bottomMargin.constant = screenH - keyboardY;
// 執行動畫
// 獲取執行動畫的時間
CGFloat duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
[UIView animateWithDuration:duration animations:^{
// 更新約束
[self.view layoutIfNeeded];
}];
}
注意:控制器銷毀的時候一定要記得移除監聽
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
評論界面上方cell的顯示有兩種做法。
總共分為三組cell 第一組cell 用來顯示內容 第二組cell用來顯示 最熱評論 第三組cell用來顯示最新評論
cell分為兩組,將cell的內容轉化為heardView。
如果tableView的style設置為 plain 而不是group,同時設置tableView的頭標題 heardView , tableView往上面滑動的時候 heardView就會停留在屏幕最上方。
heardTitle的設置可以在代理方法中直接返回內容
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
但是為了能夠使heardView更加豐富,可以直接返回UIview
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
如果heardView特別多 可以使用 UITableViewHeaderFooterView
。
UITableViewHeaderFooterView
和cell一樣有重用機制,需要注冊,并從緩存池中取
也可以繼承UITableViewHeaderFooterView進行自定義
通過重寫- (instancetype)initWithReuseIdentifier:(NSString *)
方法對其內部進行一些修改- (void)layoutSubviews
對其內部的控件frame進行一些修改
一般如果想要修改控件內子控件的frame,等但是發現怎么改都會被改回去,那么這個時候可以嘗試在layoutSubViews中進行修改,先讓super設置完畢之后,我們在進行設置進行覆蓋,用來覆蓋對子控件的一些設置。
cell的高度計算
評論界面的cell使用的是UITableViewAutomaticDimension
自動計算高度,這樣cell在添加約束的時候需要額外小心,先來看一下評論cell的xib
值得注意的評論的內容可能是音頻button也可能是label,幾個需要額外注意的約束是,內容label與cell的contentView底部間距固定為10,保證cell的高度隨著label的高度變化而變化,而無論label有沒有內容,label的高度應該大于等于音頻button的高度,保證當是音頻評論label沒有內容的時候,cell的高度同樣等于音頻button + 10的高度,label的行數設置為0,保證label可以自動換行顯示全部文字。音頻button與label左邊與上邊對齊。來看一下label的約束。
同時在代碼中需要設置cell的高度自動計算,并且給cell一個大致的估算高度
// 設置cell行高自動計算 自動計算尺寸
self.commentTableView.rowHeight = UITableViewAutomaticDimension;
// 需要先給一個大約的估算高度
self.commentTableView.estimatedRowHeight = 44;
cell的內容顯示
cell的內容顯示就非常簡單了,無非需要對評論的內容進行判斷,如果是文字內容則隱藏音頻button,如果是音頻則表示肯定沒有文字,設置button的title即可。
另外因為評論分為最熱評論和最新評論,分為幾種情況,最熱評論和最新評論都有,有最新評論但是沒有最熱評論,和沒有評論。設置heardtitle,返回行數,和賦值的時候進行一些判斷即可。
// 如果是第0組,并且最熱評論有值則返回最評論行數
if (section == 0 && self.hotestComments.count) {
return self.hotestComments.count;
}
// 否則都返回最新評論行數
return self.latestComments.count;
評論內容刷新注意點
除了進行請求之前要取消之前的請求之外,評論界面的上拉刷新和下拉加載還有一些需要注意的地方
- 當沒有評論的時候服務器返回給我們的是一個空的數組,所以此時需要對返回數據類型進行判斷,如果是數組說明沒有評論,則直接結束刷新,返回即可。
// 如果沒有評論的話 服務器返回的是一個數組
if (![responseObject isKindOfClass:[NSDictionary class]]) {
[self.commentTableView.mj_header endRefreshing];
return ;
}
- 如果評論小于10條,一次就可以全部請求下來,此時已經不需要上拉加載更多評論了,所以除了關閉下拉刷新,還要判斷評論數組的count如果等于評論總數,則隱藏上拉加載更多
int total = [responseObject[@"total"]intValue];
if (weakSelf.latestComments.count == total) {// 說明加載完全了,隱藏上拉刷新
// 沒有更多數據,隱藏上拉加載更多
weakSelf.commentTableView.mj_footer.hidden = YES;
}
- 上拉加載更多同樣需要判斷,如果已經加載全部評論則隱藏上拉加載更多,如果沒有加載全部,則僅僅結束本次上拉加載即可
int total = [responseObject[@"total"]intValue];
if (weakSelf.latestComments.count == total) {// 說明加載完全了,隱藏上拉刷新
weakSelf.commentTableView.mj_footer.hidden = YES;
}else{
// 結束刷新
[weakSelf.commentTableView.mj_footer endRefreshing];
}
- 當沒有數據的時候MJRefresh提供了自動判斷的方法
/** 自動根據有無數據來顯示和隱藏(有數據就顯示,沒有數據隱藏。默認是NO) */
self.commentTableView.mj_footer.automaticallyHidden = YES;
tableView的heardView的顯示
評論界面的heardView和精華頁面的cell內容一致,我們可以直接通過cell的loadNibNamed方法來直接加載xib中的cell,但是內容還是需要自己設置。
// viewFromNib 是在分類中對loadNibNamed方法進行的封裝
CLTopicCell *cell = [CLTopicCell viewFromNib];
cell.topic = self.topic;
cell.cl_height = self.topic.cellHeight + 20;
// 設置heardView
self.commentTableView.tableHeaderView = cell;
需要注意的一點是,因為我們在之前設置cell之間的間距的時候重寫過cell的setFrame方法,在setFrame中將cell的高度減少了10,所以每次設置cell的frame都會來到這個方法,將cell的高度減少10,評論界面顯示的時候來到一次setFrame方法,設置cell高度的時候又來到一次,一共來到兩次setFrame方法,cell的高度被減少了20,所以設置cell高度的時候需要加上20。
另外因為這里setFrame方法中只對cell的高度做了修改,所以稍作修改就可以完整的顯示cell,但是如果在setFrame中對cell的位置和寬高同時做了修改,就會產生難以捉摸的錯誤,所以如果需要在setFrame中對cell的位置和寬高同時做修改時,建議使用一個UIView當做載體,heardView上添加UIView,UIView上在添加cell,此時cell的setFrame不會對UIView產生任何影響。
消除評論界面heardView中的最熱評論
如果是有最熱評論的cell,加載到評論界面時需要將最熱評論去掉,這里將CLTopic模型的top_cmt最熱評論屬性置為空,然后在給cell的topic賦值
但是這里存在兩個問題
此時最熱評論雖然沒有了,但是那部分會被空出來,這是因為我們之前對cell的高度進行了緩存,當設置cell高度時,發現cellHeight不為零,則直接返回高度,不會重新計算。因此我們這里將cellHeight設置為0,當設置cell的cellHeight時就會重新計算cellHeight。
此時我們返回精華界面,將cell滑出界面在滑回來,這時發現cell內的熱門評論也沒有了,這是因為我們之前將CLTopic模型的top_cmt最熱評論屬性置為空了,并且緩存了cell的高度,因此這里需要將top_cmt最熱評論屬性記錄保存起來,在評論控制器將要被銷毀的時候,也就是返回精華界面的時候,重新將top_cmt最熱評論屬性賦值回去,并將cellHeight高度重新設置為0,使其重新計算高度。
這里貼出設置heardView和dealloc方法
@property(nonatomic,strong)CLComment *saveTopCom;
-(void)setupTableHeard
{
// 如果有最熱評論,則設為空
// 當控制器銷毀的時候,需要將值重新設置回來,并且將cellheight設置為0 讓其在重新計算一次。所以先將他保存起來
self.saveTopCom = self.topic.top_cmt;
self.topic.top_cmt = nil;
self.topic.cellHeight = 0;
// 從xib加載cell
CLTopicCell *cell = [CLTopicCell viewFromNib];
cell.topic = self.topic;
cell.cl_height = self.topic.cellHeight + 20;
// 如果使用UIView當中間的載體,需要設置cell的frame。
// cell.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, self.topic.cellHeight);
// // 創建heardView
// UIView *heardView = [[UIView alloc]init];
// [heardView addSubview:cell];
// heardView.cl_height = self.topic.cellHeight;
// heardView.backgroundColor = CLCommonColor(206);
// 設置heardView
self.commentTableView.tableHeaderView = cell;
}
- (void)dealloc
{
// 控制器銷毀的時候 將值重新設置回去,并將cellHeight設置為0,讓其重新計算高度
self.topic.top_cmt = self.saveTopCom;
self.topic.cellHeight = 0;
}
新帖模塊的完成
新帖模塊頁面和精華完全一樣,只是請求的數據不同,只需要讓新帖的控制器繼承自精華控制器,請求數據的時候對控制器類型進行判斷,根據不同的控制器設置不同的請求參數即可。
- (NSString *)aParam
{
if (self.parentViewController.class == [CLNewViewController class]) {
return @"newlist";
}
return @"list";
}
通過一張圖來看一下精華模塊和新帖模塊的結構
中間加號彈出界面完成
點擊中間加號,會彈出發表頁面。
考慮到發表頁面內部按鈕點擊事件較為復雜,發表頁面使用控制器,點擊加號按鈕moda出發表頁面控制器,至于發表頁面內容的布局和賦值不在贅述,6個button有一個飛出動畫,逐個從底部飛出到頁面上,其實現原理為:
布局button時,先將button放在現在的位置上,然后設置button的transform下移一個屏幕的高度
btn.transform = CGAffineTransformMakeTranslation(0, self.view.bounds.size.height);
然后當控制器view顯示完成的時候,設置每隔0.1s執行一次動畫,將一個button的transform恢復
self.time = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(upData) userInfo:nil repeats:YES];
恢復button的transform
btn.transform = CGAffineTransformIdentity;
當六個button全部恢復完成的時候將self.time取消
[self.time invalidate];
點擊狀態欄返回tableView頂部實現
當點擊狀態欄的時候,tableView會自動滾動到最上方,其實scrollView有scrollsToTop這個屬性,并且默認就是YES,但是有個局限性,只有在有一個屏幕滾動視圖的時候才會生效,當scrollView中有一個以上的滾動視圖時,將會失效。
而且只能設置狀態欄的狀態,卻沒有辦法拿到狀態欄做一些事情,使用控件遮擋狀態欄也會被狀態欄覆蓋。
那么如果想要遮住狀態欄,需要創建一個優先級大于statusBar的透明的Window用來遮擋狀態欄,并監聽點擊事件。
需要注意一點:iOS9之后,要求如果window在程序啟動完之后就顯示則必須有一個根控制器。因此需要設置將window延遲創建即可。
實現思路為:短暫延遲創建狀態欄大小的window,并設置window的層級大于StatusBar的層級,為window添加點擊事件,然后拿到keywindow的所有子控件找到scrollView,判斷scrollView有沒有顯示在keywindow上,如果顯示了則修改scrollView的offset.y
等于頂端的偏移量即-contentInset.top
即可。
window的層級分為三種,層級高的顯示在最外面,當層級相同時,越靠后調用的顯示在外面。
UIWindowLevelNormal; //默認,值為0
UIWindowLevelAlert; //值為2000
UIWindowLevelStatusBar ; // 值為1000
判斷scrollView有沒有顯示在keywindow上,實質上是判斷scrollView和keywindow有沒有重疊的地方,而判斷他們有沒有重疊的前提是他們在同一個坐標系中,即在同一個父控件中。
UIView提供了轉換坐標系和判斷兩個空間是否有重疊的方法,
// 讓rect這個矩形框, 從view2坐標系轉換到view1坐標系, 得出一個新的矩形框newRect
CGRect newRect = [view1 convertRect:rect fromView:view2];
// 讓rect這個矩形框, 從view1坐標系轉換到view2坐標系, 得出一個新的矩形框newRect
CGRect newRect = [view1 convertRect:rect toView:view2];
是否包含
CGRectContainsRect(CGRect1,CGrect2)
是否交叉
CGRectIntersectsRect(CGrect1,CGRect2)
這里將判斷兩個空間知否交叉的判斷方法添加到UIView的分類中,自定義window,在application中延遲添加顯示。
判斷控件是否交叉方法
-(BOOL)intersectWithView:(UIView *)view
{
// 這里使用keywindow是為了防止兩個控件在兩個不同的window中,這種情況一般不會出現,toView:nil 默認就是控件所在的window。
UIWindow *window = [UIApplication sharedApplication].keyWindow;
CGRect newRect = [self convertRect:self.bounds toView:window];
CGRect newView = [view convertRect:view.bounds toView:window];
return CGRectIntersectsRect(newRect, newView);
}
window的創建與添加點擊事件
#import "CLTopWindow.h"
@implementation CLTopWindow
static UIWindow *window_;
+(void)show
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
window_ = [[UIWindow alloc]init];
window_.frame = [UIApplication sharedApplication].statusBarFrame;
window_.backgroundColor = [UIColor clearColor];
window_.windowLevel = UIWindowLevelAlert;
window_.hidden = NO;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(topWindowClick)];
[window_ addGestureRecognizer:tap];
});
}
+(void)topWindowClick
{
UIWindow *keiwindow = [UIApplication sharedApplication].keyWindow;
[self findscrollViewsInView:keiwindow];
}
+(void)findscrollViewsInView:(UIView *)view
{
for (UIView *subview in view.subviews) {
[self findscrollViewsInView:subview];
}
if (![view isKindOfClass:[UIScrollView class]]) return;
if(![view intersectWithView:[UIApplication sharedApplication].keyWindow])return;
UIScrollView *scrollView = (UIScrollView *)view;
// 修改offset
CLLog(@"%@",scrollView);
CGPoint offset = scrollView.contentOffset;
offset.y = - scrollView.contentInset.top;
[scrollView setContentOffset:offset animated:YES];
// 這是使scrollView顯示出某個區域
// [scrollView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES];
}
@end
重復點擊tabbarbutton和titleView中button后刷新數據實現
重復點擊tabbarButton或者titleView中的button之后刷新數據,首先需要記錄下來上次的點擊按鈕,與本次點擊比較,如果發現是重復點擊則通知界面刷新。
所以需要監聽按鈕的點擊,并發送通知,為了避免其他界面同時刷新,需要判斷控制器的view在不在window上和view跟window有沒有重疊,兩者缺一不可,判斷控制器的view在不在window上排除的是tabbar上的其他控制器view,判斷view跟window有沒有重疊排除的是精華模塊中其他子控制器。
監聽按鈕的點擊,分別可以在application中使用UITabBarControllerDelegate的代理方法監聽tabbarbutton的點擊,titlebutton的點擊在button點擊事件中。分別進行判斷并添加通知。
播放視頻和音樂
視頻的播放項目中暫時使用了MPMoviePlayerViewController,跳轉控制器進行播放,和音樂的播放,查看百思不得姐原項目,發現視頻和音頻都是在本界面播放的,自己嘗試了一下使用AVPlaylayer基本可以實現在本界面播放,但是還是存在很多問題,很多細節例如暫停播放,進度條等都沒有實現,并且覺得自己的實現并不正確,所以這里就不放上來了。
如果有朋友做過視頻,音頻播放這方面的實現,有時間并且愿意的話請多多指教
項目總體結構圖
最后成果。
至此,項目已經基本完成,內容非常有限,其中涉及到登陸的一些模塊無法獲得授權沒有完成,發布內容頁面,添加關注頁面,視頻音頻的播放等也不夠完善,其中也有許多欠缺的地方,一些細節處理不夠好,以后在慢慢完善。
昨天晚上rm-rf之后蒙掉了,還好有最近的代碼備份,今天又整理了一下。
代碼已經上傳到github,源碼下載。
最后總結:如果不去做,就永遠不知道自己什么時候能準備好。
文中如果有不對的地方歡迎指出。我是xx_cc,一只長大很久但還沒有二夠的家伙。