第六天任务
- 推荐标签页面的完成
- 圆形头像的设置和封装
- 评论界面的完成
- 新帖界面的完成
- 发布界面的完成
推荐标签页面的完成
点击精华页面左上角按钮来到推荐标签界面。
推荐标签的实现有了之前的经验就非常简单了,根据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
@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,一只长大很久但还没有二够的家伙。