iOSiOS iOS积累用之

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

2016-10-11  本文已影响3212人  xx_cc

第六天任务

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

推荐标签页面的完成

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

推荐标签界面
推荐标签的实现有了之前的经验就非常简单了,根据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和底部工具条。

评论界面xib

需要注意的还是约束的添加,因为这里需要底部工具条随着键盘的弹出上移,所以底部工具条的底部与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的显示有两种做法。

  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

评论界面cell的xib

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

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

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

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

精华模块和新帖模块的结构

中间加号弹出界面完成

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


发表页面

考虑到发表页面内部按钮点击事件较为复杂,发表页面使用控制器,点击加号按钮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,一只长大很久但还没有二够的家伙。

上一篇下一篇

猜你喜欢

热点阅读