iOS开发

iPad小案例 -- QQ空间界面

2016-06-04  本文已影响573人  面糊

一. iPad的一些常识

  1. iPad的屏幕尺寸和分辨率

    • 建议没有做过iPad适配的同学, 在苹果官方文档查看一下不同型号iPad的尺寸
    • 注意一下点与像素的区别, 尤其是Retina屏幕
  2. iPhone和iPad开发之间的一些区别

    • iPad的屏幕相比iPhone较大, 因此能容纳更多内容, 并且多数iPad的UI排布都是左右分屏
    • iPad的键盘多了一个退出键盘的按钮, 因此不需要手动写endEditing这样的代码了
    • API:
      • 基本所有的API, 在iPad和iPhone上都是共用的, 只是有些效果会有些不同
      • iPad比iPhone多了一些特有类: 如UIPopoverControllerUISplitViewController, 这两个类分别对应下拉菜单和分屏
    • 屏幕方向
      • iPhone只有三个方向: 即竖屏, 左横屏和右横屏(一般App只需要竖屏)
      • iPad则支持四个方向: 竖屏, 返转竖屏, 左横屏和有横屏(苹果官方建议, 最好同时支持横竖屏两个方向)

二. 项目分析

  1. iPad的QQ空间, 使用的是分屏模式, 即左侧dock边栏, 右侧content内容展示

  2. 本项目比较麻烦的点就在于, 在转换屏幕的时候, 如何设置好dockcontent的约束

    • 尺寸大小发生改变
    • 部分控件的排列结构也会发生改变: 如横向排列改为纵向排列
    • 按钮的状态变化: 横竖屏不同的状态, 按钮展示的内容也不同(只展示图片/图文展示)
  3. 方案选择(布局方式):

    1. Autoresizing
      • 该方案通常用来解决父控件子控件之间相对关系的问题
      • 因此Autoresizing只能改变父子控件的相对位置/尺寸, 但是当横竖屏发生变化时, 无法重新排列控件
      • 从Autoresizing的各种属性也能得知, 他是更改相对位置和尺寸
      • UIViewAutoresizingFlexibleLeftMargin等等
    2. Autolayout
      • Autolayout是从iOS6.0之后推出的布局方案, 用于解决相对控件的位置/尺寸问题
      • 他可以很便捷的布局控件之间的约束关系, 并且可以让控件之间产生耦合, 互相耦合的控件可以一同发生改变
      • 但他和Autoresing有相同的缺点, 就是不能改变控件的排列方式(横向排列改为纵向排列)
      • 并且, 控件之间的耦合性过强, 一旦控件发生改变, 和他有关联的控件都要变
    3. UIStackView
      • iOS9.0后退出的新控件, 他可以快速的将一些控件进行水平/垂直排布, 并且可以设置间距
      • 控件之间的耦合性很弱, 耦合性只针对于控件和StackView之间产生
      • 一般用于实现一些简单的水平/垂直排列
      • iOS9.0+才能使用, 对于目前普遍从iOS8.0开始适配来说, 该方案不可选
    4. Sizeclass
      • iOS8.0推出的功能, 可以解决不同屏幕状态下的排列方式和重设控件的内容样式
      • Sizeclass最大的优点就在于他可以区分不同的屏幕尺寸来设置约束, 但是详细设置还要配合其他方案
      • 无法区分iPad的横竖屏!!!
    5. 纯代码
      • 较为繁琐, 很麻烦, 鄙人比较不喜欢纯代码的方式
      • 优势: 优秀的代码风格, 可以让你的控件耦合性很低, 并且复用性很高
      • 对于iPad这种横竖屏发生变化后, 控件的位置/大小/内容都会发生改变的情况下, 最优选择就是纯代码搭建

三. 登录界面搭建

  1. 此项目的重点在于如何做好iPad情况下的横竖屏适配, 所以业务逻辑并没有实现

  2. 登录界面的重点:

    • 对于横竖屏而言, 最好不要将约束参照设置为屏幕的四边, 当屏幕旋转时, 屏幕尺寸会产生很大的变化, 导致控件拉伸会很严重
    • 在开发的过程中, 一定要记得, 尽量将可复用的功能抽取为工具类!!也就是封装思想!! + 在封装工具类时, 涉及的block的回调
  3. 具体实现部分

    • 登录的工具类的封装(block的回调)

        @implementation QQLoginTool
        + (void)loginByAccount:(NSString *)account password:(NSString *)password result:(void (^)(BOOL))loginResult {
            
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                
                if ([account isEqualToString:@"qq"] && [password isEqualToString:@"123"]) {
                    
                    loginResult(YES);
                } else {
                    
                    loginResult(NO);
                }
            });
        }
        @end
      
    • 登录的执行

        - (IBAction)loginBtnClick:(id)sender {
            
            // 0. 开始加载动画
            [self.loginLoading startAnimating];
            
            // 1. 判断是否内容完全
            _loginBtn.enabled = (_accountTF.text.length && _pwdTF.text.length);
            
            // 2. 判断内容密码是否正确
            [QQLoginTool loginByAccount:_accountTF.text password:_pwdTF.text result:^(BOOL isSuccess) {
                
                if (isSuccess) {
                    // 登录成功跳转界面
                    QQHomeViewController *homeVC = [[QQHomeViewController alloc] init];
                    [UIApplication sharedApplication].keyWindow.rootViewController = homeVC;
                } else {
                    // 登录失败, 执行弹跳动画
                    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.x"];
                    animation.values = @[@-50, @0, @50, @0];
                    animation.duration = 0.2;
                    animation.repeatCount = 3;
                    [_animationView.layer addAnimation:animation forKey:@"error"];
                    
                    // 提示账号密码错误
                    [QQShowMessageTool showErrorMessage:@"账号或密码错误!"];
                    
                    // 3. 停止加载动画
                    [self.loginLoading stopAnimating];
                }
            }];
        }
      

三. 主页的实现

  1. 重点

    • dockViewcontentView在切换横竖屏时的适配
    • 对于一整块具有多个子界面的View, 最好的办法是将其划分成不同的区域, 并且抽出不同的类
      • QQDockTopView
      • QQDockMiddleView
      • QQDockBottomView
    • 对于一些固定的取值(横竖屏状态下的dock和contentView的尺寸), 单独放在一个类或PCH文件中, 方便管理
  2. dockViewcontentView的布局

    • 在切换横竖屏之后, 屏幕的尺寸就会发生改变, 而这时候我们要获取屏幕准确的尺寸, 来重新布局控件
      • -viewWillLayoutSubviews
      • -viewDidLayoutSubviews
      • 经过检测, 以上两个方法, 是在屏幕转动之后, 获取屏幕尺寸最准确的方法, 所以更新控件frame的方法应该放在这里
    • 鄙人在处理控件不同状态下的尺寸时, 使用的是单例模型, 苹果公司建议大家尽量少用PCH文件的
      • 创建一个QQHomeViewFrameItem模型类, 并且制作为单例

      • 在这个类中, 判断当前屏幕的横竖屏状态

      • 根据横竖屏状态获取各个控件的尺寸/位置值

          #import "QQHomeViewFrameItem.h"
          
          static QQHomeViewFrameItem *_frameItem;
          
          @implementation QQHomeViewFrameItem
          
          #pragma mark - 确定dockView的宽度
          
          + (instancetype)shareFrameItem {
              
              static dispatch_once_t onceToken;
              dispatch_once(&onceToken, ^{
                  _frameItem = [[QQHomeViewFrameItem alloc] init];
              });
              
              return _frameItem;
          }
          
          - (BOOL)isLandScape {
              
              CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
              CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height;
              
              return (screenWidth > screenHeight);
          }
          
          - (CGFloat)dockWidth {
              
              return (self.isLandScape ? 210 : 70);
          }
          // 等等控件的尺寸位置.....
          @end        
        
  3. dockView的一些重点

    • dockView在切换横竖屏之后, 内部的控件也会相应的发生变化, 所以要在layoutSubviews方法中, 重新布局子控件
    • dockMiddleView中的按钮, 在横屏的时候显示图片+文字, 而在竖屏的时候只显示图片, 因此要自定义按钮
      • - (CGRect)titleRectForContentRect:(CGRect)contentRect

      • - (CGRect)imageRectForContentRect:(CGRect)contentRect

      • 给上面两个方法增加一个判断, 根据横竖屏的状态来设置按钮的图片/文字的布局

          // 个人认为对于设置按钮内容布局来说, 非常实用的方法
          - (CGRect)titleRectForContentRect:(CGRect)contentRect {
              
              if (self.frameItem.isLandScape) {
                  return CGRectMake(contentRect.size.width * radio, 0, contentRect.size.width * (1 - radio), contentRect.size.height);
              } else {
                  return CGRectZero;
              }
          }
          
          - (CGRect)imageRectForContentRect:(CGRect)contentRect {
              
              if (self.frameItem.isLandScape) {
                  return CGRectMake(0, 0, contentRect.size.width * radio, contentRect.size.height);
              } else {
                  return contentRect;
              }
          }
        
    • 按钮监听的传递
      • 在日常开发中, MVC设计模式, 一定要终于谁的事情交给谁处理, 而这里的麻烦点就在于按钮是被View包装起来的, 并且View还有一个dockView来包装, 但是按钮点击的方法实现应该交由控制器来管理

      • 因此这里要做多层传递, 最简单的办法是使用通知来跨层传递

      • 但鄙人这里使用的是代理传递: 将一个控件的代理, 赋值给另一个控件的代理, 然后交由控制器管理

          #pragma mark - QQDockBottomView
          - (void)addBtn {
              
              NSArray *imageNames = @[@"tabbar_blog", @"tabbar_mood", @"tabbar_photo"];
              
              for (int i = 0; i < imageNames.count; i++) {
                  
                  UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
                  
                  [btn setImage:[UIImage imageNamed:imageNames[i]] forState:UIControlStateNormal];
                  
                  btn.tag = i;
                  [btn addTarget:self action:@selector(btnClicked:) forControlEvents:UIControlEventTouchUpInside];
                  
                  [self addSubview:btn];
              }
          }
          
          - (void)btnClicked:(UIButton *)btn {
              
              if ([self.delegate respondsToSelector:@selector(dockBottomViewClickButtonWithType:)]) {
                  
                  [self.delegate dockBottomViewClickButtonWithType:btn.tag];
              }
          }
          
          #pragma mark - QQDockView
          // 这里要重写Setter, 将代理传给外界
          - (void)setDelegate:(id<QQdockViewDelegate>)delegate {
              
              _delegate = delegate;
              
              // 将代理传给Dock的代理
              self.middleView.delegate = _delegate;
              self.bottomView.delegate = _delegate;
          }
          
          #pragma mark - QQDockViewController
          // 传递给控制器, 由控制器来实现代理方法, 监听按钮的点击
          - (void)dockBottomViewClickButtonWithType:(DockBottomViewButtonType)type {
              
              switch (type) {
                  case DockBottomViewButtonTypeRizhi:
                      NSLog(@"日志");
                      break;
                  case DockBottomViewButtonTypeShuoshuo:
                      NSLog(@"说说");
                      break;
                  case DockBottomViewButtonTypeCamera:
                      NSLog(@"相机");
                      break;
              }
          }
        

小结:

  1. iPad开发麻烦的地方就在于横竖屏的适配问题
  2. 在这个案例中使用了一次代理传递, 比较不好理解
  3. 对于比较复杂并且多变的控件, 笔者建议大家使用纯代码的方式来搭建, 比较灵活, 也便于维护
  4. iOS开发多注重封装思想, 能抽调出来的功能一定要封装为一个工具类
  5. 但是, 不要为了解耦去过度解耦, 弄得自己都混乱了
1EAA6434-0B24-4EEB-B96F-E1701997DEDA.png
507EB7CF-7939-41DB-AD37-B3473AF8F113.png
4FDEB061-A77E-421E-B5FC-4B80606B252E.png
上一篇 下一篇

猜你喜欢

热点阅读