前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >六天完成一个简单iOS App - 第六天

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

作者头像
xx_Cc
发布2018-05-10 11:39:43
1.3K0
发布2018-05-10 11:39:43
举报

第六天任务

  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

代码语言:javascript
复制
#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添加设置头像的分类

代码语言:javascript
复制
#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的底部间距为零,如图

底部工具条最底端约束

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

代码语言:javascript
复制
// 添加监听
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];
代码语言:javascript
复制
- (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];
    }];
}

注意:控制器销毁的时候一定要记得移除监听

代码语言:javascript
复制
- (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的设置可以在代理方法中直接返回内容

代码语言:javascript
复制
-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section

但是为了能够使heardView更加丰富,可以直接返回UIview

代码语言:javascript
复制
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section

如果heardView特别多 可以使用 UITableViewHeaderFooterViewUITableViewHeaderFooterView和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一个大致的估算高度

代码语言:javascript
复制
    // 设置cell行高自动计算 自动计算尺寸
    self.commentTableView.rowHeight = UITableViewAutomaticDimension;
    // 需要先给一个大约的估算高度
    self.commentTableView.estimatedRowHeight = 44;

cell的内容显示 cell的内容显示就非常简单了,无非需要对评论的内容进行判断,如果是文字内容则隐藏音频button,如果是音频则表示肯定没有文字,设置button的title即可。

另外因为评论分为最热评论和最新评论,分为几种情况,最热评论和最新评论都有,有最新评论但是没有最热评论,和没有评论。设置heardtitle,返回行数,和赋值的时候进行一些判断即可。

代码语言:javascript
复制
// 如果是第0组,并且最热评论有值则返回最评论行数
if (section == 0 && self.hotestComments.count) {
    return self.hotestComments.count;
}
// 否则都返回最新评论行数
return self.latestComments.count;

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

  1. 当没有评论的时候服务器返回给我们的是一个空的数组,所以此时需要对返回数据类型进行判断,如果是数组说明没有评论,则直接结束刷新,返回即可。
代码语言:javascript
复制
// 如果没有评论的话 服务器返回的是一个数组
if (![responseObject isKindOfClass:[NSDictionary class]]) {
    [self.commentTableView.mj_header endRefreshing];
    return ;
}
  1. 如果评论小于10条,一次就可以全部请求下来,此时已经不需要上拉加载更多评论了,所以除了关闭下拉刷新,还要判断评论数组的count如果等于评论总数,则隐藏上拉加载更多
代码语言:javascript
复制
int total = [responseObject[@"total"]intValue];
if (weakSelf.latestComments.count == total) {// 说明加载完全了,隐藏上拉刷新
    // 没有更多数据,隐藏上拉加载更多
    weakSelf.commentTableView.mj_footer.hidden = YES;
}
  1. 上拉加载更多同样需要判断,如果已经加载全部评论则隐藏上拉加载更多,如果没有加载全部,则仅仅结束本次上拉加载即可
代码语言:javascript
复制
int total = [responseObject[@"total"]intValue];
        if (weakSelf.latestComments.count == total) {// 说明加载完全了,隐藏上拉刷新
            weakSelf.commentTableView.mj_footer.hidden = YES;
        }else{
            // 结束刷新
            [weakSelf.commentTableView.mj_footer endRefreshing];
        }
  1. 当没有数据的时候MJRefresh提供了自动判断的方法
代码语言:javascript
复制
/** 自动根据有无数据来显示和隐藏(有数据就显示,没有数据隐藏。默认是NO) */
self.commentTableView.mj_footer.automaticallyHidden = YES;

tableView的heardView的显示 评论界面的heardView和精华页面的cell内容一致,我们可以直接通过cell的loadNibNamed方法来直接加载xib中的cell,但是内容还是需要自己设置。

代码语言:javascript
复制
// 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方法

代码语言:javascript
复制
@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;    
}
代码语言:javascript
复制
 - (void)dealloc
{
//     控制器销毁的时候 将值重新设置回去,并将cellHeight设置为0,让其重新计算高度
    self.topic.top_cmt = self.saveTopCom;
    self.topic.cellHeight = 0;
}

新帖模块的完成

新帖模块页面和精华完全一样,只是请求的数据不同,只需要让新帖的控制器继承自精华控制器,请求数据的时候对控制器类型进行判断,根据不同的控制器设置不同的请求参数即可。

代码语言:javascript
复制
- (NSString *)aParam
{
    if (self.parentViewController.class == [CLNewViewController class]) {
        return @"newlist";
    }
    return @"list";
}

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

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

中间加号弹出界面完成

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

发表页面

考虑到发表页面内部按钮点击事件较为复杂,发表页面使用控制器,点击加号按钮moda出发表页面控制器,至于发表页面内容的布局和赋值不在赘述,6个button有一个飞出动画,逐个从底部飞出到页面上,其实现原理为: 布局button时,先将button放在现在的位置上,然后设置button的transform下移一个屏幕的高度

代码语言:javascript
复制
btn.transform = CGAffineTransformMakeTranslation(0, self.view.bounds.size.height);

然后当控制器view显示完成的时候,设置每隔0.1s执行一次动画,将一个button的transform恢复

代码语言:javascript
复制
self.time = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(upData) userInfo:nil repeats:YES];

恢复button的transform

代码语言:javascript
复制
btn.transform = CGAffineTransformIdentity;

当六个button全部恢复完成的时候将self.time取消

代码语言:javascript
复制
[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的层级分为三种,层级高的显示在最外面,当层级相同时,越靠后调用的显示在外面。

代码语言:javascript
复制
UIWindowLevelNormal; //默认,值为0
UIWindowLevelAlert; //值为2000 
UIWindowLevelStatusBar ; // 值为1000

判断scrollView有没有显示在keywindow上,实质上是判断scrollView和keywindow有没有重叠的地方,而判断他们有没有重叠的前提是他们在同一个坐标系中,即在同一个父控件中。 UIView提供了转换坐标系和判断两个空间是否有重叠的方法,

代码语言:javascript
复制
//    让rect这个矩形框, 从view2坐标系转换到view1坐标系, 得出一个新的矩形框newRect
CGRect newRect = [view1 convertRect:rect fromView:view2];
//    让rect这个矩形框, 从view1坐标系转换到view2坐标系, 得出一个新的矩形框newRect
CGRect newRect = [view1 convertRect:rect toView:view2];
代码语言:javascript
复制
是否包含
CGRectContainsRect(CGRect1,CGrect2)
是否交叉
CGRectIntersectsRect(CGrect1,CGRect2)

这里将判断两个空间知否交叉的判断方法添加到UIView的分类中,自定义window,在application中延迟添加显示。

判断控件是否交叉方法

代码语言:javascript
复制
-(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的创建与添加点击事件

代码语言:javascript
复制
#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,一只长大很久但还没有二够的家伙。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016.10.11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第六天任务
  • 推荐标签页面的完成
  • 圆形头像的设置
  • 评论界面的完成。
  • 新帖模块的完成
  • 中间加号弹出界面完成
  • 点击状态栏返回tableView顶部实现
  • 重复点击tabbarbutton和titleView中button后刷新数据实现
  • 播放视频和音乐
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档