一. iPad的一些常识
-
iPad的屏幕尺寸和分辨率
- 建议没有做过iPad适配的同学, 在苹果官方文档查看一下不同型号iPad的尺寸
- 注意一下点与像素的区别, 尤其是Retina屏幕
-
iPhone和iPad开发之间的一些区别
- iPad的屏幕相比iPhone较大, 因此能容纳更多内容, 并且多数iPad的UI排布都是左右分屏
- iPad的键盘多了一个退出键盘的按钮, 因此不需要手动写
endEditing
这样的代码了 - API:
- 基本所有的API, 在iPad和iPhone上都是共用的, 只是有些效果会有些不同
- iPad比iPhone多了一些特有类: 如
UIPopoverController
和UISplitViewController
, 这两个类分别对应下拉菜单和分屏
- 屏幕方向
- iPhone只有三个方向: 即竖屏, 左横屏和右横屏(一般App只需要竖屏)
- iPad则支持四个方向: 竖屏, 返转竖屏, 左横屏和有横屏(苹果官方建议, 最好同时支持横竖屏两个方向)
二. 项目分析
iPad的QQ空间, 使用的是分屏模式, 即左侧
dock
边栏, 右侧content
内容展示-
本项目比较麻烦的点就在于, 在转换屏幕的时候, 如何设置好
dock
和content
的约束- 尺寸大小发生改变
- 部分控件的排列结构也会发生改变: 如横向排列改为纵向排列
- 按钮的状态变化: 横竖屏不同的状态, 按钮展示的内容也不同(只展示图片/图文展示)
-
方案选择(布局方式):
- Autoresizing
- 该方案通常用来解决父控件和子控件之间相对关系的问题
- 因此Autoresizing只能改变父子控件的相对位置/尺寸, 但是当横竖屏发生变化时, 无法重新排列控件
- 从Autoresizing的各种属性也能得知, 他是更改相对位置和尺寸
UIViewAutoresizingFlexibleLeftMargin等等
- Autolayout
- Autolayout是从iOS6.0之后推出的布局方案, 用于解决相对控件的位置/尺寸问题
- 他可以很便捷的布局控件之间的约束关系, 并且可以让控件之间产生耦合, 互相耦合的控件可以一同发生改变
- 但他和Autoresing有相同的缺点, 就是不能改变控件的排列方式(横向排列改为纵向排列)
- 并且, 控件之间的耦合性过强, 一旦控件发生改变, 和他有关联的控件都要变
- UIStackView
- iOS9.0后退出的新控件, 他可以快速的将一些控件进行水平/垂直排布, 并且可以设置间距
- 控件之间的耦合性很弱, 耦合性只针对于控件和StackView之间产生
- 一般用于实现一些简单的水平/垂直排列
- iOS9.0+才能使用, 对于目前普遍从iOS8.0开始适配来说, 该方案不可选
- Sizeclass
- iOS8.0推出的功能, 可以解决不同屏幕状态下的排列方式和重设控件的内容样式
- Sizeclass最大的优点就在于他可以区分不同的屏幕尺寸来设置约束, 但是详细设置还要配合其他方案
- 无法区分iPad的横竖屏!!!
- 纯代码
- 较为繁琐, 很麻烦, 鄙人比较不喜欢纯代码的方式
- 优势: 优秀的代码风格, 可以让你的控件耦合性很低, 并且复用性很高
- 对于iPad这种横竖屏发生变化后, 控件的位置/大小/内容都会发生改变的情况下, 最优选择就是纯代码搭建
- Autoresizing
三. 登录界面搭建
此项目的重点在于如何做好iPad情况下的横竖屏适配, 所以业务逻辑并没有实现
-
登录界面的重点:
- 对于横竖屏而言, 最好不要将约束参照设置为屏幕的四边, 当屏幕旋转时, 屏幕尺寸会产生很大的变化, 导致控件拉伸会很严重
- 在开发的过程中, 一定要记得, 尽量将可复用的功能抽取为工具类!!也就是封装思想!! + 在封装工具类时, 涉及的block的回调
-
具体实现部分
-
登录的工具类的封装(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]; } }]; }
-
三. 主页的实现
-
重点
-
dockView
和contentView
在切换横竖屏时的适配 -
对于一整块具有多个子界面的View, 最好的办法是将其划分成不同的区域, 并且抽出不同的类
- QQDockTopView
- QQDockMiddleView
- QQDockBottomView
- 对于一些固定的取值(横竖屏状态下的dock和contentView的尺寸), 单独放在一个类或PCH文件中, 方便管理
-
-
dockView
和contentView
的布局- 在切换横竖屏之后, 屏幕的尺寸就会发生改变, 而这时候我们要获取屏幕准确的尺寸, 来重新布局控件
- -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
- 在切换横竖屏之后, 屏幕的尺寸就会发生改变, 而这时候我们要获取屏幕准确的尺寸, 来重新布局控件
-
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; } }
-
小结:
- iPad开发麻烦的地方就在于横竖屏的适配问题
- 在这个案例中使用了一次代理传递, 比较不好理解
- 对于比较复杂并且多变的控件, 笔者建议大家使用纯代码的方式来搭建, 比较灵活, 也便于维护
- iOS开发多注重封装思想, 能抽调出来的功能一定要封装为一个工具类
- 但是, 不要为了解耦去过度解耦, 弄得自己都混乱了