09.项目实战 百思不得姐 设置帖子圆形头像,最热评论处理,修复重复点击bug,刷新控件的使用.

@(iOS 项目实战)[项目实战]


目录

  • 09.项目实战 百思不得姐 设置帖子圆形头像,最热评论处理,修复重复点击bug,刷新控件的使用.
  • 1.设置帖子头像
    • SDWebImage图片下载失败处理
    • Xcode插件安装路径
    • KSImageName添加自定义方法提示功能
  • 2.最热评论
    • 最热评论
  • 3.拖动触发按钮重复点击bug
  • 4.苹果官方刷新控件
    • UIRefreshControl刷新控件
  • 5.使用MJRefresh刷新框架
    • MJRefresh的使用
  • 补充
    • AFNetworking的使用
    • 使用第三方框架时子类化
    • github使用
    • 真机测试问题

1.设置帖子头像

设置思路: 项目中多处用到设置圆形图片的功能,考虑封装生成圆形图片的分类UIImage+Circle.因为头像图片资源都是通过url获取,所以可以封装设置圆形头像分类,将使用SDWebImage获取图片的操作也封装到设置圆形头像分类中UIImageView+Header.

SDWebImage图片下载失败处理

  • 图片下载失败处理

    • 图片下载失败时存在问题: 如果图片下载失败时,image为nil,又将nil赋值给UIImageView,会出现图片下载失败时,显示为空白,连占位图片都不显示.
    • 解决方案: 在使用sd_setImageWithURL:方法加载图片的时候,判断SDWebImage获取回来的image是否为nil,如果为nil,直接返回.
    • 参考代码
      // 2.加载图片
      [self sd_setImageWithURL:[NSURL URLWithString:url] placeholderImage:placeholderImage completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
          
          // 2.1 如果图片获取失败,直接退出,否则会出现图片加载失败,占位图片不显示
          if (image == nil) return;
          // 2.2 图片加载成功,显示图片
          self.image = image;
      }];
    

  • 圆形头像处理

    • 封装生成圆形图片UIImage+Circle分类
      • 提供生成圆形图片的对象方法wx_circleImage方法.
      • 提供通过图片名快速生成圆形图片的类方法wx_circleImageName:方法.
      // ----------------------------------------------------------------------------
      // 生成圆形图片
      - (instancetype)wx_circleImage
      {
          // 1.开启图形上下文
          UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
          
          // 2.描述裁减路径
          UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
          // 3.设置裁减路径
          [path addClip];
          // 4.绘制图片
          [self drawAtPoint:CGPointZero];
          // 5.生成新图片
          UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
          // 6.关闭图形上下文
          UIGraphicsEndImageContext();
          
          return image;
      }
      
      // ----------------------------------------------------------------------------
      // 通过图片名生成圆形图片
      + (instancetype)wx_circleImageName:(NSString *)imageName
      {
          return [[UIImage imageNamed:imageName] wx_circleImage];
      }
    
    • 设置圆形头像分类UIImageView+Header
      • 设置圆形占位图片.
      • 将使用SDWebImage获取图片的操作封装到设置圆形头像的分类中.
      // ----------------------------------------------------------------------------
      // 设置圆形头像
      - (void)wx_setHeader:(NSString *)url
      {
          // 1.创建占位图片
          UIImage *placeholderImage = [UIImage wx_circleImageName:@"defaultUserIcon"];
          
          // 2.加载图片
          [self sd_setImageWithURL:[NSURL URLWithString:url] placeholderImage:placeholderImage completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
              
              // 2.1 如果图片获取失败,直接退出,否则会出现图片加载失败,占位图片不显示
              if (image == nil) return;
              // 2.2 图片加载成功,显示图片
              self.image = [image wx_circleImage];
          }];
      }
    

Xcode插件安装路径

  • Xcode插件安装路径
    • 路径一: /Users/用户名/Library/Application Support/Developer/Shared/Xcode/Plug-ins
    • 路径二: /Users/用户名/Library/Developer/Xcode/Plug-ins

KSImageName添加自定义方法提示功能

  • 1.查找插件安装路径
    • 获取KSImageName在Xcode7的安装路径(/Users/用户名/Library/Developer/Xcode/Plug-ins).

    • 选中KSImageNamed.ideplugin,右击显示包内容,打开/Contents/Resources/Completions.plist文件,在Completions.plist文件中进行添加配置.

      • 复制插件默认的提示imageName选项,复制item0.
      • 将自定义方法添加到新增的item中.
      • 注意: 修改KSImageNamed的plist配置文件,其中有个配置方法中左边有个空格.
    • Completions.plist文件配置图解

01.Completions.plist文件配置图解.png

  • 2.在KSImageName源码中修改配置

    • 从github下载KSImageName的项目,在源码中修改配置.
  • 3.如果找不到资源库是因为本地化文件.localized被删除了,可以从其他文件夹中复制一份到Library中即可重新显示资源库.注意: .localized文件是隐藏文件,要确保Mac电脑已经开启显示隐藏文件选项.


  • 4.设置帖子头像为圆形头像

    • 在WXTopicCell.m的模型的set方法setTopicItem:方法中设置圆形头像.
      // 重写模型的set方法
      - (void)setTopic:(XMGTopic *)topic
      {
          // 设置帖子圆形头像
          [self.profileImageView xmg_setHeader:topic.profile_image];
          
          // 其他设置...
      }
    

2.最热评论

分析: 如果服务器返回的数据有最热评论有数据,则需要显示最热评论,否则无需显示最热评论。最热评论中包含语音评论,需对语音评论处理。

最热评论

  • 最热评论请求数据分析

    • 请求所需参数字段(a, c, type)
    02.最热评论请求数据分析.png
  • 最热评论所需数据: 用户名(user.username) + 评论内容(content).
/**
 
// 服务器返回的JSON数据,其中包含用户名 + 评论内容
{
    content = "\U4e94\U767e\U5e74\U524d\U4e00\U4eba\U8e0f\U5e73\U5929\U754c\Uff0c\U4eca\U5929\U5341\U4e09\U4ebf\U4eba\U5e2e\U4f60\U8e0f\U5e73\U6625\U665a\U3002";
    ctime = "2016-01-28 16:54:32";
    "data_id" = 17032894;
    id = 42164160;
    "like_count" = 678;
    precid = 0;
    precmt =     (
    );
    preuid = 0;
    status = 0;
    user =     {
        id = 16413351;
        "is_vip" = 0;
        "personal_page" = "http://user.qzone.qq.com/DFA2DC83F91D296282FD922FA4C45181";
        "profile_image" = "http://qzapp.qlogo.cn/qzapp/100336987/DFA2DC83F91D296282FD922FA4C45181/100";
        "qq_uid" = "";
        "qzone_uid" = DFA2DC83F91D296282FD922FA4C45181;
        sex = f;
        username = "\U3001Just .";
        "weibo_uid" = "";
    };
    voicetime = 0;
    voiceuri = "";
}
 */

  • 1.给WXTopicItem模型添加最热评论属性.(NSArray *top_cmt;)

  • 2.在模型的set方法setTopicItem:处理最热评论(隐藏/显示).

    • 最热评论: 用户名 + 评论内容
    • 最热评论处理参考代码
      // ----------------------------------------------------------------------------
      // 设置最热评论
      - (void)setTopCmt
      {
          // 1.获取最热评论数据,返回数据为字典,只显示第一条,只需取出第一条即可
          NSDictionary *dict = self.topicItem.top_cmt.firstObject;
          // 2.判断是否有最热评论数据
          if (dict) {
              
              // 1.获取最热评论 用户名 + 评论内容
              NSString *username = dict[@"user"][@"username"];
              NSString *content = dict[@"content"];
              // 判断评论内容是否为空串,空串是语音评论
              if (content.length == 0) {
                  content = @"[语音评论]";
              }
              
              self.topCmtContentLabel.text = [NSString stringWithFormat:@"%@ : %@", username, content];
              self.topCmtView.hidden = NO;
          } else {
              self.topCmtContentLabel.text = @"";
              self.topCmtView.hidden = YES;
          }
      }
    

  • 注意: 判断数组有没有内容用count方法,判断字符串是不是空串,用length方法.

    • 错误写法
      // 如果判断数组有没有内容,不能使用下面的判断
      NSArray *array = @[@"abc", @"cdf"];
      if (array != nil) {
      }
      if (array) {
      }
      
      // 如果判断字符串是不是空串,不能使用下面的判断
      NSString *content = @"abcdef";
      if (content != nil) {
      }
    

3.拖动触发按钮重复点击bug

  • 拖动scrollView时,没滚动到其他界面,还是在原来界面时,又会触发按钮重复点击.

    • bug重现
03.重复点击bug.gif
  • 原因: 在监听scrollView停止拖动的scrollViewDidEndDecelerating:方法中调用了titleButtonClick:方法,导致只要用户稍微左右滑动的时候就触发标题按钮重复点击.

  • 解决方案: 抽取titleButtonClick:方法在处理标题按钮、下划线、scrollView,封装dealTitleButtonClick:方法.在scrollViewDidEndDecelerating:方法中调用了dealTitleButtonClick:方法即可.

  • 1.在标题按钮监听的方法中,处理按钮,下划线滚动,scrollView滚动触发标题按钮重复点击

    • 重复点击标题按钮才发通知
    • 拖动时不需要发通知
      
      // ----------------------------------------------------------------------------
      // 处理标题按钮、下划线、scrollView
      - (void)dealTitleButtonClick:(WXTitleButton *)button
      {
          // 切换中状态
          self.selectedButton.selected = NO;
          button.selected = YES;
          self.selectedButton = button;
          
          // 1.获取索引,按钮的tag值
          NSInteger index = button.tag;
          
          // 2.执行下划线动画,动画执行完成修改scrollView的偏移量,显示对应子控制器的view
          [UIView animateWithDuration:0.25 animations:^{
              
              // TODO: 设置下划线的宽度和中心点
              self.underLineView.wx_width = button.titleLabel.wx_width;
              self.underLineView.wx_centerX = button.wx_centerX;
              
              // 切换到对应的view
              self.scrollView.contentOffset = CGPointMake(self.scrollView.wx_width * index, self.scrollView.contentOffset.y);
          } completion:^(BOOL finished) {
              // 更新偏移量
              CGPoint offset = self.scrollView.contentOffset;
              offset.x = index * self.scrollView.wx_width;
              [self.scrollView setContentOffset:offset];
              
              // 添加对应子控制器的view
              [self addChildVcViewIntoScrollView:index];
          }];
      }
      
      #pragma =======================================================================
      #pragma mark - UIScrollViewDelegate代理方法
      - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
      {
          // 1.获取索引
          NSInteger index = self.scrollView.contentOffset.x / self.scrollView.wx_width;
          
          // 2.根据索引获取按钮
          WXTitleButton *titleButton = self.titleView.subviews[index];
          
          // 3.调用按钮的点击事件
          [self dealTitleButtonClick:titleButton];
      }
    
  • 2.重复点击标题按钮下拉刷新

    • 设置偏移量将header偏移到titleView下面.
    • 连续重复点击时,如果正在刷新,就不需要再刷新.
    • 重复点击标题按钮代码参考
      // ----------------------------------------------------------------------------
      // 监听tabBarButton重复点击通知
      - (void)tabBarButtonDidRepeatClick
      {
          // ------------------------------------------------------------------------
          // 1.判断控制器的view有没有在window上,有没有和window重叠
          // 如果控制器的view不在window上,则直接返回
          if (self.view.window == nil) return;
          
          // 如果控制器的view没有和window重叠,则直接返回
          if (![self.view wx_intersectWithView:nil]) return;
          
          // ------------------------------------------------------------------------
          // 2.重复点击,执行下拉刷新
          // 2.1 判断当前是否在刷新,如果正在刷新直接退出
          if (self.isHeaderRefreshing) return;
          // 2.2 更新为刷新状态
          self.headerRefreshing = YES;
          self.headerLabel.text = @"正在刷新数据...";
          self.headerLabel.backgroundColor = [UIColor greenColor];
          
          // 2.3 设置内边距和偏移量,让header处于titleView的下面
          [UIView animateWithDuration:0.25 animations:^{
              
              // 2.3.1 修改内边距,增加顶部内边距
              UIEdgeInsets inset = self.tableView.contentInset;
              inset.top += self.header.wx_height;
              self.tableView.contentInset = inset;
              
              // 2.3.2 修改偏移量
              CGPoint offset = self.tableView.contentOffset;
              offset.y = -(WXNavMaxY + WXTitlesViewH + self.header.wx_height);
              [self.tableView setContentOffset:offset];
          }];
          
          // ------------------------------------------------------------------------
          // 3.请求数据, 延迟模拟
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
              [self loadNewTopics];
          });
      }
    
  • 最好考虑footer用addSubview添加,不用tableViewFooterView,不要占用tableViewFooterView.

4.苹果官方刷新控件

UIRefreshControl刷新控件

  • UIRefreshControl继承自UIControl,可以使用addTarget方法监听ValueChange事件.

  • 手动调用begin方法,不会触发ValueChange

  • 使用系统的刷新控件UIRefreshControl控件有个bug,在UIRefreshControl的刷新指示器正在刷新时切换到其他控制器,再返回控制器,会出现刷新指示器停止动画了.

    // TODO: 1.使用系统的刷新控件UIRefreshControl, UIRefreshControl控件有个问题,在UIRefreshControl的刷新指示器
    // 正在刷新时切换到其他控制器,再返回控制器,会出现刷新指示器停止动画了.
    UIRefreshControl *header = [[UIRefreshControl alloc] init];
    [header addTarget:self action:@selector(loadNewTopics) forControlEvents:UIControlEventValueChanged];
    [self.tableView addSubview:header];
    self.header = header;

5.使用MJRefresh刷新框架

MJRefresh的使用

  • MJRefresh的类结构图

    • MJRefresh刷新框架中上拉刷新有2个类,下拉刷新有4个类可以使用.
      • 上拉刷新: MJRefreshNormalHeader, MJRefreshGifHeader
      • 下拉刷新: MJRefreshBackNormalFooter, MJRefreshBackGifFooter,MJRefreshAutoNormalFooter,MJRefreshAutoGifFooter
    04.MJRefresh的类结构图.png
  • 下拉scrollView时或程序进入时开始刷新,当请求完成/失败时结束刷新.

    • 开始刷新: [self.tableView.mj_header beginRefreshing];
    • 结束刷新: [self.tableView.mj_footer endRefreshing];
  • 统一设置下拉刷新显示内容.

    • 使用继承MJRefreshNormalHeader的自定义类WXRefreshHeader.
    • 使用重写框架的prepare方法系统的initWithFrame方法初始化.封装下拉控件的属性设置,无需每个地方都设置.
    • 使用方式,只需导入自定义类WXRefreshHeader.h,设置tableView的mj_header属性为自定义类WXRefreshHeader即可
    // WXRefreshHeader.m
    // ----------------------------------------------------------------------------
    // 初始化子控件
    - (void)prepare
    {
        [super prepare];
        
        // ------------------------------------------------------------------------
        // 1.设置刷新控件属性
        // 1.1 设置自动改变透明度
        self.automaticallyChangeAlpha = YES;
        // 1.2 设置隐藏时间
        self.lastUpdatedTimeLabel.hidden = YES;
        // 1.3 设置状态文字颜色
        self.stateLabel.textColor = [UIColor orangeColor];
        
        // ------------------------------------------------------------------------
        // 2.设置刷新状态文字
        [self setTitle:@"下拉刷新" forState:MJRefreshStateIdle];
        [self setTitle:@"松开🐴上刷新" forState:MJRefreshStatePulling];
        [self setTitle:@"正在玩命刷新中..." forState:MJRefreshStateRefreshing];
        
        // ------------------------------------------------------------------------
        // 添加loginImageView
        UIImageView *loginImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"MainTitle"]];
        [self addSubview:loginImageView];
        self.loginImageView = loginImageView;
    }


    // ------------------------------------------------------------------------
    // 使用方式, 设置为tableView的mj_header属性,下拉刷新调用loadNewTopics方法加载网络数据.
    self.tableView.mj_header = [WXRefreshHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadNewTopics)];
    // 开始刷新
    [self.tableView.mj_header beginRefreshing];
  • 添加下拉刷新显示公司logo图片

    • placeSubviewslayoutSubviews中重新布局loginImageView
      // ----------------------------------------------------------------------------
      // 布局子控件
      - (void)placeSubviews
      {
          [super placeSubviews];
          
          // ------------------------------------------------------------------------
          // 布局loginImageView
          self.loginImageView.wx_centerX = self.wx_width * 0.5;
          self.loginImageView.wx_centerY = - self.loginImageView.wx_height;
          
      }
    

补充

  • 写UI框架考虑控件成为属性

  • NS_REQUIRES_SUPER: 如果在方法后面使用NS_REQUIRES_SUPER,该方法重写时必须调用父类的方法,如果没有调用父类方法[super prepare]会有警告.

    // 如果在方法后面使用NS_REQUIRES_SUPER,该方法重写时必须调用父类的方法
    - (void)prepare NS_REQUIRES_SUPER;
    

AFNetworking的使用

  • 自定义继承AFHTTPSessionManager的子类WXHTTPSessionManager

    • 在子类中统一设置请求会话管理者的属性
    • 重写initWithBaseURL:sessionConfiguration:方法,在方法中对会话管理者进行设置.
    • 设置请求头,在请求头可以添加设备类型和系统版本方法,参考代码如下所示
      // ----------------------------------------------------------------------------
      // 初始化设置会话管理者
      - (instancetype)initWithBaseURL:(NSURL *)url sessionConfiguration:(NSURLSessionConfiguration *)configuration
      {
          if (self = [super initWithBaseURL:url sessionConfiguration:configuration]) {
              
              // 设置请求头
              [self.requestSerializer setValue:@"iPhone 6s" forHTTPHeaderField:@"device"];
              [self.requestSerializer setValue:@"9.2" forHTTPHeaderField:@"version"];
              
          }
          return self;
      }
    
  • 使用自定义类WXHTTPSessionManager执行网络请求,用法和AFHTTPSessionManager一样.

    • 查看请求头信息
      self.mgr.requestSerializer.HTTPRequestHeaders

使用第三方框架时子类化

使用第三方框架时如果能子类化,最好子类化封装第三方框架,这样如果第三方框架更新,只需更改自定义封装的子类即可.

github使用

  • Pull requests: 别人提交
    • 紫色代表有整合进去
    • 绿色代表有待审核
    • 红色代表没有处理
  • 点击蓝色线,查看使用的编程语言
05.查看使用的编程语言.png

点击后显示使用语言的比例

06.显示使用语言的比例.png
  • CocoaPods如果没指定版本号,默认是最新的版本.
    例如: pod 'AFNetworking'

  • 第三方框架.podspec文件里面描述最新版本等信息.

    • version表示版本号.
      Pod::Spec.new do |s|
      s.name     = 'AFNetworking'
      s.version  = '3.0.4'
      s.license  = 'MIT'
      s.summary  = 'A delightful iOS and OS X networking framework.'
      s.homepage = 'https://github.com/AFNetworking/AFNetworking'
      s.social_media_url = 'https://twitter.com/AFNetworking'
      s.authors  = { 'Mattt Thompson' => 'm@mattt.me' }
      s.source   = { :git => 'https://github.com/AFNetworking/AFNetworking.git', :tag => s.version, :submodules => true }
      s.requires_arc = true
      
      s.public_header_files = 'AFNetworking/AFNetworking.h'
      s.source_files = 'AFNetworking/AFNetworking.h'
    
  • github下载指定版本

    • 在release中下载指定版本
07.在release中下载指定版本.png

真机测试问题

  • 登录界面一点程序崩溃
  • 在精华控制器点击顶部状态栏区域,没反应,模拟器有反应
  • 推荐标签订阅按钮不好点
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容