适配 iPhone Duo,不应该从判断设备型号开始
我做了一个包含 12 个可运行示例的 SwiftUI 自适应布局项目
适配 iPhone Duo,不应该从判断设备型号开始
做 iOS 布局时,我们很容易写出这样的逻辑:
如果是手机,显示单栏;如果是平板,显示双栏;如果屏幕足够宽,再换成另一套布局。
这种做法在设备形态比较固定时似乎能够工作。但随着横竖屏切换、分屏多任务、动态字体、多窗口,以及 iPhone Duo 带来的不同姿态出现,“设备型号”越来越不能准确描述应用当前拥有的空间。
同一台设备上的同一个应用,可能占据整个屏幕,也可能只得到一个狭窄窗口。它可能处于展开状态,也可能出现需要避让的分隔区域。即使外部屏幕尺寸没有变化,安全区域和可用空间也可能已经不同。
因此,真正应该驱动布局的,不是“用户正在使用哪台设备”,而是:
系统和父容器此刻给了这个界面多少可用空间?
这也是我创建 DuoKit 的原因。
GitHub:https://github.com/openHacking/DuoKit

DuoKit 是什么?
DuoKit 是一个开源的 SwiftUI 示例项目,集中展示自适应布局、导航、安全区域、折叠区域避让、多任务和多场景设计。
它目前包含 12 个可以直接运行的示例。
但需要特别说明:DuoKit 不是一个 UI 框架。
它不会提供一组需要集成到业务里的自定义组件,也不希望开发者依赖某套私有抽象。它更像一本可以运行的 SwiftUI 手册:打开一个示例,修改窗口宽度或交互状态,就能直接观察布局如何变化。
每个示例都围绕一个具体问题组织:
- 要解决的问题是什么
- 推荐采用什么方式
- 涉及哪些 SwiftUI API
- 常见错误是什么
- 应该测试哪些边界情况
- 对应的源码和官方资料在哪里
我希望它解决的不是“复制一段代码”,而是“理解这段代码为什么这样写”。
12 个示例,覆盖自适应界面的完整路径
DuoKit 的示例从基础布局开始,逐步进入 iPhone Duo、多任务和真实应用场景。
1. Adaptive Layout:响应可用空间
第一个示例组合使用:
horizontalSizeClassGeometryReaderViewThatFitsAnyLayout
它展示同一个界面如何随着容器宽度变化,在紧凑布局和展开布局之间自然切换。
这里最重要的不是某个具体断点,而是区分两个问题:
尺寸类别适合决定界面的整体层级,容器几何信息则适合处理局部排布。
如果只是通过 UIScreen.main.bounds 获取屏幕宽度,或者根据设备型号选择布局,就无法准确描述应用当前所在的窗口。
2. NavigationSplitView:只维护一套导航状态
列表与详情是最常见的自适应界面之一。
紧凑空间下,它通常表现为逐级进入的导航;空间足够时,则可以同时显示侧边栏和详情。
常见做法是分别为手机和平板维护两套导航结构。但这样很容易产生重复状态:用户在一种布局中选中了内容,切换布局后,另一套界面却不知道当前选中了什么。
DuoKit 使用 NavigationSplitView 和统一的 selection,让系统负责导航结构的展开与收起,应用只维护一份业务状态。
3. Adaptive Grid:让宽度决定列数
网格布局不应该写成:
“手机两列,平板四列。”
更稳妥的方式是定义内容能够接受的最小宽度,然后让 LazyVGrid 根据容器空间计算列数。
这样,无论应用运行在全屏、分屏还是不同方向下,网格都能连续变化,而不是只在几个预设设备之间跳转。
4. Safe Area:不要假设边距一定对称
安全区域经常被当成一个统一的边距处理。但在不同设备形态和姿态下,leading、trailing、top 和 bottom 可能完全不同。
DuoKit 会实时展示各个方向的安全区域数值,用来说明一个很容易被忽略的问题:
不能读取左侧边距,再乘以二,推断整个可用宽度。
每一侧的安全区域都应该被独立读取和处理。
5. Fold Avoidance:只移动真正受影响的内容
如果一个关键按钮刚好跨越折叠或分隔区域,继续把它放在几何中心并不合理。
但这也不意味着整个界面都需要重新布局。
更合适的方式是判断哪些自定义控件与保留区域相交,然后只把受影响的控件移动到最近的可用区域。
DuoKit 的 Fold Avoidance 示例使用一个明确标注的模拟折叠区域,对比按钮避让前后的表现。
6. Reserved Regions:理解分隔与遮挡
保留区域并不只有一种。
有些区域代表显示空间之间的分隔,例如折叠位置;另一些区域则可能代表摄像头等造成的遮挡。
DuoKit 将 division 和 occlusion 两类区域分别可视化,帮助开发者理解:屏幕矩形存在,不代表其中每个位置都适合放置交互控件。
7—9. 把导航和展示交给系统
接下来的三个示例分别展示:
- Adaptive Toolbar
- Adaptive TabView
- Sheets & Popovers
它们背后的原则是一致的:优先使用系统提供的语义化容器。
例如,通过 ToolbarItem 的 placement 描述一个操作的作用,而不是用固定坐标绘制自己的工具栏;使用标准 TabView 描述导航目的地,而不是维护一套固定位置的自定义标签栏。
系统知道当前窗口和设备状态,也更有机会在环境变化时正确调整这些控件。
10. Split View Multitasking:设备没变,窗口已经变了
在分屏多任务环境中,应用拿到的可能只是显示区域的一部分。
因此,“这是一台大屏设备”并不代表应用当前有足够空间显示大屏布局。
DuoKit 使用同一个 Dashboard,在窄、中、宽三种空间中连续变化。开发者可以拖动宽度,直接观察信息层级和组件排列如何调整。
这比只在两个固定模拟器上分别截图,更容易暴露中间尺寸的问题。
11. Multiple Displays & Scenes:失败也是界面状态
多场景设计不能假设第二个场景一定能够创建。
系统可能不支持当前操作,场景激活也可能失败。因此,应用除了处理成功状态,还需要明确展示能力是否可用,并正确处理错误和回退。
DuoKit 使用一个演示者与观众视图的状态机,展示场景创建、内容同步和失败处理。
12. DuoNotes:把前面的原则放进一个完整应用
单独的 API 示例很容易理解,但真实应用的问题通常出现在多个能力组合之后。
因此,最后一个示例是一个可以创建、选择、编辑和删除内容的笔记应用 DuoNotes。
它组合使用了:
NavigationSplitViewBindingtoolbarsheet- 安全区域处理
- 无障碍支持
DuoNotes 不会为紧凑布局和展开布局分别维护两份笔记数据。无论导航如何变化,笔记列表和当前选择都只有一个事实来源。
这也是自适应应用中非常重要的一条原则:
布局可以变化,业务状态不应该因为布局变化而被复制。
DuoKit 坚持的四个设计原则
把这些示例放在一起后,可以归纳出四条共同原则。
第一,根据可用空间布局,不根据设备型号布局
设备型号只是一个间接信号,而且经常不准确。
真正与布局有关的是当前窗口、父容器、安全区域和场景状态。
第二,优先使用系统容器
NavigationSplitView、TabView、toolbar 和系统 presentation API,通常比固定坐标和自定义外壳更能适应环境变化。
自定义界面应该集中在真正需要自定义的内容上。
第三,只调整受到影响的部分
出现折叠区域或遮挡区域时,不要立刻重建整个页面。先判断哪些内容真的发生相交,再移动相关控件。
局部调整通常比全局特判更稳定。
第四,把连续变化纳入测试
不要只测试“最窄”和“最宽”两个端点。
拖动窗口宽度时,中间状态可能出现文字截断、按钮重叠、网格跳动和导航丢失。还需要同时测试:
- 横屏与竖屏
- 分屏窗口
- 深色模式
- 辅助功能字体
- 不同安全区域
- 场景创建失败
- 不同设备姿态
自适应布局不是几个静态尺寸,而是一段连续变化的区间。
关于模拟功能的边界
DuoKit 当前使用 Xcode 26.6 构建,部署目标为 iOS 26.0。
其中 Fold Avoidance、Reserved Regions 和 Multiple Displays & Scenes 涉及的部分原生能力需要 iOS 27.1 SDK。为了让开发者现在也能理解相关概念,项目提供了明确标注的模拟环境。
这些模拟区域只是教学工具,不代表真实硬件尺寸,也不应该被当成 iPhone Duo 的几何数据。
采用 Xcode 27.1 后,应当在示例边界处将模拟数据替换为系统原生 API,并在真实的设备姿态环境中重新验证。
我认为这种限制必须直接写出来。教学示例可以模拟一个概念,但不能把模拟结果包装成硬件事实。
如何运行
DuoKit 没有第三方依赖,不需要账号,也不需要私有配置。
运行步骤如下:
- 安装 Xcode 26.6 或更高版本
- 克隆项目仓库
- 打开
DuoKit.xcodeproj - 选择 DuoKit Scheme
- 选择 iPhone 或 iPad 模拟器并运行
项目采用 MIT License,所有示例都可以自由阅读、修改和使用。
写在最后
DuoKit 表面上是在介绍如何为 iPhone Duo 设计界面,但它真正讨论的是一个更长期的问题:
当设备和窗口不再只有几种固定形态时,我们应该怎样组织 SwiftUI 应用?
答案不是继续增加设备判断,也不是为每一种形态维护一套界面。
更可靠的方向是读取系统提供的环境,根据当前获得的空间组织内容,把导航和展示交给标准容器,并保证业务状态独立于布局存在。
如果你正在开发 SwiftUI 应用,欢迎试用 DuoKit,也欢迎提交新的示例、边界情况或改进建议。
项目地址:











































