[iOS] iOS布局适配
这篇文章真的是拖拖拖拖拖了好久,因为自己也没有什么成熟的经验,在看了超多文章以后大概总结一下,心里非常虚吖。。
#define kIS_IPHONE_3_5_INCH (kSCREEN_WIDTH == 320.f && kSCREEN_HEIGHT == 480.0f)//4(S) / 3GS
#define kIS_IPHONE_4_0_INCH (kSCREEN_WIDTH == 320.f && kSCREEN_HEIGHT == 568.0f)//5(C)/ 5(S)
#define kIS_IPHONE_4_7_INCH (kSCREEN_WIDTH == 375.f && kSCREEN_HEIGHT == 667.0f)//6(S) / 7 / 8
#define kIS_IPHONE_5_5_INCH (kSCREEN_WIDTH == 414.f && kSCREEN_HEIGHT == 736.0f)//6(S)+ / 7+ / 8+
#define kIS_IPHONE_5_15_INCH (kSCREEN_WIDTH == 375.f && kSCREEN_HEIGHT == 812.f)//X XS
#define kIS_IPHONE_6_1_INCH (kSCREEN_WIDTH == 414.f && kSCREEN_HEIGHT == 896.f)//XR XSMax
iOS最开始的布局定位都是靠frame,也就是坐标和宽高(x.y.heiight.width),任何一个view你都得固定写死它的frame,然而这样的话,当屏幕尺寸多种多样的时候就非常难过了,于是出现了AutoLayout,所以先看两个背景知识吧~
Base1: AutoLayout
简单的说,autolayout将原来的固定frame改为了用相对位置来计算,我们现在经常用到constraint就属于autolayout。
举个栗子哈,如果定位一个按钮距离屏幕两侧都是32点,那么只要设定:
button.leading = superview.leading + 32
button.trailing = superview.trailing + 32
如果没有autolayout,那么我们就需要每种屏幕都用以下的方式定位:
if (kIS_IPHONE_4_7_INCH) {
button.frame = CGRectMake(32, 0, 375 - 32 - 32, 48); // y坐标和height是随便写的
} else if (kIS_IPHONE_5_5_INCH) {
button.frame = CGRectMake(32, 0, 414 - 32 - 32, 48);
} else {
……
}
这么看的话autolayout其实已经帮我们解决很多问题了,所以其实约束超级有用,但是有的时候不是所有屏幕的button都距离两侧是32个点,如果希望在iphone6/7/8上面是32点,但是其他屏幕根据屏幕宽度比例进行调整,那么就涉及了屏幕适配。
例如:
iPhone 6/7/8/X/XS 宽度375 -> 32点
iPhone 6+/7+/8+/Xr/Xs max 宽度414 -> 32/375*414 = 35.328点
iPhone 4/5 宽度320 -> 32/375*320 = 27.306点
P.S. 这里强烈不建议每个屏幕的距离设定是没有比例关系并且不一致的,这样就会导致开发每种屏幕都要if然后设定constraint数值,非常的麻烦不好维护,出任何一种新的屏幕的时候都非常要命。。
Base2: SizeClass
sizeClass可以用于区分横竖屏、iPAD之类的,使用场景例如你可以建好2个constraint,一个用于横屏,一个用于竖屏,具体可参考:https://www.jianshu.com/p/0b91341fead4,这个我之前有用来做extension的横竖屏适配。
屏幕适配我看了蛮多文章的,大意上分两种方式:(字体适配另说哦)
- 手写布局+比例适配
- xib+比例适配
还有一些神奇的方法,比如借鉴了web适配的flexlib,具有热更新的优点,只是因为改起来容易冲突所以并不推荐。
比较common的是,无论怎样都是基于比例适配,也就是设计师只要给一个iphone8的(具体尺寸看各个公司的偏好了)设计稿,然后所有间距、宽高都按照比例缩放。
但是不是所有控件的尺寸都应该按照屏幕大小等比放大的哈,具体的适配理念主要是三种:文字流式、控件弹性、图片等比缩放
所以设计师需要确定每一种控件的布局规则,要不dev做出来的可能和他们设想的不一样,导致折返跑。
首先common其实都是比例适配,下面我们来探讨一下如何做到比例适配啦~ 虽然其实很简单就是按比例计算一下上下左右应该是多少。。
#define kIPHONE8_WIDTH 375.0f
#define kIPHONE8_HEIGHT 667.0f
// 水平or竖直方向
+ (CGFloat)horizontalAdapter:(CGFloat)constant {
return constant * ([UIScreen mainScreen].bounds.size.width / kIPHONE8_WIDTH);
}
+ (CGFloat)verticalAdapter:(CGFloat) constant {
return constant * ([UIScreen mainScreen].bounds.size.height / kIPHONE8_HEIGHT);
}
上下有一点要注意的就是刘海屏的适配,一般可以考虑用safeArea作为屏幕尺寸,或者以全屏view作为适配尺寸后单独处理刘海。

Step 1:首先得加入顶部和底部margin
+ (CGFloat)topAdapter:(CGFloat)top {
if (kIS_IPHONE_5_15_INCH || kIS_IPHONE_6_1_INCH) {
return top + 44;
}
return top;
}
+ (CGFloat)bottomAdapter:(CGFloat)bottom {
if (kIS_IPHONE_5_15_INCH || kIS_IPHONE_6_1_INCH) {
return bottom + 34;
}
return bottom;
}
Step 2:可选部分,是不是以不算marigin的区域作为比例缩放的标尺
如果你想获取safeArea的大小,可以用下面的方式得到,但是时机有点晚所以我会在代码里面写死宏:
float vertocalInset = self.view.safeAreaInsets.top + self.view.safeAreaInsets.bottom;
NSLog(@"vertocalInset :%f", vertocalInset);
输出 vertocalInset :78.000000
注意不是刘海屏其实也有safeArea哈,就是顶部状态栏,大概20个点,这个取决于你的app是不是有状态栏了,如果没有的话可以不管这个20点,有的话需要减去。
#define kIPHONEX_VERTICAL_INSET 78.0f
#define kIPHONE_VERTICAL_INSET 20.0f
+ (CGFloat)safeAreaVerticalAdapter:(CGFloat)constant {
if (kIS_IPHONE_5_15_INCH || kIS_IPHONE_6_1_INCH) {
return constant * (([UIScreen mainScreen].bounds.size.height - kIPHONEX_VERTICAL_INSET) / (kIPHONE8_HEIGHT - kIPHONE_VERTICAL_INSET));
}
return constant * ([UIScreen mainScreen].bounds.size.height / kIPHONE8_HEIGHT);
}
1. 手写布局+比例适配
手写布局有很多好处,例如性能更好,但是没有xib看起来明了,所以写的时候可能只是稍微有点痛苦,布局一旦复杂起来,后面改的人就是相当痛苦。。
其实关键还是比例适配,手写布局的实现太多啦,正常的话就像下面这样写就可以啦:
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake([AdapterUtils horizontalAdapter:20], [AdapterUtils topAdapter:[AdapterUtils safeAreaVerticalAdapter:20]], [AdapterUtils horizontalAdapter:40], [AdapterUtils safeAreaVerticalAdapter:40])];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
这样是不是看起来有一点麻烦?也可以将CGRectMake写成一个util的函数,然后每次只要调用自己定义的CGRectMake就可以啦。
CG_INLINE CGRect
CGRectMakeAdapater(CGFloat x, CGFloat y, CGFloat width, CGFloat height)
{
CGRect rect;
rect.origin.x = [AdapterUtils horizontalAdapter:x];
rect.origin.y = [AdapterUtils topAdapter:[AdapterUtils safeAreaVerticalAdapter:y]];
rect.size.width = [AdapterUtils horizontalAdapter:width];
rect.size.height = [AdapterUtils safeAreaVerticalAdapter:height];
return rect;
}
但是有的时候可能并不是上下左右都是按比例的哈,大部分其实长宽比应该保持不变的。
效果就是酱紫啦:(左:Xr 中:8 右:5)


比较明显的可以看出来如果不通过屏幕比例缩放的适配前的图中,40*40的方块在iphone5上面过大,在iphone Xr上面过小。
2. xib布局+比例适配
通过xib直接做其实就是通过category以及runtime动态在运行时设置adapt后的constraint。
#import <objc/runtime.h>
#import "NSLayoutConstraint+Adapter.h"
#import "AdapterUtils.h"
@implementation NSLayoutConstraint (Adapter)
- (void)setHorizontalAdapter:(BOOL)horizontalAdapter {
if (horizontalAdapter) {
self.constant = [AdapterUtils horizontalAdapter:self.constant];
}
objc_setAssociatedObject(self, @selector(horizontalAdapter), @(horizontalAdapter), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)horizontalAdapter {
NSNumber *value = objc_getAssociatedObject(self, @selector(horizontalAdapter));
return value.boolValue;
}
- (void)setVerticalAdapter:(BOOL)verticalAdapter {
if (verticalAdapter) {
self.constant = [AdapterUtils verticalAdapter:self.constant];
}
objc_setAssociatedObject(self, @selector(verticalAdapter), @(verticalAdapter), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)verticalAdapter {
NSNumber *value = objc_getAssociatedObject(self, @selector(verticalAdapter));
return value.boolValue;
}
- (void)setTopAdapter:(BOOL)topAdapter {
if (topAdapter) {
self.constant = [AdapterUtils topAdapter:self.constant];
}
objc_setAssociatedObject(self, @selector(topAdapter), @(topAdapter), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)topAdapter {
NSNumber *value = objc_getAssociatedObject(self, @selector(topAdapter));
return value.boolValue;
}
- (void)setBottomAdapter:(BOOL)bottomAdapter {
if (bottomAdapter) {
self.constant = [AdapterUtils bottomAdapter:self.constant];
}
objc_setAssociatedObject(self, @selector(bottomAdapter), @(bottomAdapter), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)bottomAdapter {
NSNumber *value = objc_getAssociatedObject(self, @selector(bottomAdapter));
return value.boolValue;
}
当我们把新写的category引入以后,在布局界面就能看到下图这样的选项:(右上)

适配后的效果如下:

字体适配
其实原理和view的很像,如果直接按屏幕的话可以像下面酱紫:
+ (CGFloat)fontSizeAdapter:(CGFloat)fontSize {
return fontSize * ([UIScreen mainScreen].bounds.size.width / kIPHONE8_WIDTH);
}
但我们设计师的需求一般都是se减2号,Xr和Max系列加1号,这种情况就要判断一下啦~
+ (CGFloat)fontSizeAdapter:(CGFloat)fontSize {
if (kIS_IPHONE_6_1_INCH || kIS_IPHONE_5_15_INCH) {
return fontSize + 1;
}
if (kIS_IPHONE_4_0_INCH || kIS_IPHONE_3_5_INCH) {
return fontSize - 2;
}
return fontSize;
}
还可以自定义fontWithName,这样每次用的时候用自己自定义的比较方便。
其实还有一个最最最傻的方式,也是我们之前用的方式,如果有哪些屏幕特别别扭,那么就特殊判断一下然后改constant,但及其不推荐啦。屏幕适配其实还是以设计师为主的,他们需要指出每个屏幕的适配准则,是比例还是固定宽高比,亦或是固定距离无论屏幕多大。
如果大家有好的方法欢迎私信或留言,非常感激!
参考: