一键导入
xxf-aabb-list-vc-template
创建 XXF iOS 列表页模板(DiffableDataSource + BaseCollectionViewCell + StatefulView + 分页 ViewModel)
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
创建 XXF iOS 列表页模板(DiffableDataSource + BaseCollectionViewCell + StatefulView + 分页 ViewModel)
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
规范 ViewController 与 ViewModel 的分区组织方式。用于治理成员变量和方法过多、顺序混乱、阅读成本高的问题;通过 MARK 分区和职责分层保持代码导航清晰。
处理 XXF iOS 项目中的通用编码任务交付流程。用于 bugfix、功能开发、重构、回归修复等未显式指明测试或 review 的日常 coding 请求;负责自动串起模块 skill、补测、验证、代码审查与风险门禁。
iOS 性能门禁与主动鉴别。针对常规 coding 改动自动识别性能风险(主线程阻塞、列表卡顿、内存抖动、启动耗时、无效并发、过度渲染),并执行最小可行验证与门禁结论,不依赖用户额外提示。
Vibe Coding 通用治理闭环。用于把模糊需求转成可验证交付:先定义问题与边界,再走拼好码优先、最小改动、硬门禁验证、证据化交付和风险结论。适用于需求澄清、实现前规划、AI 协作治理、质量门禁落地。
XXFViewModel MVVM 的 VM 基类与生命周期。当用户要写 ViewModel、绑定 View、处理输入输出流,或询问"XXF 的 MVVM 怎么用"时使用。若出现 ViewModel 成员或方法膨胀、顺序混乱,应联动 `xxf-aaa-class-declaration-guidelines` 做分区治理。
产出 ADR/RFC 技术决策文档,明确背景、备选方案、权衡、风险、迁移与回滚策略,避免口头决策和重复争论。
| name | xxf-aabb-list-vc-template |
| description | 创建 XXF iOS 列表页模板(DiffableDataSource + BaseCollectionViewCell + StatefulView + 分页 ViewModel) |
ViewControllerUICollectionViewDiffableDataSourceBaseCollectionViewCell<Model>StatefulView(loading / empty / error / content)ViewController 继承 BaseViewControllerViewModel 继承 NSObject, ViewModelUICollectionViewDiffableDataSource<SingleSection, Item>dequeueReusableCell(for:cellType:),并调用 configure(with:payloads:)ViewState<[Item]>willDisplay,不要和 scrollViewDidScroll 双触发DiffableDataSource 首次创建后应 ensureSection(.main, animatingDifferences: false)final class XxxListViewController: BaseViewController {
private lazy var viewModel: XxxListViewModel = ViewModelProvider(owner: self).of(XxxListViewModel.self)
private var statefulView: StatefulView!
private lazy var collectionView: UICollectionView = {
let cv = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
cv.backgroundColor = .clear
cv.showsVerticalScrollIndicator = false
cv.delegate = self
return cv
}()
private lazy var dataSource: UICollectionViewDiffableDataSource<SingleSection, ItemDTO> = {
let ds = UICollectionViewDiffableDataSource<SingleSection, ItemDTO>(collectionView: collectionView) { collectionView, indexPath, item in
collectionView.dequeueReusableCell(for: indexPath, cellType: XxxCell.self).apply { cell in
cell.configure(with: item, payloads: nil)
}
}
ds.ensureSection(.main, animatingDifferences: false)
return ds
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
viewModel.loadData(isRefresh: true)
}
private func setupUI() {
setupStatefulView()
collectionView.addRefreshing { [weak self] in
self?.viewModel.loadData(isRefresh: true)
}
collectionView.addLoadingMore { [weak self] in
self?.viewModel.loadData(isRefresh: false)
}
view.addSubview(statefulView)
statefulView.snp.makeConstraints { $0.edges.equalToSuperview() }
}
private func setupStatefulView() {
statefulView = StatefulView(contentView: collectionView) { [weak self] in
self?.viewModel.loadData(isRefresh: true)
}
let emptyView = CommonEmptyView(title: "No data", message: "Please check back later.", buttonTitle: "Refresh")
emptyView.onRetry = { [weak self] in self?.viewModel.loadData(isRefresh: true) }
statefulView.setStateView(emptyView, for: .empty)
let errorView = DefaultErrorView(title: "Something went wrong", message: "Please try again", buttonTitle: "Retry")
errorView.onRetry = { [weak self] in self?.viewModel.loadData(isRefresh: true) }
statefulView.setStateView(errorView, for: .error)
}
private func bindViewModel() {
_ = viewModel.items
.bindLifecycle(of: self)
.observeOnMainIfNeeded()
.subscribe(onNext: { [weak self] items in
self?.dataSource.replaceItems(items, in: .main)
})
_ = viewModel.viewState
.bindLifecycle(of: self)
.observeOnMain()
.subscribe(onNext: { [weak self] state in
guard let self else { return }
switch state {
case .idle:
break
case .loading:
// 仅首屏无数据时展示整页 loading,避免分页时闪回 loading 页
if self.dataSource.snapshot().numberOfItems == 0 {
self.statefulView.update(state: .loading)
}
case .success(let items):
self.statefulView.update(state: items.isEmpty ? .empty : .content)
case .error:
// 有历史数据时保留 content,仅依赖 toast/局部提示;无数据才切 error 页
if self.dataSource.snapshot().numberOfItems == 0 {
self.statefulView.update(state: .error)
}
case .completed:
break
}
})
_ = viewModel.refreshableState
.bindLifecycle(of: self)
.bind(to: collectionView.rx.refreshableState)
}
}
extension XxxListViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView,
willDisplay cell: UICollectionViewCell,
forItemAt indexPath: IndexPath) {
let total = dataSource.snapshot().numberOfItems
guard total > 0, indexPath.item >= total - 3 else { return }
viewModel.loadData(isRefresh: false)
}
}
final class XxxCell: BaseCollectionViewCell<ItemDTO> {
override func configure(with model: ItemDTO, payloads: [Any]?) {
super.configure(with: model, payloads: payloads)
// bind UI
}
}
final class XxxListViewModel: NSObject, ViewModel {
@LazyInjected(\\.xxxService) private var service
let refreshableState = RefreshableState().obs
let viewState: Obs<ViewState<[ItemDTO]>> = .init(value: .idle)
let items: Obs<[ItemDTO]> = .init(value: [])
private var page = 1
private let limit = 20
private var hasMore = true
func loadData(isRefresh: Bool) {
if case .loading = viewState.state { return }
if isRefresh {
page = 1
hasMore = true
viewState.state = .loading
} else if !hasMore {
refreshableState.state = refreshableState.state.copy(isLoadingMore: false)
viewState.state = .completed
return
}
_ = service.fetchList(page: page, limit: limit, cacheType: isRefresh ? .firstCache() : .onlyRemote)
.bindRefreshableState(of: refreshableState, isRefresh: isRefresh)
.bindErrorNotice()
.observeOnMain()
.bindUntilDeallocated(of: self)
.doOnCompleted { [weak self] in
self?.page += 1
}
.subscribe(onNext: { [weak self] response in
guard let self else { return }
self.hasMore = response.pagination.hasMore
if isRefresh {
// 服务端偶发重复 ID,refresh / loadMore 都统一去重,避免 diffable crash
self.items.state = response.list.distinctBy(\\.id)
} else {
self.items.state = (self.items.state + response.list).distinctBy(\\.id)
}
self.viewState.state = .success(self.items.state)
}, onError: { [weak self] error in
self?.viewState.state = .error(error)
})
}
}
scrollViewDidScroll 和 willDisplay 触发分页register(cellType:) 后又使用 dequeueReusableCell(for:cellType:)refreshableState,导致刷新控件状态不同步page/hasMore),而不是放在 ViewModel