iPhone屏幕尺寸这个话题,很多刚开始做iOS开发的同学觉得没什么好讲的:不就屏幕变大变小吗?但实际真刀真枪上手做适配时,才发现这里面的水比想象中深得多。同一个UIView,在旧机型上稳稳地贴在底部,到了带灵动岛的机器上直接跑进挖孔区域;设计稿上标的是750宽的尺寸,代码里写375刚好,换个机型就变了形。这篇文章我打算把这几年和屏幕尺寸打交道的心得整理一遍,从逻辑分辨率、缩放因子、安全区这些基础概念,到Auto Layout、Size Classes、切图规范、真机调试里的实际坑,一次性讲清楚。不管你是刚接触iOS开发,还是已经写了一阵子UI但总觉得适配心里没底,这篇都值得花十分钟读完。1. 为什么说屏幕尺寸是iOS开发的第一道坎1.1 从初代iPhone到灵动岛:尺寸演进路线iOS设备的屏幕尺寸其实是一路“折腾”过来的。最早大家手里那台3.5英寸屏幕,逻辑上只有320x480,后来iPhone 4把物理像素提到了640x960,但逻辑尺寸还是320x480,只是把画面做得更精细了。接下来是4英寸的iPhone 5,逻辑分辨率变成320x568,这也是iOS开发里第一次真正因为屏幕变长导致布局要重新考虑的节点。再往后4.7英寸的iPhone 6把逻辑宽度从320提到了375,5.5英寸的Plus机型宽度也是375但逻辑高度到了667。到这里很多人都形成了“宽度375、高度667”的思维定式,结果iPhone X一出来,带刘海、圆角、底部横条,逻辑分辨率变成375x812,高度一下子多出145pt。后面iPhone 12系列又引入"超大杯"概念,逻辑宽度变成390pt,iPhone 15 Pro Max更是到了430pt。这种持续变化意味着单纯靠几个固定坐标堆UI的方案根本活不长。很多人习惯用物理像素去理解屏幕,比如"我的手机是2556x1179分辨率",这在iOS开发里不是最直接的思考方式。因为UIKit内部所有布局用的都是point(pt),不是物理像素。物理像素只是最终显示在屏幕上的颜色点阵,pt则是逻辑上的坐标单位。系统在渲染时会把pt乘以缩放因子(scale factor)变成物理像素。iPhone 8的scale是2x,iPhone 14 Pro的scale是3x,所以你看到的分辨率数值高了,不代表UI坐标也要跟着写大,底层逻辑尺寸可能根本没变。1.2 屏幕尺寸的本质是逻辑分辨率,而不是物理分辨率我们查设备参数时常常看到一大串数字,像iPhone 15的2556x1179像素,但这些数字对写UI来说没有直接参考意义。真正决定布局的是逻辑分辨率。我把几代代表性机型的数据整理成了一张表,对照起来看会更清楚:机型代际逻辑分辨率(pt)缩放因子物理分辨率(px)典型代表早期3.5英寸320x4801x/2x320x480 / 640x960iPhone 3GS、iPhone 44英寸320x5682x640x1136iPhone 5、iPhone SE(第一代)4.7英寸375x6672x750x1334iPhone 6、iPhone 85.5英寸 Plus414x7363x1242x2208iPhone 6 Plus、iPhone 8 Plus5.8英寸刘海屏375x8123x1125x2436iPhone X、iPhone XS6.1英寸刘海屏390x8443x1170x2532iPhone 12、iPhone 13、iPhone 146.1英寸灵动岛393x8523x1179x2556iPhone 15、iPhone 166.7英寸 Pro Max430x9323x1290x2796iPhone 15 Pro Max、iPhone 16 Pro Max从这张表能看出来,逻辑宽度基本集中在320、375、390、393、414、430这几个档位。写代码时不需要针对每一台设备单独判断,只需要让布局在这几个宽度档位下都能合理伸缩就够了。这也是Auto Layout能成为标配的根本原因:屏幕尺寸不再是少数几种固定值,而是一个区间,必须用约束关系而不是死坐标来解决问题。2. 适配思路:从固定坐标到自动布局2.1 用pt不用px:缩放因子的来龙去脉接触iOS开发一段时间后你会发现,UI设计稿上经常标注750宽、1242宽这类数值,直观上看好像代码里也要写750。其实这个理解有个偏差:设计稿一般以物理像素为基准作图,而UIKit用的是pt。750px的设计稿在2x屏幕上对应375pt,在3x屏幕上对应250pt,如果直接拿px数值往Frame里填,整套界面都会偏大。正确做法是先搞清楚设计的基准宽度。现在主流设计规范通常以375pt或390pt为基准出图,这样开发时把标注值直接当成pt用就行。我自己经历过一个项目,设计那边按750px出稿,我早期没注意,直接把设计稿里的间距、字号原封不动写到代码里,结果跑在iPhone 8上整体宽了一倍,所有按钮都挤出了屏幕边缘。后来统一约定:设计稿标注px值除以缩放因子才是代码里要写的pt值。当前设计稿以2x为基准时,px=pt2;以3x为基准时,px=pt3。搞清楚这条换算关系,很多“怎么看尺寸都对、跑起来就是不对”的诡异问题都迎刃而解。理解了pt和px的关系,另一个关键就被牵扯出来:为什么iPhone X之后很多老代码会崩。因为之前大家默认底部就是屏幕边缘,用frame直接写死,或者用autoresizingMask设置固定间距。iPhone X引入了底部Home Indicator区域,屏幕底部有一段专门留给小白条的安全距离,同时还增加了顶部刘海区域。如果还是简单地把视图放在safe area之外,UI就会被系统手势区遮挡或干脆看不见。iOS 11之后苹果统一提供了安全区域这个概念,UIView的frame不再等于可视区域,适配时所有贴边元素都应该以safeAreaLayoutGuide为基准来计算。2.2 安全区域与刘海屏适配安全区域从iOS 11开始成为标配。通俗讲,屏幕的四个边缘可能被刘海、圆角、底部小白条遮挡,系统会划出一块不被遮挡的区域,这被称为Safe Area。顶部的刘海区影响比较大,比如状态栏从20pt涨到44pt甚至更高,写死20pt的代码在全面屏上会和电池时间图标重叠;底部则多出一个34pt的Home Indicator区域,写死0间距的底部按钮会被小白条盖住一半。代码里处理安全区,比较推荐的做法是使用safeAreaLayoutGuide。举个例子,如果想让一个按钮贴屏幕底部:let button = UIButton() view.addSubview(button) button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), button.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), button.heightAnchor.constraint(equalToConstant: 44) ])如果项目最低支持版本还没到iOS 11,通常会用if #available(iOS 11.0, *)判断,低版本直接用顶部/底部布局锚点即可。实际开发中,手动约束写起来相对冗长,我更习惯把几个常用的约束封装成工具函数,每次创建约束时传safeAreaLayoutGuide而不是view自身边界,减少重复代码的同时也不会漏掉适配。说到这,就不得不提XIB和Storyboard里的一个注意事项:很多人在Interface Builder中拖控件时,喜欢把约束直接绑定到view边缘,而不是Safe Area。这在小屏机器上看不出问题,一旦换到刘海屏,控件就会自动钻进刘海范围内。在XIB中正确做法是勾选“Use Safe Area Layout Guides”选项,让约束默认基于安全区生成。2.3 设计稿标注与开发视角的转换设计稿上的尺寸到底怎么翻译成代码里的值,这个问题困扰过不少新人。针对iOS开发,建议直接跟设计沟通:所有标注统一使用pt单位,设计软件里可以设置以375pt宽度为画板基准。这样前端和设计之间根本不需要换算,设计标多少,代码写多少。另一种情况是设计交付的是一张以750px为宽度的大图,这时候需要明确这是一张2x图还是3x图。iPhone 8时代的2x图和现在iPhone 15系列的3x图基准不一样。如果设计稿是1242px宽,对应的逻辑宽度是414pt,直接按标注值写没问题;如果是750px宽,对应的逻辑宽度是375pt。遇到以像素为单位的标注时,用“逻辑宽度 = 物理宽度 / 缩放因子”这个公式换算出pt值,再按pt去写布局,比较稳妥。字体大小也一样。不少设计习惯用像素标注字号,比如24px,到了代码里直接用24pt设置字号。这里有一个常见的误伤点:虽然数字一样,但设计是按2x或3x的物理像素出的图,开发按pt设置时,需要根据画板基准做一次除法。比如画板是以750px宽的2x尺寸出的图,24px对应12pt,设置字号应该是12pt,而不是24pt。如果画板是375pt宽,那24px就直接对应24pt。跟设计团队定好基准后,这类问题不再反复出现。3. 实操:一套代码适配全机型3.1 Auto Layout与Size Classes的操作要点Auto Layout的核心思想是让视图之间的相对关系决定位置和尺寸,而不是写死一个坐标。比如一个登录按钮,不约束它距屏幕顶部固定200pt,而是约束它距离上方Logo底部32pt、左右各16pt、高度44pt;屏幕变高时按钮自动下移,屏幕变宽时按钮自动拉伸或居中。这样iPhone SE和iPhone 15 Pro Max跑的是同一套约束,但UI看起来都是合理的。Size Classes是适配横竖屏和不同尺寸设备的另一个重要工具。它在iOS 8引入,把设备分成紧凑(Compact)和常规(Regular)两类,横屏iPhone是Compact宽度+Compact高度,竖屏iPhone是Compact宽度+Regular高度,iPad多数情况是Regular宽度+Regular高度。通过traitCollection可以判断当前环境,对不同Size Class做差异化布局。我们常用的一种场景是:竖屏时按钮堆叠排列,横屏时改成水平排列。override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.horizontalSizeClass == .compact { // 紧凑宽度,执行堆叠布局 } else { // 常规宽度,执行水平布局 } }Auto Layout的约束优先级也值得留意。两个约束都满足时系统会报约束冲突;当一个布局需要在不同情境下选择不同约束时,可以给约束设置不同的priority,比如800和750,系统自动选择更重要的那个。实际操作中,我给底部贴边约束设750,给最小间距约束设1000,小屏时拉伸间距,大屏时保证最小间距不破,比写死逻辑判断省心不少。3.2 代码中坐标与比例的通用处理虽然有Auto Layout,但有些场景还是需要手动算坐标,比如自定义绘制、动画路径、渐变图层等。这时可以用屏幕宽度做比例换算。主流做法是以设计基准宽度(通常是375)作为参考,计算出当前设备宽度与基准宽度的比例,然后按比例缩放间距、字号、控件尺寸。// 以375pt为基准宽度等比缩放 CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; CGFloat scale = screenWidth / 375.0; CGFloat buttonWidth = 120 * scale;这个思路简单直接,但有一个明显的缺点:iPhone 16 Pro Max的宽度是430pt,等比放大后120pt会变成137.6pt;反过来iPhone SE的320pt下会缩成102.4pt。视觉上虽然整体协调,但有些控件拉伸后看起来不够精致。因此等比缩放更适合用于间距、内边距、图标大小这类相对弹性元素,不适合用于固定大小的控件和必须保持清晰度的关键UI。更精细的做法是采用“非线性适配”,比如在小屏上取最小值,在超大屏上封顶:CGFloat scale = MIN(MAX(screenWidth / 375.0, 0.85), 1.15); CGFloat buttonWidth = 120 * scale;这个“MIN/MAX封顶”的思路很实用。小屏不至于缩得太小难点击,大屏也不至于放得太大显得空旷。很多成熟的第三方UI框架内部也是类似策略,只不过封装得更完善。3.3 SwiftUI下的自适应写法SwiftUI把适配做成了“默认能力”,但仍有技巧。ViewThatFits可以在空间不足以容纳首选布局时自动切换到备选布局,实现类似“塞得下就横排,塞不下就竖排”的效果:ViewThatFits(in: .horizontal) { HStack { Text("余额") Text("¥100") Button("充值") {} } VStack { Text("余额") Text("¥100") Button("充值") {} } }SwiftUI里还有GeometryReader用来读取父视图的尺寸,配合frame(maxWidth:)或containerRelativeFrame可以做出响应式布局。实际开发中SwiftUI的优势是写相同逻辑比UIKit代码量少得多,但在处理自定义导航栏和键盘避让时还是有不少细节。好在这几年系统版本迭代快,SwiftUI的成熟度已经能支撑生产环境,新项目用SwiftUI写列表、表单类页面体验很顺畅。4. 切图资源:2x与3x的配套规则4.1 @2x/@3x的命名与选择机制适配不只是代码上的事,切图资源也得按尺寸配套。iOS资源命名后缀有一套既定规则:默认无后缀的图用于1x设备,@2x后缀用于2x设备,@3x后缀用于3x设备。系统加载图片时,会根据设备的scale factor自动选择对应文件,不需要代码里判断设备类型。目前新发布的主流机型全是3x设备,所以提供@3x图是底线;旧机型数量不少,@2x图也建议保留。理论上1x图永远不会被用到,但还是建议预留一套,方便低版本系统或某些特殊屏幕使用。图片资源建议全部放进Asset Catalog里管理,Assets.xcassets会自动处理同一套图在不同scale下的匹配,构建时还会做必要的压缩优化。命名上我习惯统一用英文小写加下划线,比如icon_login_highlight@2x.png和icon_login_highlight@3x.png,方便脚本批量处理。4.2 Asset Catalog的Single Scale与矢量图Asset Catalog支持一种叫做Single Scale的模式,跟传统@2x/@3x三套图的方式不同,它只放一张矢量图(PDF或SVG),由系统在运行时按需渲染出任意尺寸。这样做的好处是图片在不同屏幕尺寸下始终清晰,不像点阵图放大后会模糊。它特别适合图标、背景纹理这类需要频繁缩放或固定展示尺寸的产品元素。使用Single Scale的流程比较固定:先在矢量绘图工具里导出PDF格式,拖入Assets.xcassets,在右侧属性面板中把Scale Filters改成Single Scale,然后设置好Preserve Vector Data。发布时iOS系统会自动针对不同scale生成相应分辨率。需要提醒的是,复杂的渐变或阴影效果用PDF矢量图导出时偶尔会有色差,这类资源还是用PNG点阵图更稳妥。项目结构示例: Assets.xcassets └── Icon.imageset ├── 图标.pdf (Single Scale) └── Contents.json图标切图还有一个常见场景:UIButton的backgroundImage。有些设计师提供一张规范的小图,但按钮实际尺寸随机型变化,直接拉伸会变形。这时用resizableImage(withCapInsets:resizingMode:)来指定可拉伸区域,或者用backgroundImage配合内容模式设置,避免图片被拉伸变形。4.3 图片拉伸与UIButton背景处理处理可拉伸图片我是这样做的。上线下一套背景图时,先确定需要保护的圆角范围,比如四角16pt半径的圆角矩形,中间区域是纯色或重复纹理,就可以创建可拉伸图片。直接用UIImage.resizableImage(withCapInsets:)设置内边距,然后赋值给Button:UIImage *original = [UIImage imageNamed:@"btn_bg"]; UIEdgeInsets insets = UIEdgeInsetsMake(16, 16, 16, 16); UIImage *resizable = [original resizableImageWithCapInsets:insets resizingMode:UIImageResizingModeStretch]; [button setBackgroundImage:resizable forState:UIControlStateNormal];这里的UIEdgeInsets参数不是随便拍的。上下左右的数值表示“不可拉伸区域”的范围,对应圆角的大小或特殊边框宽度。如果四个值都设成0,图片就是整体拉伸,圆角会被拉成大椭圆,看起来非常奇怪。一般上下值取圆角半径+2,左右值也取圆角半径+2,这样可以保证四角始终是完美圆弧。5. 我踩过的那些适配坑5.1 死值布局与键盘弹起老代码里最常见的问题是把布局写死在viewDidLoad里,用self.view.frame.size直接计算坐标。第一次运行没问题,但一旦屏幕旋转、键盘弹出、设备切换,frame就变了,写死的坐标全错位。一个很典型的场景是弹出键盘后输入框被键盘遮住。很多人第一反应是给view添加一个键盘通知监听,把整个view往上顶,一顿操作后发现各种问题。更稳妥的方法是结合Auto Layout。给输入框底部约束绑定到view.safeAreaLayoutGuide.bottom,键盘弹出时把约束的constant改成键盘高度,键盘收起时改回0,配合动画执行。这样只用更新一个约束值,不用手动移动整个view,未来屏幕尺寸变化也不会破。判断键盘高度时不要直接用屏幕上可见区域的变化量,不同机型、不同语言输入法的高度差异很大,建议用系统键盘通知里的keyboardFrameEndUserInfoKey值来换算,更准确。5.2 底部安全区与旋转方向全面屏出现后,底部间距的处理成了重灾区。不少界面底部有一条操作栏,有的开发习惯直接用bottomAnchor = view.bottomAnchor,结果被小白条遮住一半。正确做法是参考safeAreaLayoutGuide,或者把安全区的值动态读取出来:- (CGFloat)safeAreaBottom { if (@available(iOS 11.0, *)) { return self.view.safeAreaInsets.bottom; } return 0; }用safeAreaInsets还有一个好处是能处理横竖屏切换。横屏时底部安全区会变到屏幕两侧,如果在竖屏布局里写死34pt,旋转后侧边就会空出一片。直接用系统提供的值,代码不用感知旋转细节,系统自动更新。唯一需要注意的是,不要在viewDidLoad里读safeAreaInsets,此时布局还没有完成,拿到的值往往是0。正确时机是viewDidLayoutSubviews或首次布局完成后。5.3 真机调试中分辨率的误判今年我遇到过一个比较隐蔽的问题:代码在模拟器上所有机型看起来都正常,但真机一跑,图片变模糊,位置错了一两个点。排查后才发现模拟器默认渲染scale和真机不完全一样,尤其是用模拟器非Retina模式渲染时,很多色值和坐标会有细微偏差。后来我定了一个规矩:所有涉及精确对齐、图片临界尺寸的页面,必须拿真机跑一遍,不能只依赖模拟器。真机上最容易发现的问题是“Point精度和像素精度不一致”。比如一个View的宽度设成了0.5pt的奇数倍,在2x设备上会显示成奇数个物理像素,系统渲染时可能自动做round处理,导致视图边缘发虚。这类问题在模拟器上不明显,真机上则能明显看到边界锯齿。解决办法是给视图frame取整:CGFloat pixelAlign = floor(value * scale) / scale;当发现屏幕尺寸不同导致某些控件位置错乱时,我通常会在调试控制台打印当前设备和安全区信息:print("screen: \(UIScreen.main.bounds)") print("safeArea: \(view.safeAreaInsets)") print("scale: \(UIScreen.main.scale)")这样对比真机输出和模拟器输出,能很快定位是设备判断逻辑出错还是约束缺少。我把这几年碰到的屏幕尺寸相关坑整理成了一张排查表,开发时对照着看能省不少时间:现象可能原因排查方向控件被刘海遮住约束绑定了view边缘,没有用安全区检查约束是否基于safeAreaLayoutGuide底部按钮被小白条遮住底部间距写死为0或16改用safeAreaInsets.bottom动态设置图片显示模糊缺少@3x切图或图片被强行拉伸检查Asset Catalog中的缩放因子配置模拟器正常、真机偏移模拟器与真机渲染scale不同用真机调试并打印safeAreaInsets键盘弹出后输入框被遮挡没有动态更新输入框底部约束监听键盘通知,更新约束constant横屏布局错乱没有处理横竖屏安全区差异使用系统安全区,配合Size Classes字号忽大忽小设计稿像素值直接当成pt使用换算设计稿基准宽度,统一pt标注5.4 字号和行高在不同机型上的表现屏幕尺寸变大,不只是位置要变,字号的表现也值得注意。同一套字号设置,在3.5英寸屏上可能显得刚好,在6.7英寸大屏上就会显得局促。不少设计规范会要求大屏设备显示更大一点的文字。实现方式有两种,一种用动态字体(UIFontMetrics),让文本跟随系统字体设置自动缩放,系统辅助功能里的“更大字体”也能适配;另一种是手动按屏幕宽度设置比例字号,比如宽度大于390pt时基础字号增加2pt。这两种方案里我更推荐动态字体,尤其是文本类页面。用户主动调大字体的需求是真实存在的,如果代码里写死字号,用户辅助功能里的显示大小调节对这些字体毫无效果,体验会差很多。当然,动态字体不是所有控件都适合,像TabBar标题、底部按钮上的文字,保持固定字号反而更统一。这类控件直接用系统默认样式就够了。6. 沉淀自己的适配工具箱6.1 屏幕尺寸判断与等比缩放函数做了几个项目后,我把屏幕尺寸相关的逻辑收敛到一个工具类里,不再各处写UIScreen bounds判断。这个工具类包含几个常用的方法:当前屏幕宽度、是否为全面屏、安全区高度、等比缩放比例、以及一个根据设计基准转换pt的方法。有了这些封装,UI层所有涉及尺寸的计算都可以只调用一个方法,既统一又方便调试。enum Screen { static var width: CGFloat { UIScreen.main.bounds.width } static var height: CGFloat { UIScreen.main.bounds.height } static var scale: CGFloat { UIScreen.main.scale } /// 是否全面屏:通过底部安全区判断 static var isNotchScreen: Bool { if #available(iOS 11.0, *) { if let window = UIApplication.shared.connectedScenes .compactMap({ $0 as? UIWindowScene }) .first?.windows.first { return window.safeAreaInsets.bottom > 0 } } return false } /// 以375pt为基准的缩放比例 static var horizontalScale: CGFloat { min(max(width / 375.0, 0.85), 1.15) } }不管你用UIKit还是SwiftUI,这个工具类都能用。在实际项目里,我还会把用到的设计基准常量单独放一个文件,比如基准宽度375、底部安全间距16、顶部导航高度44等。后续设计规范有变动,只需要改一个地方,搜索替换的痛苦能少很多。6.2 我用着顺手的调试方法适配类问题调试有自己的一套方法。我习惯在开发阶段启动模拟器时多跑几个不同尺寸的设备,重点覆盖320pt宽(iPhone SE)、375pt宽(iPhone 15标准版)、430pt宽(iPhone Pro Max)这三档。宽度覆盖了这三档,高度方面再追加一个812pt和一个932pt,基本上所有可能的布局约束问题都能暴露。调试safe area时,可以打开模拟器的“环境预览”功能,或者在真机上把界面截图后用可视化工具查看safe area覆盖层。不过最简单的还是直接用代码加载Debug视图,给所有视图描边,一眼就能看出哪个控件超出了预期边界。iOS提供了lldb命令来快速查看约束详情,也可以直接在view的debugDescription里打印约束列表。我通常会在启动时加一个环境变量,把所有约束冲突的日志统一导出,格式化成表格查看,排查效率比在Xcode控制台里翻找高。最后再分享一个小技巧,也是我现在写新页面时一定会做的一件事:先确定页面在屏幕宽度最小和设备最大的两个极端下的表现,再回头补中间状态。这样操作看起来笨,但能避免“只有设计稿尺寸下一个版本好看,换机型就崩”的尴尬。屏幕尺寸这个话题虽然基础,但真想做扎实,还是值得花时间把每个设备都跑一遍的。
iPhone屏幕尺寸这个话题,很多刚开始做iOS开发的同学觉得没什么好讲的:不就屏幕变大变小吗?但实际真刀真枪上手做适配时,才发现这里面的水比想象中深得多。同一个UIView,在旧机型上稳稳地贴在底部,到了带灵动岛的机器上直接跑进挖孔区域;设计稿上标的是750宽的尺寸,代码里写375刚好,换个机型就变了形。这篇文章我打算把这几年和屏幕尺寸打交道的心得整理一遍,从逻辑分辨率、缩放因子、安全区这些基础概念,到Auto Layout、Size Classes、切图规范、真机调试里的实际坑,一次性讲清楚。不管你是刚接触iOS开发,还是已经写了一阵子UI但总觉得适配心里没底,这篇都值得花十分钟读完。1. 为什么说屏幕尺寸是iOS开发的第一道坎1.1 从初代iPhone到灵动岛:尺寸演进路线iOS设备的屏幕尺寸其实是一路“折腾”过来的。最早大家手里那台3.5英寸屏幕,逻辑上只有320x480,后来iPhone 4把物理像素提到了640x960,但逻辑尺寸还是320x480,只是把画面做得更精细了。接下来是4英寸的iPhone 5,逻辑分辨率变成320x568,这也是iOS开发里第一次真正因为屏幕变长导致布局要重新考虑的节点。再往后4.7英寸的iPhone 6把逻辑宽度从320提到了375,5.5英寸的Plus机型宽度也是375但逻辑高度到了667。到这里很多人都形成了“宽度375、高度667”的思维定式,结果iPhone X一出来,带刘海、圆角、底部横条,逻辑分辨率变成375x812,高度一下子多出145pt。后面iPhone 12系列又引入"超大杯"概念,逻辑宽度变成390pt,iPhone 15 Pro Max更是到了430pt。这种持续变化意味着单纯靠几个固定坐标堆UI的方案根本活不长。很多人习惯用物理像素去理解屏幕,比如"我的手机是2556x1179分辨率",这在iOS开发里不是最直接的思考方式。因为UIKit内部所有布局用的都是point(pt),不是物理像素。物理像素只是最终显示在屏幕上的颜色点阵,pt则是逻辑上的坐标单位。系统在渲染时会把pt乘以缩放因子(scale factor)变成物理像素。iPhone 8的scale是2x,iPhone 14 Pro的scale是3x,所以你看到的分辨率数值高了,不代表UI坐标也要跟着写大,底层逻辑尺寸可能根本没变。1.2 屏幕尺寸的本质是逻辑分辨率,而不是物理分辨率我们查设备参数时常常看到一大串数字,像iPhone 15的2556x1179像素,但这些数字对写UI来说没有直接参考意义。真正决定布局的是逻辑分辨率。我把几代代表性机型的数据整理成了一张表,对照起来看会更清楚:机型代际逻辑分辨率(pt)缩放因子物理分辨率(px)典型代表早期3.5英寸320x4801x/2x320x480 / 640x960iPhone 3GS、iPhone 44英寸320x5682x640x1136iPhone 5、iPhone SE(第一代)4.7英寸375x6672x750x1334iPhone 6、iPhone 85.5英寸 Plus414x7363x1242x2208iPhone 6 Plus、iPhone 8 Plus5.8英寸刘海屏375x8123x1125x2436iPhone X、iPhone XS6.1英寸刘海屏390x8443x1170x2532iPhone 12、iPhone 13、iPhone 146.1英寸灵动岛393x8523x1179x2556iPhone 15、iPhone 166.7英寸 Pro Max430x9323x1290x2796iPhone 15 Pro Max、iPhone 16 Pro Max从这张表能看出来,逻辑宽度基本集中在320、375、390、393、414、430这几个档位。写代码时不需要针对每一台设备单独判断,只需要让布局在这几个宽度档位下都能合理伸缩就够了。这也是Auto Layout能成为标配的根本原因:屏幕尺寸不再是少数几种固定值,而是一个区间,必须用约束关系而不是死坐标来解决问题。2. 适配思路:从固定坐标到自动布局2.1 用pt不用px:缩放因子的来龙去脉接触iOS开发一段时间后你会发现,UI设计稿上经常标注750宽、1242宽这类数值,直观上看好像代码里也要写750。其实这个理解有个偏差:设计稿一般以物理像素为基准作图,而UIKit用的是pt。750px的设计稿在2x屏幕上对应375pt,在3x屏幕上对应250pt,如果直接拿px数值往Frame里填,整套界面都会偏大。正确做法是先搞清楚设计的基准宽度。现在主流设计规范通常以375pt或390pt为基准出图,这样开发时把标注值直接当成pt用就行。我自己经历过一个项目,设计那边按750px出稿,我早期没注意,直接把设计稿里的间距、字号原封不动写到代码里,结果跑在iPhone 8上整体宽了一倍,所有按钮都挤出了屏幕边缘。后来统一约定:设计稿标注px值除以缩放因子才是代码里要写的pt值。当前设计稿以2x为基准时,px=pt2;以3x为基准时,px=pt3。搞清楚这条换算关系,很多“怎么看尺寸都对、跑起来就是不对”的诡异问题都迎刃而解。理解了pt和px的关系,另一个关键就被牵扯出来:为什么iPhone X之后很多老代码会崩。因为之前大家默认底部就是屏幕边缘,用frame直接写死,或者用autoresizingMask设置固定间距。iPhone X引入了底部Home Indicator区域,屏幕底部有一段专门留给小白条的安全距离,同时还增加了顶部刘海区域。如果还是简单地把视图放在safe area之外,UI就会被系统手势区遮挡或干脆看不见。iOS 11之后苹果统一提供了安全区域这个概念,UIView的frame不再等于可视区域,适配时所有贴边元素都应该以safeAreaLayoutGuide为基准来计算。2.2 安全区域与刘海屏适配安全区域从iOS 11开始成为标配。通俗讲,屏幕的四个边缘可能被刘海、圆角、底部小白条遮挡,系统会划出一块不被遮挡的区域,这被称为Safe Area。顶部的刘海区影响比较大,比如状态栏从20pt涨到44pt甚至更高,写死20pt的代码在全面屏上会和电池时间图标重叠;底部则多出一个34pt的Home Indicator区域,写死0间距的底部按钮会被小白条盖住一半。代码里处理安全区,比较推荐的做法是使用safeAreaLayoutGuide。举个例子,如果想让一个按钮贴屏幕底部:let button = UIButton() view.addSubview(button) button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), button.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), button.heightAnchor.constraint(equalToConstant: 44) ])如果项目最低支持版本还没到iOS 11,通常会用if #available(iOS 11.0, *)判断,低版本直接用顶部/底部布局锚点即可。实际开发中,手动约束写起来相对冗长,我更习惯把几个常用的约束封装成工具函数,每次创建约束时传safeAreaLayoutGuide而不是view自身边界,减少重复代码的同时也不会漏掉适配。说到这,就不得不提XIB和Storyboard里的一个注意事项:很多人在Interface Builder中拖控件时,喜欢把约束直接绑定到view边缘,而不是Safe Area。这在小屏机器上看不出问题,一旦换到刘海屏,控件就会自动钻进刘海范围内。在XIB中正确做法是勾选“Use Safe Area Layout Guides”选项,让约束默认基于安全区生成。2.3 设计稿标注与开发视角的转换设计稿上的尺寸到底怎么翻译成代码里的值,这个问题困扰过不少新人。针对iOS开发,建议直接跟设计沟通:所有标注统一使用pt单位,设计软件里可以设置以375pt宽度为画板基准。这样前端和设计之间根本不需要换算,设计标多少,代码写多少。另一种情况是设计交付的是一张以750px为宽度的大图,这时候需要明确这是一张2x图还是3x图。iPhone 8时代的2x图和现在iPhone 15系列的3x图基准不一样。如果设计稿是1242px宽,对应的逻辑宽度是414pt,直接按标注值写没问题;如果是750px宽,对应的逻辑宽度是375pt。遇到以像素为单位的标注时,用“逻辑宽度 = 物理宽度 / 缩放因子”这个公式换算出pt值,再按pt去写布局,比较稳妥。字体大小也一样。不少设计习惯用像素标注字号,比如24px,到了代码里直接用24pt设置字号。这里有一个常见的误伤点:虽然数字一样,但设计是按2x或3x的物理像素出的图,开发按pt设置时,需要根据画板基准做一次除法。比如画板是以750px宽的2x尺寸出的图,24px对应12pt,设置字号应该是12pt,而不是24pt。如果画板是375pt宽,那24px就直接对应24pt。跟设计团队定好基准后,这类问题不再反复出现。3. 实操:一套代码适配全机型3.1 Auto Layout与Size Classes的操作要点Auto Layout的核心思想是让视图之间的相对关系决定位置和尺寸,而不是写死一个坐标。比如一个登录按钮,不约束它距屏幕顶部固定200pt,而是约束它距离上方Logo底部32pt、左右各16pt、高度44pt;屏幕变高时按钮自动下移,屏幕变宽时按钮自动拉伸或居中。这样iPhone SE和iPhone 15 Pro Max跑的是同一套约束,但UI看起来都是合理的。Size Classes是适配横竖屏和不同尺寸设备的另一个重要工具。它在iOS 8引入,把设备分成紧凑(Compact)和常规(Regular)两类,横屏iPhone是Compact宽度+Compact高度,竖屏iPhone是Compact宽度+Regular高度,iPad多数情况是Regular宽度+Regular高度。通过traitCollection可以判断当前环境,对不同Size Class做差异化布局。我们常用的一种场景是:竖屏时按钮堆叠排列,横屏时改成水平排列。override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if traitCollection.horizontalSizeClass == .compact { // 紧凑宽度,执行堆叠布局 } else { // 常规宽度,执行水平布局 } }Auto Layout的约束优先级也值得留意。两个约束都满足时系统会报约束冲突;当一个布局需要在不同情境下选择不同约束时,可以给约束设置不同的priority,比如800和750,系统自动选择更重要的那个。实际操作中,我给底部贴边约束设750,给最小间距约束设1000,小屏时拉伸间距,大屏时保证最小间距不破,比写死逻辑判断省心不少。3.2 代码中坐标与比例的通用处理虽然有Auto Layout,但有些场景还是需要手动算坐标,比如自定义绘制、动画路径、渐变图层等。这时可以用屏幕宽度做比例换算。主流做法是以设计基准宽度(通常是375)作为参考,计算出当前设备宽度与基准宽度的比例,然后按比例缩放间距、字号、控件尺寸。// 以375pt为基准宽度等比缩放 CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; CGFloat scale = screenWidth / 375.0; CGFloat buttonWidth = 120 * scale;这个思路简单直接,但有一个明显的缺点:iPhone 16 Pro Max的宽度是430pt,等比放大后120pt会变成137.6pt;反过来iPhone SE的320pt下会缩成102.4pt。视觉上虽然整体协调,但有些控件拉伸后看起来不够精致。因此等比缩放更适合用于间距、内边距、图标大小这类相对弹性元素,不适合用于固定大小的控件和必须保持清晰度的关键UI。更精细的做法是采用“非线性适配”,比如在小屏上取最小值,在超大屏上封顶:CGFloat scale = MIN(MAX(screenWidth / 375.0, 0.85), 1.15); CGFloat buttonWidth = 120 * scale;这个“MIN/MAX封顶”的思路很实用。小屏不至于缩得太小难点击,大屏也不至于放得太大显得空旷。很多成熟的第三方UI框架内部也是类似策略,只不过封装得更完善。3.3 SwiftUI下的自适应写法SwiftUI把适配做成了“默认能力”,但仍有技巧。ViewThatFits可以在空间不足以容纳首选布局时自动切换到备选布局,实现类似“塞得下就横排,塞不下就竖排”的效果:ViewThatFits(in: .horizontal) { HStack { Text("余额") Text("¥100") Button("充值") {} } VStack { Text("余额") Text("¥100") Button("充值") {} } }SwiftUI里还有GeometryReader用来读取父视图的尺寸,配合frame(maxWidth:)或containerRelativeFrame可以做出响应式布局。实际开发中SwiftUI的优势是写相同逻辑比UIKit代码量少得多,但在处理自定义导航栏和键盘避让时还是有不少细节。好在这几年系统版本迭代快,SwiftUI的成熟度已经能支撑生产环境,新项目用SwiftUI写列表、表单类页面体验很顺畅。4. 切图资源:2x与3x的配套规则4.1 @2x/@3x的命名与选择机制适配不只是代码上的事,切图资源也得按尺寸配套。iOS资源命名后缀有一套既定规则:默认无后缀的图用于1x设备,@2x后缀用于2x设备,@3x后缀用于3x设备。系统加载图片时,会根据设备的scale factor自动选择对应文件,不需要代码里判断设备类型。目前新发布的主流机型全是3x设备,所以提供@3x图是底线;旧机型数量不少,@2x图也建议保留。理论上1x图永远不会被用到,但还是建议预留一套,方便低版本系统或某些特殊屏幕使用。图片资源建议全部放进Asset Catalog里管理,Assets.xcassets会自动处理同一套图在不同scale下的匹配,构建时还会做必要的压缩优化。命名上我习惯统一用英文小写加下划线,比如icon_login_highlight@2x.png和icon_login_highlight@3x.png,方便脚本批量处理。4.2 Asset Catalog的Single Scale与矢量图Asset Catalog支持一种叫做Single Scale的模式,跟传统@2x/@3x三套图的方式不同,它只放一张矢量图(PDF或SVG),由系统在运行时按需渲染出任意尺寸。这样做的好处是图片在不同屏幕尺寸下始终清晰,不像点阵图放大后会模糊。它特别适合图标、背景纹理这类需要频繁缩放或固定展示尺寸的产品元素。使用Single Scale的流程比较固定:先在矢量绘图工具里导出PDF格式,拖入Assets.xcassets,在右侧属性面板中把Scale Filters改成Single Scale,然后设置好Preserve Vector Data。发布时iOS系统会自动针对不同scale生成相应分辨率。需要提醒的是,复杂的渐变或阴影效果用PDF矢量图导出时偶尔会有色差,这类资源还是用PNG点阵图更稳妥。项目结构示例: Assets.xcassets └── Icon.imageset ├── 图标.pdf (Single Scale) └── Contents.json图标切图还有一个常见场景:UIButton的backgroundImage。有些设计师提供一张规范的小图,但按钮实际尺寸随机型变化,直接拉伸会变形。这时用resizableImage(withCapInsets:resizingMode:)来指定可拉伸区域,或者用backgroundImage配合内容模式设置,避免图片被拉伸变形。4.3 图片拉伸与UIButton背景处理处理可拉伸图片我是这样做的。上线下一套背景图时,先确定需要保护的圆角范围,比如四角16pt半径的圆角矩形,中间区域是纯色或重复纹理,就可以创建可拉伸图片。直接用UIImage.resizableImage(withCapInsets:)设置内边距,然后赋值给Button:UIImage *original = [UIImage imageNamed:@"btn_bg"]; UIEdgeInsets insets = UIEdgeInsetsMake(16, 16, 16, 16); UIImage *resizable = [original resizableImageWithCapInsets:insets resizingMode:UIImageResizingModeStretch]; [button setBackgroundImage:resizable forState:UIControlStateNormal];这里的UIEdgeInsets参数不是随便拍的。上下左右的数值表示“不可拉伸区域”的范围,对应圆角的大小或特殊边框宽度。如果四个值都设成0,图片就是整体拉伸,圆角会被拉成大椭圆,看起来非常奇怪。一般上下值取圆角半径+2,左右值也取圆角半径+2,这样可以保证四角始终是完美圆弧。5. 我踩过的那些适配坑5.1 死值布局与键盘弹起老代码里最常见的问题是把布局写死在viewDidLoad里,用self.view.frame.size直接计算坐标。第一次运行没问题,但一旦屏幕旋转、键盘弹出、设备切换,frame就变了,写死的坐标全错位。一个很典型的场景是弹出键盘后输入框被键盘遮住。很多人第一反应是给view添加一个键盘通知监听,把整个view往上顶,一顿操作后发现各种问题。更稳妥的方法是结合Auto Layout。给输入框底部约束绑定到view.safeAreaLayoutGuide.bottom,键盘弹出时把约束的constant改成键盘高度,键盘收起时改回0,配合动画执行。这样只用更新一个约束值,不用手动移动整个view,未来屏幕尺寸变化也不会破。判断键盘高度时不要直接用屏幕上可见区域的变化量,不同机型、不同语言输入法的高度差异很大,建议用系统键盘通知里的keyboardFrameEndUserInfoKey值来换算,更准确。5.2 底部安全区与旋转方向全面屏出现后,底部间距的处理成了重灾区。不少界面底部有一条操作栏,有的开发习惯直接用bottomAnchor = view.bottomAnchor,结果被小白条遮住一半。正确做法是参考safeAreaLayoutGuide,或者把安全区的值动态读取出来:- (CGFloat)safeAreaBottom { if (@available(iOS 11.0, *)) { return self.view.safeAreaInsets.bottom; } return 0; }用safeAreaInsets还有一个好处是能处理横竖屏切换。横屏时底部安全区会变到屏幕两侧,如果在竖屏布局里写死34pt,旋转后侧边就会空出一片。直接用系统提供的值,代码不用感知旋转细节,系统自动更新。唯一需要注意的是,不要在viewDidLoad里读safeAreaInsets,此时布局还没有完成,拿到的值往往是0。正确时机是viewDidLayoutSubviews或首次布局完成后。5.3 真机调试中分辨率的误判今年我遇到过一个比较隐蔽的问题:代码在模拟器上所有机型看起来都正常,但真机一跑,图片变模糊,位置错了一两个点。排查后才发现模拟器默认渲染scale和真机不完全一样,尤其是用模拟器非Retina模式渲染时,很多色值和坐标会有细微偏差。后来我定了一个规矩:所有涉及精确对齐、图片临界尺寸的页面,必须拿真机跑一遍,不能只依赖模拟器。真机上最容易发现的问题是“Point精度和像素精度不一致”。比如一个View的宽度设成了0.5pt的奇数倍,在2x设备上会显示成奇数个物理像素,系统渲染时可能自动做round处理,导致视图边缘发虚。这类问题在模拟器上不明显,真机上则能明显看到边界锯齿。解决办法是给视图frame取整:CGFloat pixelAlign = floor(value * scale) / scale;当发现屏幕尺寸不同导致某些控件位置错乱时,我通常会在调试控制台打印当前设备和安全区信息:print("screen: \(UIScreen.main.bounds)") print("safeArea: \(view.safeAreaInsets)") print("scale: \(UIScreen.main.scale)")这样对比真机输出和模拟器输出,能很快定位是设备判断逻辑出错还是约束缺少。我把这几年碰到的屏幕尺寸相关坑整理成了一张排查表,开发时对照着看能省不少时间:现象可能原因排查方向控件被刘海遮住约束绑定了view边缘,没有用安全区检查约束是否基于safeAreaLayoutGuide底部按钮被小白条遮住底部间距写死为0或16改用safeAreaInsets.bottom动态设置图片显示模糊缺少@3x切图或图片被强行拉伸检查Asset Catalog中的缩放因子配置模拟器正常、真机偏移模拟器与真机渲染scale不同用真机调试并打印safeAreaInsets键盘弹出后输入框被遮挡没有动态更新输入框底部约束监听键盘通知,更新约束constant横屏布局错乱没有处理横竖屏安全区差异使用系统安全区,配合Size Classes字号忽大忽小设计稿像素值直接当成pt使用换算设计稿基准宽度,统一pt标注5.4 字号和行高在不同机型上的表现屏幕尺寸变大,不只是位置要变,字号的表现也值得注意。同一套字号设置,在3.5英寸屏上可能显得刚好,在6.7英寸大屏上就会显得局促。不少设计规范会要求大屏设备显示更大一点的文字。实现方式有两种,一种用动态字体(UIFontMetrics),让文本跟随系统字体设置自动缩放,系统辅助功能里的“更大字体”也能适配;另一种是手动按屏幕宽度设置比例字号,比如宽度大于390pt时基础字号增加2pt。这两种方案里我更推荐动态字体,尤其是文本类页面。用户主动调大字体的需求是真实存在的,如果代码里写死字号,用户辅助功能里的显示大小调节对这些字体毫无效果,体验会差很多。当然,动态字体不是所有控件都适合,像TabBar标题、底部按钮上的文字,保持固定字号反而更统一。这类控件直接用系统默认样式就够了。6. 沉淀自己的适配工具箱6.1 屏幕尺寸判断与等比缩放函数做了几个项目后,我把屏幕尺寸相关的逻辑收敛到一个工具类里,不再各处写UIScreen bounds判断。这个工具类包含几个常用的方法:当前屏幕宽度、是否为全面屏、安全区高度、等比缩放比例、以及一个根据设计基准转换pt的方法。有了这些封装,UI层所有涉及尺寸的计算都可以只调用一个方法,既统一又方便调试。enum Screen { static var width: CGFloat { UIScreen.main.bounds.width } static var height: CGFloat { UIScreen.main.bounds.height } static var scale: CGFloat { UIScreen.main.scale } /// 是否全面屏:通过底部安全区判断 static var isNotchScreen: Bool { if #available(iOS 11.0, *) { if let window = UIApplication.shared.connectedScenes .compactMap({ $0 as? UIWindowScene }) .first?.windows.first { return window.safeAreaInsets.bottom > 0 } } return false } /// 以375pt为基准的缩放比例 static var horizontalScale: CGFloat { min(max(width / 375.0, 0.85), 1.15) } }不管你用UIKit还是SwiftUI,这个工具类都能用。在实际项目里,我还会把用到的设计基准常量单独放一个文件,比如基准宽度375、底部安全间距16、顶部导航高度44等。后续设计规范有变动,只需要改一个地方,搜索替换的痛苦能少很多。6.2 我用着顺手的调试方法适配类问题调试有自己的一套方法。我习惯在开发阶段启动模拟器时多跑几个不同尺寸的设备,重点覆盖320pt宽(iPhone SE)、375pt宽(iPhone 15标准版)、430pt宽(iPhone Pro Max)这三档。宽度覆盖了这三档,高度方面再追加一个812pt和一个932pt,基本上所有可能的布局约束问题都能暴露。调试safe area时,可以打开模拟器的“环境预览”功能,或者在真机上把界面截图后用可视化工具查看safe area覆盖层。不过最简单的还是直接用代码加载Debug视图,给所有视图描边,一眼就能看出哪个控件超出了预期边界。iOS提供了lldb命令来快速查看约束详情,也可以直接在view的debugDescription里打印约束列表。我通常会在启动时加一个环境变量,把所有约束冲突的日志统一导出,格式化成表格查看,排查效率比在Xcode控制台里翻找高。最后再分享一个小技巧,也是我现在写新页面时一定会做的一件事:先确定页面在屏幕宽度最小和设备最大的两个极端下的表现,再回头补中间状态。这样操作看起来笨,但能避免“只有设计稿尺寸下一个版本好看,换机型就崩”的尴尬。屏幕尺寸这个话题虽然基础,但真想做扎实,还是值得花时间把每个设备都跑一遍的。