关于 app-resizability
面向 Swift 和 Objective-C UIKit 项目的编码技能,帮助应用适配动态场景尺寸、多窗口和现代 iOS 生命周期。它指导针对性 API 替换,并强调保留控制流、限制修改范围。
主要功能
- 01更新旧式共享状态 API
- 02迁移至场景生命周期
- 03支持可调整大小的场景与窗口
- 04涵盖 Swift 和 Objective-C
- 05规范安全且限范围的修改
适用场景
- 01为折叠屏 iPhone 做准备
- 02修复窗口调整大小时的布局
- 03适配分屏或 Stage Manager
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tartinerlabs/ skills --skill app-resizability安装后在 Claude Code 中输入/app-resizability确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/idiom-task.mdorientation-task.mdsafe-area-task.mdscene-lifecycle-task.mduiscreen-task.md详细介绍
App Resizability Skill
适用场景
无论请求是否提及 API,只要涉及以下任一内容,都请使用此技能:
- 为可折叠 iPhone Duo 做好应用准备,或针对其优化应用。
- 应用运行时必须适应窗口大小的变化。
- 分屏多任务、Stage Manager 或可调整大小的场景。
- 下文「范围」部分列出的任何已弃用 API。
请求不一定是技术性的。「让我的应用为 iPhone Duo 做好准备」「针对可折叠 iPhone 优化此应用」和「让此应用正确调整大小」都会启用此技能。iPhone Duo 就是可折叠 iPhone,因此请将关于 iPhone Duo 或可折叠 iPhone 的问题视为同一请求。对于可折叠 iPhone Duo 相关请求,请将其视为任务注册表中的所有任务,因为屏幕大小变化会同时暴露所有这些问题。
目的
使 UIKit 应用现代化,以便在现代 iOS 上正确运行:
- 消除对旧版共享状态 API 的引用
- 从应用生命周期迁移到场景生命周期
- 支持动态场景大小和多窗口环境
范围
此技能会对 Swift 和 Objective-C 代码库进行特定且有针对性的现代化改造:
- 将旧版共享状态 API 替换为适合当前上下文的现代 API
- 迁移到基于场景的生命周期
- 移除以下用法,更新应用以支持可调整大小的用户界面:
- 主屏幕(
UIScreen.mainScreen、UIScreen.main) - 界面方向(
interfaceOrientation) - 用户界面风格(
userInterfaceIdiom、UI_USER_INTERFACE_IDIOM()) - 关于安全区域对称的假设(
safeAreaLayoutGuide、safeAreaInsets) - 用应用生命周期代替场景生命周期(
UIApplicationDelegate)
- 主屏幕(
核心原则
- 尽可能靠近使用方 — 优先采用最接近使用位置的信息(例如,优先使用视图的特征集合,而非窗口的特征集合)。
- 目标 API 存在时,始终应用替换方案。 仅添加 TODO 算作失败。包含目标 API 的文件没有差异,同样算作失败。 如果文件包含目标弃用 API,并且可以采用当前任务参考文件中的任一模式实施具体替换,就必须应用该替换。只有当目标 API 仅出现在无效代码(
#if 0/#endif)中时,才可以跳过。如果在两种有效替换方案之间无法确定,应选择最符合用户请求的方案,而不是不产生任何差异。切勿默默跳过文件:如果你不愿意应用更改,应与用户讨论可选方案,绝不能对此文件不做任何处理。对于简单文件,不要纠结边缘情况;如果替换方案显而易见,就直接应用并继续。 - TODO 必须可执行。 说明为何需要更改、替换方案是什么,以及生命周期或线程方面的注意事项。将 TODO 单独放在未更改代码上方一行,切勿放在代码行内。含糊的 TODO(「以后再修复」)比没有 TODO 更糟糕,因为它会占用审查精力,也无法提供有用信息。 插入注释的每一行都要缩进到与其前面的代码行相同。 后续行如果缩进不同,代码块就会错位。重新输出后面的代码行以调整对齐,会修改到你未被要求触碰的行。
- 如果现有注释已涵盖迁移事项,就不要添加多余的 TODO。 如果调用位置已有与错误报告引用配对的
#pragma clang diagnostic ignored、现有的// TODO,或指向迁移事项的弃用注释,就不要再添加一条。只有当新的 TODO 能提供现有注释中没有的迁移指导时,才添加它。 - 进行有风险的代码更改前先询问用户;只有无法进行交互指导时才退而添加 TODO。 如果替换可能导致调用方出错或改变可观察行为(例如,更改其他模块导入的头文件中的方法签名;当左右方向很重要时,用
width > height替代方向判断),首先要询问用户如何处理。只有在技能以非交互方式运行,或用户明确拒绝提供指导时,才添加 TODO 并继续。此规则不适用于活动任务的参考文件指定的标准、可直接替换且安全的替代方案——必须按照核心原则 2 应用这些方案。 - 遵循用户的明确指示;否则采用任务参考文件中的默认方案。 用户要求特定做法时——例如指定属性、参数名称、参数位置、特征来源或回退行为——就严格照做。不要擅自替换成你认为更现代的方案。用户只给出笼统要求(如“现代化这个应用”或“修复
UIScreen.main的用法”)时,采用活动任务参考文件中的默认方案。 - 绝不将动态值替换为字面值——始终保持替换后的值为动态值。
- 保留控制流——优先采用保持原代码结构的直接替换。只有直接替换行不通时,才添加 guard/提前返回模式。修改控制流(
if/else、switch/case/default、do/catch)周边代码时,请确认编辑后分支结构仍然保留。绝不要把if/else折叠成顺序执行:这样两个代码块都会无条件运行,属于严重错误。保留所有条件合理的分支,只替换其中已弃用的值。只有当活动任务明确指出该条件本身有缺陷,并说明正确行为应当无条件执行时,才删除分支。在这种情况下,删除条件,保留符合正确行为的代码块,并在摘要中指出删除了哪个分支,以便审阅者核查。 - 严格限定范围——不要顺手清理。 仅修改包含活动任务目标弃用 API 的行。不要顺带修复附近的其他弃用项。不要删除行尾空格、重新调整空行格式,也不要“清理”周边格式。即使相邻行存在明显的现代化机会,也要保留原样——每个任务都应独立处理;超出范围的修改会让原本成功的范围内更改变成警告。
- 提取重复表达式——如果同一替换值在一个作用域内使用多次,请将其提取为具名局部变量。
- 绝不遍历全局场景/窗口状态——绝不要将
UIApplication.shared、UIDevice.current、UIScreen.main或其他共享对象用作替代方案。如果没有可用的局部对象,就修改方法以接受新参数,并更新调用方以传入该参数。 - 完整模式——必须原子性地完成,绝不只做一部分——活动任务定义的每个多部分模式,都必须将所有部分作为一个整体一并应用。当任务要求同时进行 API 替换和响应式更新(例如,观察特征变化)时,这两者构成一个不可拆分的原子性更改——绝不能只做其中一项。 如果无法完成任务指定的所有部分,就不要进行部分更改——要么完成整个模式,要么跳过并明确说明原因。
- 保留无关的保护逻辑和回退方案。 移除对目标 API 的引用时,只修改该引用。不要同时删除
respondsToSelector:检查、nil 保护、if (x != nil)防御、版本检查(#available、@available)或调用位置周围的其他防御逻辑——除非用户明确要求删除。每项保护逻辑都有其独立原因(跨 SDK 版本的选择器可用性、nil 窗口安全、功能标志);现代化只涉及要替换的值,不涉及周边控制流。 - 防止替换偏离目标。 修改任何行之前,请确认两点:(a) 该行包含活动任务的目标弃用 API;(b) 你修改的是用户要求处理的弃用项,而不是附近一个“看起来相似”的行。
前置条件
在每个项目的 Phase 1 之前,运行一次全部三项检查。这些检查是只读的:不要在 Info.plist 或构建设置中添加键、修改值或删除键。报告你读取到的内容,然后继续工作流。无论源代码如何,未通过检查的应用都无法调整大小。
读取目标 Info.plist 中以及构建设置里的每个键。构建设置位于项目文件和任何 .xcconfig 文件中。构建会合并所有来源,因此报告前要全部读取。某个来源中缺少键并不能说明什么。当两个来源设置了同一个键时,以项目文件为准。
如果请求指定的是文件而非项目,则跳过全部三项检查。不要提及这些检查。不要在文件系统中搜索项目。
| 检查 | 已满足的条件 | 否则报告 |
|---|---|---|
| 启动画面 | 目标设置了 UILaunchScreen、UILaunchScreens、UILaunchStoryboardName、UILaunchStoryboards 或 INFOPLIST_KEY_UILaunchScreen_Generation | 应用未声明启动画面。iOS 27 会以 ITMS-90870 拒绝 App Store 上传。请参阅 TN3208。有一个键就足够,因此请保留现有的 storyboard |
| iPad 方向 | iPad 声明列出了全部四种方向,或目标未作任何声明。UISupportedInterfaceOrientations~ipad 是 iPad 声明;如果不存在该键,则使用普通的 UISupportedInterfaceOrientations | 指出声明中缺少哪些方向。补充说明,supportedInterfaceOrientations 覆写仍可能锁定场景,并说明你没有检查是否存在此类覆写。仅检查 iPad:iPhone 仅支持纵向是正常的 |
| 退出全屏限制 | 未设置 UIRequiresFullScreen,或已设置 UIRequiresFullScreenIgnoredStartingWithVersion | 目标设置了 UIRequiresFullScreen。iOS 27 会忽略它,并仍然调整场景大小。请参阅 TN3192 |
最后一行中的两个键都必须保持原样。绝不要删除 UIRequiresFullScreen,因为删除后应用会立即变为可调整大小,而布局可能尚未准备好。绝不要添加 UIRequiresFullScreenIgnoredStartingWithVersion,因为它的值决定哪些版本继续沿用旧行为,这是开发者的决定。值为 27 或更早时,应用在 iOS 27 上会完全支持调整大小,这与安全迁移的做法恰恰相反。应用能够处理大小调整后,应移除该键。确认这一点需要运行应用并调整其大小。即使其余部分正常,也可能有一个视图控制器出现问题。
工作流
阶段 0:简单情况的快速处理
在查阅决策树之前,先检查该用法是否符合简单情况。 很多 UIScreen.main/UIScreen.mainScreen 用法都是在 UIView/UIViewController 实例方法中进行的简单替换,且值会即时使用。这些情况无需分析——直接替换即可:
| 原始写法 | 替换为 |
|---|---|
UIView/UIViewController 实例方法中内联使用(未存储)的 UIScreen.main.scale(Swift) | self.traitCollection.displayScale |
UIView/UIViewController 实例方法中内联使用(未存储)的 [UIScreen mainScreen].scale(ObjC) | self.traitCollection.displayScale |
layoutSubviews、drawRect:、updateConstraints 或 viewIsAppearing: 中的 UIScreen.main.scale | self.traitCollection.displayScale(无需注册——UIKit 会在特征变化时自动调用这些方法) |
不要过度纠结简单替换。 如果封闭类是 UIView/UIViewController,且该值没有赋给实例变量、图层属性、约束或已存储的图像,就直接替换。简单文件中出现空差异是最常见的错误——请应用替换后继续。 仅当简单情况不适用时(非视图类、缓存值、类/静态方法、用户有特殊说明),才查阅决策树。
阶段 1:检测
使用每个相关任务文件中的检测模式,找出需要现代化改造的用法。针对任务注册表中适用于此代码库的每项任务运行检测,而不只是其中一项——见下文的任务注册表。
阶段 2:分析
对于每处用法,请阅读周边上下文,以了解:
- 类层次结构(UIView/UIViewController 子类,还是普通 NSObject,或非视图类)
- 方法类型(实例方法、静态方法、自由函数、缓存的
dispatch_once辅助方法) - 生命周期阶段(init、viewDidLoad、viewWillAppear、layoutSubviews)
- 代码意图(代码要判断什么:可用空间、布局变体、渲染、设备类别、生命周期)
当前任务的参考文件可能会在此列表中添加特定任务的条目。
使用子 Agent 找出需要更新的代码,以缩小上下文窗口的占用。
阶段 3:决策与验证
| 条件 | 操作 |
|---|---|
| 存在安全的一对一替换方案 | 应用替换。 不添加任何说明(不要添加 // TODO: FIXME、// TODO 或 // FIXME——只做替换)。使用当前任务参考文件中指定的替换方案。 |
| 存在多种有效方案,或代码移动超过 10 行 | 询问用户。 |
| 无法安全替换(极少见) | 添加 todo,明确列出交由用户处理的任务。绝不能悄无声息地留下空差异。得出不适用的结论前,请用子 Agent 重新检查每种模式。 |
进行任何代码更改前,使用子 Agent 对照当前任务的 Post-file Checklist 进行验证。
阶段 3b:文件处理完整性
处理每个包含目标弃用 API 的文件。 不要提前停止、跳过文件,也不要悄悄将文件从待处理队列中移除。阶段 1 中识别出的文件,如果没有产生差异,也没有说明跳过原因,就属于处理失败。
明确跟踪文件: 开始处理时,使用可用的任务 / 待办工具或 Markdown 文件,列出所有需要修改的文件。处理每个文件时,将其标记为已完成。完成前,将这份列表与输出进行核对——任何没有差异或明确跳过原因的文件,都必须在完成前处理。
上下文长度: 如果担心上下文长度,请使用子 Agent 处理单个文件或任务。
防止静默遗漏: 完成前,使用子 Agent 对比收到的文件列表与实际生成输出的文件列表。如果有文件未出现在输出中,请返回并处理。静默遗漏的常见原因:
- 文件大小: 大型文件(1000 行以上)也必须处理。不要例外。
- 复杂性: 包含预处理器宏、复杂类层次结构或特殊代码模式的文件仍需修改。
- 项目分组: 不要跳过某个特定项目或目录中的所有文件。如果发现同一项目中的多个文件被遗漏,说明可能存在系统性问题——请调查并修复。
- 存在歧义: 如果不确定如何修复文件,请询问用户——不要悄悄生成空差异。
大型或复杂文件: 大量使用预处理器(#if / #ifdef 嵌套)、超过 1000 行,或包含较少见模式(C++ 互操作、dispatch_once 缓存、深层嵌套宏)的文件也必须处理。如果此类文件中出现目标 API,请按相同的决策树处理。如果文件太大,无法一次编辑完成,请逐个处理弃用 API 的用法。必要时可使用子 Agent。如果确实无法根据宏展开或预处理器的复杂性判断出安全的替代方案,请询问用户——绝不要悄悄跳过。
批量处理规范: 处理文件列表时,不要先分析所有文件,再一次性生成所有差异。应当逐个文件或按小批次(3–5 个文件)处理:阅读上下文、作出决定、生成差异,然后再处理下一批。这样可避免因输出限制或上下文耗尽而遗漏列表末尾的文件。如果发现生成输出的文件少于收到的文件,请立即停止,处理剩余文件后再完成。
如果发现某些文件的差异为空,而它们本应可以直接替换,请返回并处理这些文件——简单文件处理起来很快,绝不应被遗漏。
阶段 4:实施
应用当前任务参考文件中的实施门槛、规则和文件处理后检查清单。针对特定模式的决策树、门槛问题和验证规则,位于各任务文件中与其对应的模式旁。使用子 Agent 进行验证。
阶段 5:最终验证
文件覆盖审计: 使用子 Agent 对比收到的文件列表(或阶段 1 中检测到的文件列表)与实际生成差异的文件列表。每个输入文件都必须有非空差异。如果有文件缺少更改,请返回并立即处理。
当前任务的参考文件可能会补充特定任务的验证步骤。
任务注册表
除非开发者的请求明确限定了范围,否则请将此注册表中的所有任务应用于代码库。每项任务彼此独立,并在其参考文件中包含各自的检测模式、决策树和验证规则。请从上到下按顺序执行。
| 任务 | 文件 | 描述 |
|---|---|---|
| UIScreen.main 现代化 | uiscreen-task.md | 将 UIScreen.main 替换为适合当前上下文的 API |
| userInterfaceOrientation 现代化 | orientation-task.md | 将与布局相关的方向检查替换为尺寸类别或窗口边界,并迁移已弃用的场景几何回调 |
| 场景生命周期迁移 | scene-lifecycle-task.md | 将 AppDelegate 迁移到 SceneDelegate |
| 安全区域内边距 | safe-area-task.md | 替换 topLayoutGuide 和 bottomLayoutGuide,将硬编码的内边距值替换为安全区域引用,并确保现有引用适用于非对称安全区域以及应用运行期间发生变化的内边距 |
| userInterfaceIdiom 现代化 | idiom-task.md | 将与布局相关的设备类型检查替换为尺寸类别,并从本地特征集合中读取确实需要的设备类型 |