神刀安全网

六天完成一个简单iOS App – 第六天

第六天任务

  1. 推荐标签页面的完成
  2. 圆形头像的设置和封装
  3. 评论界面的完成
  4. 新帖界面的完成
  5. 发布界面的完成

推荐标签页面的完成

点击精华页面左上角按钮来到推荐标签界面。

六天完成一个简单iOS App - 第六天
推荐标签界面

推荐标签的实现有了之前的经验就非常简单了,根据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]即可,这个时候全世界的头像都变成圆的啦。

六天完成一个简单iOS App - 第六天
圆形头像

而当需要将项目中所有头像由方形转变为圆形的时候,只需要在分类方法中将[self setCircleHeader:url];修改为[self setRectHeader:url];即可,这个时候全世界的头像又都会变成方的。

评论界面的完成。

先来看一下评论界面的内容

六天完成一个简单iOS App - 第六天
评论界面

点击cell会进入到评论界面,评论界面使用xib进行描述,分为上面tableView和底部工具条。

六天完成一个简单iOS App - 第六天
评论界面xib

需要注意的还是约束的添加,因为这里需要底部工具条随着键盘的弹出上移,所以底部工具条的底部与SuperView的底部间距为零,如图

六天完成一个简单iOS App - 第六天
底部工具条最底端约束

然后我们拿到这个约束,监控键盘的弹出,当键盘弹出的时候,将约束间距修改为键盘的高度,同时也可以拿到键盘弹出的时间,使底部工具条在相同时间内上移即可。

// 添加监听 [[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的显示有两种做法。

  1. 总共分为三组cell 第一组cell 用来显示内容 第二组cell用来显示 最热评论 第三组cell用来显示最新评论

  2. 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

六天完成一个简单iOS App - 第六天
评论界面cell的xib

值得注意的评论的内容可能是音频button也可能是label,几个需要额外注意的约束是,内容label与cell的contentView底部间距固定为10,保证cell的高度随着label的高度变化而变化,而无论label有没有内容,label的高度应该大于等于音频button的高度,保证当是音频评论label没有内容的时候,cell的高度同样等于音频button + 10的高度,label的行数设置为0,保证label可以自动换行显示全部文字。音频button与label左边与上边对齐。来看一下label的约束。

六天完成一个简单iOS App - 第六天
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;

评论内容刷新注意点
除了进行请求之前要取消之前的请求之外,评论界面的上拉刷新和下拉加载还有一些需要注意的地方

  1. 当没有评论的时候服务器返回给我们的是一个空的数组,所以此时需要对返回数据类型进行判断,如果是数组说明没有评论,则直接结束刷新,返回即可。
    // 如果没有评论的话 服务器返回的是一个数组 if (![responseObject isKindOfClass:[NSDictionary class]]) {  [self.commentTableView.mj_header endRefreshing];  return ; }
  2. 如果评论小于10条,一次就可以全部请求下来,此时已经不需要上拉加载更多评论了,所以除了关闭下拉刷新,还要判断评论数组的count如果等于评论总数,则隐藏上拉加载更多
    int total = [responseObject[@"total"]intValue]; if (weakSelf.latestComments.count == total) {// 说明加载完全了,隐藏上拉刷新  // 没有更多数据,隐藏上拉加载更多  weakSelf.commentTableView.mj_footer.hidden = YES; }
  3. 上拉加载更多同样需要判断,如果已经加载全部评论则隐藏上拉加载更多,如果没有加载全部,则仅仅结束本次上拉加载即可
    int total = [responseObject[@"total"]intValue];      if (weakSelf.latestComments.count == total) {// 说明加载完全了,隐藏上拉刷新          weakSelf.commentTableView.mj_footer.hidden = YES;      }else{          // 结束刷新          [weakSelf.commentTableView.mj_footer endRefreshing];      }
  4. 当没有数据的时候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赋值
但是这里存在两个问题

  1. 此时最热评论虽然没有了,但是那部分会被空出来,这是因为我们之前对cell的高度进行了缓存,当设置cell高度时,发现cellHeight不为零,则直接返回高度,不会重新计算。因此我们这里将cellHeight设置为0,当设置cell的cellHeight时就会重新计算cellHeight。

  2. 此时我们返回精华界面,将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"; }

通过一张图来看一下精华模块和新帖模块的结构

六天完成一个简单iOS App - 第六天
精华模块和新帖模块的结构

中间加号弹出界面完成

点击中间加号,会弹出发表页面。

六天完成一个简单iOS App - 第六天
发表页面

考虑到发表页面内部按钮点击事件较为复杂,发表页面使用控制器,点击加号按钮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基本可以实现在本界面播放,但是还是存在很多问题,很多细节例如暂停播放,进度条等都没有实现,并且觉得自己的实现并不正确,所以这里就不放上来了。

如果有朋友做过视频,音频播放这方面的实现,有时间并且愿意的话请多多指教

项目总体结构图

六天完成一个简单iOS App - 第六天
项目总体结构图

最后成果。

六天完成一个简单iOS App - 第六天
最后成果

至此,项目已经基本完成,内容非常有限,其中涉及到登陆的一些模块无法获得授权没有完成,发布内容页面,添加关注页面,视频音频的播放等也不够完善,其中也有许多欠缺的地方,一些细节处理不够好,以后在慢慢完善。

昨天晚上rm-rf之后蒙掉了,还好有最近的代码备份,今天又整理了一下。
代码已经上传到github,源码下载

最后总结:如果不去做,就永远不知道自己什么时候能准备好。


文中如果有不对的地方欢迎指出。我是xx_cc,一只长大很久但还没有二够的家伙。

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » 六天完成一个简单iOS App – 第六天

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址