前言
在 SwiftUI 开发中,你是否遇到过这些痛点?
- List 性能在大数据量下表现不佳
- 原生 Refreshable 功能有限,自定义困难
- 上拉加载更多需要自己实现,逻辑繁琐
- Section Header/Footer 吸顶效果难以控制
- SwiftUI 与 UIKit 混编时状态同步麻烦
基于这些问题,我封装了 MJTableView —— 基于 UITableView + MJRefresh + UIHostingConfiguration ,通过 Diffable Data Source 的 reconfigure 保证 SwiftUI 状态实时同步。
核心功能
- ✅ 流畅的下拉刷新
- ✅ 智能的上拉加载
- ✅ 优雅的 Section 吸顶
- ✅ 灵活的多 Cell 支持
- ✅ SwiftUI 式的声明式 API
- ✅ 自动状态同步
一、单 Section 列表:MJTableView
1.1 最简用法
MJTableView(items: $items, onRefresh:
refresh, onLoadMore: loadMore) { item in
FeedCell(item: item)
}
1.2 完整配置示例
MJTableView(
items: $items,
hasMore: hasMore,
supplementary: supplementary,
style: .plain,
separatorStyle: .singleLine,
rowHeight: nil,
showsVerticalScrollIndicator: true,
onRefresh: refresh,
onLoadMore: loadMore
) { item in
FeedCell(item: item)
}
1.3 附属视图配置(链式调用)
let supplementary = MJTableSupplementary()
.tableHeader(height: 200) {
BannerView()
}
.sectionHeader(height: 44) {
SectionTitleView(title: "推荐")
}
.sectionFooter(pinToBottom: true) {
Text("共 \(items.count) 条")
}
.tableFooter(height: 56) {
FooterView()
}
关键参数说明:
- pinToBottom: true :Section Footer 贴在列表最底部(不足一屏时自动留白)
- height: nil :自动计算高度( systemLayoutSizeFitting )
二、多 Section 列表:MJSectionedTableView
2.1 定义 Section 类型
enum FeedSection: Hashable {
case hot
case feed
}
2.2 构建分区数据
@State private var sections:
[MJTableSection<FeedSection, Item>] = [
MJTableSection(id: .hot, items:
hotItems) {
.sectionHeader { HotHeaderView() }
},
MJTableSection(id: .feed, items:
feedItems) {
.sectionHeader { FeedHeaderView
() }
}
]
2.3 使用多 Section 列表
MJSectionedTableView(sections: $sections)
{ section, item in
switch section {
case .hot: HotCell(item)
case .feed: FeedCell(item)
}
}
三、多种 Cell 类型支持
3.1 定义多类型 Row
enum Row: Identifiable, Hashable,
MJTableCellTypable {
case post(Post)
case banner(String)
case ads(Ad)
var id: String { /* ... */ }
var mjCellReuseID: String {
switch self {
case .post: return "PostCell"
case .banner: return "BannerCell"
case .ads: return "AdsCell"
}
}
var mjRowHeight: CGFloat? {
switch self {
case .banner: return 120
case .ads: return 80
default: return nil
}
}
}
3.2 使用多 Cell 类型
MJTableView(
items: $rows,
cellReuseID: { $0.mjCellReuseID },
rowHeightForItem: { $0.mjRowHeight }
) { row in
switch row {
case .post(let p): PostCell(p)
case .banner(let t): BannerCell(t)
case .ads(let a): AdsCell(a)
}
}
四、下拉刷新与上拉加载
MJTableView(
items: $items,
hasMore: hasMore,
onRefresh: { end in
Task {
await fetchData()
end()
}
},
onLoadMore: { end in
Task {
let more = await loadMoreData
()
end(more)
}
}
) { item in
FeedCell(item: item)
}
五、状态同步:SwiftUI 响应式更新
封装内部通过 Diffable Data Source 实现状态同步:
if itemIDs == previousIDs {
reconfigureAllItems()
} else {
reloadSnapshot(animating: true)
}
配合 configurationUpdateHandler 实现 Cell 级别的实时更新:
cell.configurationUpdateHandler = { [weak
self] cell, _ in
guard let item = self?.item(for: id)
else { return }
cell.contentConfiguration =
UIHostingConfiguration {
CellView(item: item)
}
}
六、技术实现细节
6.1 核心架构
- UIViewControllerRepresentable :桥接 UIKit 和 SwiftUI
- UIHostingConfiguration :将 SwiftUI View 嵌入 UITableViewCell
- UITableViewDiffableDataSource :高效的数据更新
- MJRefresh :下拉刷新和上拉加载
6.2 关键技术点 1. 避免 UITableViewController 尺寸为 0
final class MJTableViewController<Item,
CellContent: View>: UIViewController,
UITableViewDelegate {
let tableView: UITableView
init(...) {
self.tableView = UITableView
(frame: .zero, style: style)
super.init(nibName: nil, bundle:
nil)
// 使用 Auto Layout 嵌入 SwiftUI
}
}
- 自定义吸顶条避免缝隙
private let stickySectionHeaderBar =
UIView()
private var stickySectionHeaderHosting:
UIHostingController<AnyView>?
private var
stickySectionHeaderHeightConstraint:
NSLayoutConstraint?
- Section Footer 贴底实现
private func layoutPinnedBottomFooterIfNeeded() {
guard usesPinnedSectionFooter, let container = pinnedBottomContainer
else { return }
let visibleHeight = tableView.bounds.
height
- tableView.adjustedContentInset.
top
- tableView.adjustedContentInset.
bottom
let spacerHeight = max(0,
visibleHeight -
contentHeightWithoutBottom -
footerContentHeight)
pinnedBottomSpacer.frame = CGRect(x:
0, y: 0, width: width, height:
spacerHeight)
sectionView.frame = CGRect(x: 0, y:
spacerHeight, width: width, height:
sectionHeight)
}
七、避坑指南
7.1 MJRefresh 控制台警告
问题 : NSBundle (null) initWithPath failed
原因 :MJRefresh 默认用系统语言(如 zh-Hans-CN )去 MainBundle 找 .lproj ,路径为空。
解决方案 :在 App.init 中调用配置:
MJRefreshConfigurator.setup()
7.2 Section Header 吸顶缝隙
问题 :系统 Section Header 与 MJRefresh/contentInset 叠加产生顶部缝隙
解决方案 :封装内部使用自定义吸顶条,自动处理边界情况。
八、完整示例代码
struct MJTableViewDemoView: View {
@State private var items: [FeedItem]
= []
@State private var hasMore = true
@State private var nextPage = 1
private var supplementary:
MJTableSupplementary {
MJTableSupplementary()
.tableHeader {
DemoTableHeaderView
(itemCount: items.count)
}
.sectionHeader(height: 44) {
DemoSectionHeaderView()
}
.sectionFooter(pinToBottom:
true) {
DemoSectionFooterView
(itemCount: items.count,
hasMore: hasMore)
}
.tableFooter(height: 52) {
DemoTableFooterView()
}
}
var body: some View {
NavigationStack {
MJTableView(
items: $items,
hasMore: hasMore,
supplementary:
supplementary,
onRefresh: refresh,
onLoadMore: loadMore
) { item in
FeedRowView(item: item)
}
}
.task {
guard items.isEmpty else {
return }
await fetchPage(page: 1,
replacing: true)
}
}
private func refresh(end: @escaping
() -> Void) {
Task {
await fetchPage(page: 1,
replacing: true)
end()
}
}
private func loadMore(end: @escaping
(Bool) -> Void) {
Task {
let more = await fetchPage
(page: nextPage, replacing:
false)
end(more)
}
}
}
九、总结
MJTableView 的核心设计理念:
- 性能优先 :基于 UITableView,大数据量依然流畅
- API 友好 :SwiftUI 声明式风格,学习成本低
- 功能完备 :下拉刷新、上拉加载、Section 吸顶一应俱全
- 扩展性强 :支持多 Cell 类型、自定义复用池、独立行高
如果你也在 SwiftUI 中饱受列表性能问题困扰,不妨试试这个封装!
Gitee 🔗