返回首页

iPhone Duo 适配

Duo 是我们对「一套代码同时跑在 H5 与 WebView」这件事的内部代号。这篇文章把 Duo 在 iPhone 全系机型上的适配工作整理成一份可以照着做的清单:安全区、灵动岛、动态视口、 深色模式与 120Hz 动画性能,每一步都配了可以直接复制的代码。

很多人以为 iPhone 的屏幕尺寸就那么几种,适配应该很轻松。真实情况恰好相反: 从 4.7 英寸的 SE 到 6.9 英寸的 Pro Max,再加上刘海、灵动岛、动态岛隐藏态、 安全区、底部 Home Indicator、横竖屏切换与键盘弹起,同一份代码要应对的场景比 Android 更离散。 更麻烦的是,这些差异几乎没有「报错」——它们只会在真机上表现为「差一点」。

下面这七步,是我在 Duo 上按顺序落地的适配流程。每一步都可以独立验收, 并且都可以在 iOS 模拟器里复现。

为什么 2026 年还要专门做 iPhone 适配

三个原因:安全区不再对称视口高度不再是常量WebView 与 Safari 的行为差异

  • 顶部安全区在刘海机型约为 44px,灵动岛机型约为 59px,全屏状态下 App 内 WebView 还会再叠加一层。
  • 底部 Home Indicator 固定占掉 34px,横屏时左右两侧也会出现安全区。
  • iOS Safari 的 100vh 指的是「地址栏收起后」的高度,所以首屏经常被裁掉一截。
  • 键盘弹起时,页面并不会自动「变矮」,只是可视视口被覆盖。

一个经验:永远不要用具体的英寸数或机型名来做判断。 只依赖三样东西——视口宽度、安全区变量、视口高度单位。

第 1 步:让页面真正全屏

标配的 viewport 加上 viewport-fit=cover,这是让 env(safe-area-inset-*) 生效的前提。少了这个属性,安全区变量全部返回 0, 页面在刘海机上会老老实实缩进到一块「安全但难看」的区域里。

<!-- 必须带 viewport-fit=cover,否则安全区变量恒为 0 -->
<meta name="viewport"
      content="width=device-width, initial-scale=1, viewport-fit=cover" />

如果页面是在 App 的 WebView 里跑,还需要原生侧配合:iOS 上把 WKWebView.scrollView.contentInsetAdjustmentBehavior 设为 .never, 否则系统会额外做一次缩进,导致安全区被计算两次。

第 2 步:安全区,从刘海到灵动岛

安全区变量有四个,可以直接当 CSS 变量用。关键是只给需要贴边的元素加 padding, 不要给 body 整体加,否则背景色会出现难看的留白。

/* 通用写法:给固定定位的头部/底部单独留出安全区 */
.app-header {
  padding-top: env(safe-area-inset-top, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}

.tabbar {
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

/* 内容区:让背景铺满,但内容躲开安全区 */
.app-main {
  padding: max(16px, env(safe-area-inset-top)) 16px
           max(24px, env(safe-area-inset-bottom));
}

灵动岛机型(iPhone 14 Pro 及之后)可用同样的方式处理,只是顶部安全区值更大。 如果界面有「全屏沉浸」需求(比如首页大图),做法是让背景元素铺满整屏, 内容层再加 padding-top,两件事分开做就不会互相伤害。

第 3 步:尺寸断点不要按机型写死

下面这张表用来对齐设计稿与逻辑像素,但代码里不要出现这些数字对应的机型名。 实践下来,只用两个断点就足够覆盖全部机型。

机型 逻辑尺寸 (CSS px) DPR 顶部安全区
iPhone SE (2 / 3 代)375 × 667220px
iPhone 13 mini375 × 812350px
iPhone 15393 × 852359px
iPhone 16 Pro402 × 874362px
iPhone 16 Pro Max440 × 956362px

推荐的断点策略:min-width: 390px 视为「现代 iPhone 基准」, min-width: 768px 视为「平板及以上」。宽度用 clamp() 做流体布局,能省掉大量中间断点。

.hero__title {
  /* 36px 起步,视口够宽时增长,最多 68px */
  font-size: clamp(36px, 7vw, 68px);
}

/* 只在需要时切换布局,而不是给每台机器写一套 */
@media (min-width: 768px) {
  .grid { grid-template-columns: repeat(3, 1fr); }
}

第 4 步:动态视口与键盘弹起

解决首屏被裁的问题,用新的视口单位:svh(小视口,地址栏完全展开时的最小高度)、 lvh(大视口)、dvh(动态跟随地址栏变化)。首屏高度建议固定用 svh,避免地址栏收放时页面高度跳动。

/* 首屏:用 svh 保证「无论地址栏什么状态都不会被裁」 */
.hero {
  min-height: 100svh;
  /* 兼容老版本 iOS 的降级写法 */
  min-height: -webkit-fill-available;
}

键盘弹起是另一类问题。iOS 不会缩小布局视口,只会覆盖可视视口, 所以「输入框被键盘挡住」要用 visualViewport 事件来跟:

const vv = window.visualViewport;
if (vv) {
  const sync = () => {
    // 键盘高度 = 布局视口高度 − 可视视口高度
    const keyboard = window.innerHeight - vv.height - vv.offsetTop;
    document.documentElement.style.setProperty('--kb', Math.max(0, keyboard) + 'px');
  };
  vv.addEventListener('resize', sync);
  vv.addEventListener('scroll', sync);
  sync();
}

拿到 --kb 之后,把底部输入栏的 transform 或底部留白 按这个值处理即可。注意用 transform 而不是改 height, 这样不会触发逐帧重排。

第 5 步:深色模式与 OLED 纯黑

iPhone 的 OLED 屏在纯黑像素上是不发光的,所以深色主题既可以省电,也能让「银河」这类 发光元素更突出。两个细节:

  • 给根元素声明 color-scheme: dark,系统控件(滚动条、表单、日期选择器)才会跟着变暗。
  • 不要把正文背景设成纯黑 #000,留一点点底色(如 #03040b)可以避免 OLED 上黑阶断层与拖影。
  • 纯黑背景下的文字对比度要重新检查,正文颜色不低于 #B6BFD8 这个亮度更稳。
:root {
  color-scheme: dark;
  --void: #03040b;  /* 深空底色,而非纯黑 */
  --ink-2: #b6bfd8; /* 正文,深色底上对比度 ≥ 7:1 */
}

第 6 步:120Hz 与动画性能

iPhone Pro 系列的 ProMotion 屏会把刷新率提到 120Hz。这既是福利也是陷阱: 如果动画只改 transformopacity,画面会顺滑到不可思议; 一旦动画里出现了 widthtopfilter 这些属性, 掉帧会比 60Hz 设备上更明显。

  1. 动画只碰 transform / opacity,必要时用 will-change 但要记得用完移除。
  2. 大面积 backdrop-filter: blur() 很贵,限制在固定数量的卡片上,不要给列表每一项都加。
  3. Canvas 动画要按 devicePixelRatio 设置画布尺寸,同时给像素比设上限(本站取 2,移动端会动态降到 1.5 以下)。
  4. requestAnimationFrame 的两次时间差做时间步长,而不是假设每帧 16.7ms。

省电策略也别忘了:页面隐藏(visibilitychange)时要停掉动画循环, 并且完整支持 prefers-reduced-motion——系统开了「减弱动态效果」时, 应该退化成一张静态图而不是继续旋转。

第 7 步:横屏、悬停与桌面级 Safari

iPhone 横屏时左右会出现安全区;iPad 与 Mac 上的 Safari 支持鼠标悬停, 但触摸屏没有 :hover。这两件事都可以用媒体查询一次性解决:

/* 只在真有指针悬停能力的设备上启用 hover 效果 */
@media (hover: hover) and (pointer: fine) {
  .post:hover { transform: translateY(-3px); }
}

/* 横屏时左右安全区 */
@media (orientation: landscape) {
  .shell { padding-inline: env(safe-area-inset-left) env(safe-area-inset-right); }
}

上线前检查清单

项目验收方式
安全区变量生效在 Safari 里检查 env(safe-area-inset-top) 是否非 0
首屏不被裁切地址栏展开 / 收起两种状态各看一遍
键盘不遮挡输入框真机点击输入框,观察底部按钮是否被覆盖
横屏无横向滚动旋转设备后 document.body.scrollWidth 是否等于视口宽
最小机型可用切到 375 × 667 视口,确认没有元素被挤出
减弱动效生效设置 → 辅助功能 → 动态效果 → 减弱动态效果
WebView 内行为一致Safari → 开发 → 连接真机,远程调试 WKWebView

最后一句提醒:Safari 的「远程调试」是唯一可靠的验收手段。 桌面浏览器模拟出来的安全区只是近似值,灵动岛、键盘、滚动回弹这些行为, 只有真机能给出答案。


以上就是 Duo 在 iPhone 上的完整适配路径。如果你也在做跨端界面, 欢迎从邮件里把你们的踩坑清单发给我,我会补充进这篇文章。

返回文章列表

最后更新于 2026-09-17 · 原创内容,转载请注明出处