mcpField
U
uikit-expert

uikit-expert

github.com/ivan-magda开发8745 次安装19 周前更新
代码仓库

关于 uikit-expert

面向实现、审查或改进 iOS 界面的开发者,提供 UIKit 指南。内容按工作流程和领域检查表组织,优先关注正确性,并将性能优化作为可选项。

主要功能

  1. 01按领域划分的审查清单
  2. 02现代 API 替换建议
  3. 03生命周期与 Auto Layout 实践
  4. 04集合视图与导航模式
  5. 05内存与并发指导
  6. 06无障碍与自适应界面指导

适用场景

  1. 01检查 UIKit 代码的正确性
  2. 02将弃用的 UIKit API 替换为现代 API
  3. 03构建集合视图和导航流程
  4. 04检查无障碍支持与内存处理

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ivan-magda/uikit-expert-skill --skill uikit-expert

安装后在 Claude Code 中输入/uikit-expert确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.tessl-plugin/
plugin.json
references/
adaptive-appearance.md
animation-patterns.md
auto-layout.md
cell-configuration.md
concurrency-main-thread.md
image-loading.md
keyboard-scroll.md
list-performance.md
memory-management.md
modern-collection-views.md
modern-uikit-apis.md
navigation-patterns.md
uikit-swiftui-interop.md
view-controller-lifecycle.md

详细介绍

UIKit 专家技能

概述

优先采用原生 API 和 Apple 的官方指导。操作原则:事实优先于架构(不强制采用 MVVM/VIPER/Coordinator,但应分离业务逻辑以便测试);先保证正确性,再考虑性能;只有涉及正确性时才使用“始终/绝不”,可选优化则使用“建议/考虑”。

工作流

以下三种模式都基于下面的核心指南;每个主题都链接到相应的参考文件。选择切入点:

  • 审查 — 按照审查清单逐项检查,通过或不通过。对于任何未通过项,都按照对应的核心指南进行修复。优先处理正确性检查项(生命周期、Auto Layout、内存、并发),样式项次之;性能项视为可选。
  • 改进 — 针对每个问题,应用“已弃用 → 现代方案”表中的现代替代方案以及对应的核心指南。性能优化(降采样、预取、减少约束变更)应作为可选优化提出,绝不能作为强制要求。
  • 实现新功能 — 按以下顺序构建,并在每一步应用相关的核心指南:
    1. 设计数据流 — 状态所有权、注入依赖、模型层
    2. 生命周期 — 在 viewDidLoad 中进行一次性设置,在 viewIsAppearing 中处理几何布局
    3. Auto Layout — 批量激活,避免反复变更
    4. Collection View — DiffableDataSource + CompositionalLayout + CellRegistration
    5. 导航 — 覆盖全部 4 个外观插槽,防范并发转场
    6. 动画、内存([weak self]、Task 取消)、并发(@MainActor)
    7. 互操作、图像加载、键盘、动态字体 / VoiceOver / 深色模式
    8. 使用 #available 为 iOS 26+ 功能添加门控,并提供合理的回退方案

核心指南

视图控制器生命周期 — 参见 references/view-controller-lifecycle.md
  • 使用 viewDidLoad 进行一次性设置:子视图、约束、代理 — 不要处理几何布局
  • 使用 viewIsAppearing(向后部署至 iOS 13+)处理依赖几何信息的工作、基于 trait 的布局、滚动到指定项目
  • viewDidLayoutSubviews 会触发多次 — 仅用于轻量级图层 frame 调整
  • viewWillAppear 仅用于转场协调器动画和成对的通知注册
  • 每个生命周期重写方法中都必须调用 super
  • 子视图控制器容器:按此顺序执行 addChild → addSubview → didMove(toParent:)
  • 开发期间通过 deinit 日志确认对象已释放
Auto Layout — 参见 references/auto-layout.md
  • 对以编程方式创建的视图,始终设置 translatesAutoresizingMaskIntoConstraints = false
  • 使用 NSLayoutConstraint.activate([]) — 绝不单独设置 .isActive = true
  • 只创建一次约束,通过切换 isActive 或修改 .constant 来调整 — 绝不移除后重新创建
  • 运行时绝不将优先级改为或改出 .required(1000)— 使用 999
  • 为约束添加动画:更新 constant → 在超视图的动画代码块中调用 layoutIfNeeded()
  • iOS 26+:使用 .flushUpdates 选项简化约束动画
  • 避免在可复用单元格中深度嵌套 UIStackView
  • 为关键约束设置 constraint.identifier,让 Unable to simultaneously satisfy constraints 日志更易读
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false   // required on programmatic views
view.addSubview(label)
NSLayoutConstraint.activate([                              // one batch = one solve pass
    label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
    label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
Collection View 与数据源 — 参见 references/modern-collection-views.md、references/cell-configuration.md、references/list-performance.md
  • 使用 UICollectionViewDiffableDataSource 和稳定标识符(UUID/数据库 ID,而非完整模型结构体)
  • 内容更新时使用 reconfigureItems,仅在单元格类型发生变化时使用 reloadItems
  • 初始填充时使用 applySnapshotUsingReloadData(跳过差异计算)
  • 对任何非简单布局都使用 UICollectionViewCompositionalLayout
  • 使用 UICollectionView.CellRegistration — 不使用字符串标识符,也不进行手动类型转换
  • 使用 UIContentConfiguration 配置单元格内容,使用 UIBackgroundConfiguration 配置单元格背景
  • 使用 configurationUpdateHandler 实现由状态驱动的样式(选中、高亮)
// Store the registration once — never create it inside the cell provider (crashes on iOS 15+).
private lazy var cellRegistration = UICollectionView.CellRegistration<PhotoCell, Photo> { cell, _, photo in
    cell.configure(with: photo)
}

// Data source is keyed by the stable ID, so the provider receives an id — look up the model, then configure with it.
dataSource = .init(collectionView: collectionView) { [weak self] cv, indexPath, id in
    guard let self, let photo = self.photo(for: id) else { return nil }
    return cv.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: photo)
}

var snapshot = NSDiffableDataSourceSnapshot<Section, Photo.ID>()
snapshot.appendSections([.main])
snapshot.appendItems(photos.map(\.id))
dataSource.apply(snapshot, animatingDifferences: true)
导航 — 请参阅 references/navigation-patterns.md
  • 配置所有 4 个 UINavigationBarAppearance 插槽(standard、scrollEdge、compact、compactScrollEdge)
  • 在 viewDidLoad 中为 navigationItem(每个 VC 单独配置)设置外观,而不是在 viewWillAppear 中为 navigationBar 设置外观
  • 使用 setViewControllers(_:animated:) 处理深度链接,不要依次调用 push
  • 防止并发转场 — push/pop 前检查 transitionCoordinator
  • 在导航栏上仅设置一次 prefersLargeTitles;针对每个 VC 使用 largeTitleDisplayMode
动画 — 请参阅 references/animation-patterns.md
  • UIView.animate — 用于简单的一次性动画;在 completion 中检查 finished
  • UIViewPropertyAnimator — 用于手势驱动、可中断的动画;遵循状态机(inactive → active → stopped)
  • CABasicAnimation — 用于仅限图层的属性(cornerRadius、shadow、3D transforms);先设置模型值
  • iOS 17+ 弹簧 API:UIView.animate(springDuration:bounce:) 与 SwiftUI 保持一致
  • 约束动画:刷新布局 → 更新常量 → 在 superview 上对 layoutIfNeeded() 进行动画处理
内存管理 — 请参阅 references/memory-management.md
  • 所有逃逸闭包默认使用 [weak self]
  • Timer:使用带 [weak self] 的基于 block 的 API,并在 viewWillDisappear 中使其失效
  • CADisplayLink:使用弱代理模式(没有可用的基于 block 的 API)
  • NotificationCenter:在闭包中使用 [weak self],并在 deinit 中移除观察者
  • 嵌套闭包:在存储的内部闭包中重新捕获 [weak self]
  • 委托:始终使用带 AnyObject 约束的 weak var delegate: SomeDelegate?
  • 使用 deinit 验证是否释放;如果从未调用,则存在循环引用
并发 — 请参阅 references/concurrency-main-thread.md
  • UIViewController 是 @MainActor — 所有子类方法都隐式运行于主 actor
  • 存储 Task 引用,并在 viewDidDisappear 中取消,而不是在 deinit 中取消
  • await 后更新 UI 前检查 Task.isCancelled
  • Task.detached 不会继承 actor 隔离 — 更新 UI 需要显式调用 MainActor.run
  • 切勿从后台调用 DispatchQueue.main.sync — 使用 await MainActor.run
UIKit–SwiftUI 互操作 — 请参阅 references/uikit-swiftui-interop.md
  • UIHostingController:完整执行子 VC 容器管理(addChild → addSubview → didMove),并将其保留为存储属性
  • 对 Auto Layout 容器使用 sizingOptions = .intrinsicContentSize(iOS 16+)
  • UIViewRepresentable:在 updateUIView 中设置可变状态,而不是在 makeUIView 中;防止更新循环
  • 在 collection view 单元格中使用 UIHostingConfiguration(iOS 16+)显示 SwiftUI 内容
图像加载 — 请参阅 references/image-loading.md
  • 解码后的位图大小 = 宽度 × 高度 × 4 字节(12MP 照片约占 48MB RAM)
  • 使用 ImageIO 按显示尺寸缩小采样 — 切勿加载完整位图后再调整大小
  • iOS 15+:使用 byPreparingThumbnail(of:) 或 prepareForDisplay() 进行异步解码
  • 单元格复用:在 prepareForReuse 中取消 Task、清除图像,并在完成时验证身份
键盘与滚动 — 请参阅 references/keyboard-scroll.md
  • 使用 UIKeyboardLayoutGuide(iOS 15+)— 将内容底部固定到 view.keyboardLayoutGuide.topAnchor
  • iPad:为浮动键盘设置 followsUndockedKeyboard = true
  • 使用布局指南替换所有手动键盘通知处理
自适应布局与辅助功能 — 请参阅 references/adaptive-appearance.md
  • 使用 registerForTraitChanges(iOS 17+)代替已弃用的 traitCollectionDidChange
  • Dynamic Type:使用 UIFont.preferredFont(forTextStyle:) + adjustsFontForContentSizeCategory = true
  • 深色模式:使用语义颜色(.label、.systemBackground);在 trait 变化时重新解析 CGColor
  • VoiceOver:为自定义视图设置 accessibilityLabel、accessibilityTraits、accessibilityHint
  • 对复杂的列表项操作使用 UIAccessibilityCustomAction

快速参考 — 已弃用 API → 现代 API

已弃用 / 旧版现代替代方案引入版本
traitCollectionDidChangeregisterForTraitChanges(_:handler:)iOS 17
键盘通知UIKeyboardLayoutGuideiOS 15
cell.textLabel / detailTextLabelUIListContentConfigurationiOS 14
register + 字符串出队UICollectionView.CellRegistrationiOS 14
对快照调用 reloadItemsreconfigureItemsiOS 15
barTintColor / isTranslucentUINavigationBarAppearance(4 个位置)iOS 13
UICollectionViewFlowLayout(复杂布局)UICollectionViewCompositionalLayoutiOS 13
动画中手动调用 layoutIfNeeded().flushUpdates 选项iOS 26
旧版应用生命周期UIScene + SceneDelegateiOS 26 起必须使用
ObservableObject + 手动失效@Observable + UIObservationTrackingEnablediOS 18

审查清单

每个领域设一道关卡——需要验证的正确性事项(详情见上方对应的核心指南):

  • 生命周期 — 不在 viewDidLoad 中获取几何信息(使用 viewIsAppearing);每个重写方法都调用 super;子 VC = addChild → addSubview → didMove;deinit 确认对象已释放
  • 自动布局 — translatesAutoresizingMaskIntoConstraints = false;NSLayoutConstraint.activate([]);避免反复变更(切换 isActive / .constant);不要在 layoutSubviews 中调用 setNeedsLayout()(会导致无限循环)
  • 集合视图 — 使用 diffable 和稳定标识符;使用 reconfigureItems 而非 reloadItems;快照中不能有重复 ID(会触发 BUG_IN_CLIENT 崩溃);使用 CellRegistration 而非字符串出队
  • 导航 — 配置全部 4 个外观位置;在 navigationItem 上配置,而非 navigationBar;添加并发转场保护
  • 动画 — API 与使用场景相符;约束动画按顺序刷新 → 更新 → 动画;CAAnimation 先设置模型值
  • 内存 — 在逃逸闭包中使用 [weak self];在 viewDidDisappear 中取消 Task;代理使用 weak;嵌套闭包中不要重新强引用 self
  • 并发 — await 后检查 Task.isCancelled;不要从后台调用 DispatchQueue.main.sync;VC 子类上不要重复标注 @MainActor
  • 图像 — 降采样至显示尺寸;单元格加载时在 prepareForReuse 中取消,并验证身份;NSCache 按解码后字节数设置容量
  • 互操作 — 保留 UIHostingController 并完整执行视图控制器包含流程;updateUIView 防止更新循环
  • 键盘 — 使用 UIKeyboardLayoutGuide,而非通知
  • 自适应 / 无障碍 — 使用 registerForTraitChanges;支持 Dynamic Type;在特征变化时重新解析 CGColor;为自定义视图设置 accessibilityLabel / Traits
  • iOS 26+ — 使用带回退方案的 #available 保护;采用 UIScene 生命周期;考虑使用 UIObservationTrackingEnabled

参考资料

  • references/view-controller-lifecycle.md — 生命周期顺序、viewIsAppearing、子视图控制器的包含关系
  • references/auto-layout.md — 批量激活、约束变更、优先级、动画、调试
  • references/modern-collection-views.md — 可差异数据源、组合式布局、CellRegistration
  • references/cell-configuration.md — UIContentConfiguration、UIBackgroundConfiguration、configurationUpdateHandler
  • references/list-performance.md — 预取、单元格复用、reconfigureItems、滚动性能
  • references/navigation-patterns.md — 栏外观、并发转场、大标题、深层链接
  • references/animation-patterns.md — UIView.animate、UIViewPropertyAnimator、CAAnimation、弹簧动画
  • references/memory-management.md — 保留循环、[weak self]、Timer/CADisplayLink/嵌套闭包陷阱
  • references/concurrency-main-thread.md — @MainActor、Task 生命周期、Swift 6、GCD 迁移
  • references/uikit-swiftui-interop.md — UIHostingController、UIViewRepresentable、尺寸调整、状态桥接
  • references/image-loading.md — 降采样、解码后位图计算、单元格复用竞态条件
  • references/keyboard-scroll.md — UIKeyboardLayoutGuide、滚动视图内边距、iPad 浮动键盘
  • references/adaptive-appearance.md — 特征变化、动态字体、深色模式、VoiceOver、无障碍功能
  • references/modern-uikit-apis.md — Observation 框架、updateProperties()、.flushUpdates、UIScene、Liquid Glass

评论

支持 Markdown