UITableView嵌套UICollectionView:动态加载子分类数据求助
解决UITableView嵌套UICollectionView动态加载Realm数据的思路
我之前也做过类似Netflix的这种垂直+水平滚动的嵌套列表,你的核心问题其实是把嵌套的数据和每个Cell里的CollectionView精准绑定,而不是共用全局数据源导致混乱。下面一步步给你拆解:
1. 先把Realm数据模型对应好
首先得把你JSON结构转换成Realm的对象模型,这样从Realm取数据时能直接拿到结构化的层级数据,避免字典解析的混乱。根据你的JSON结构,我们需要三层模型:
// 最底层:具体的运动项目(比如Surfing、Football) class SportItem: Object { @Persisted var title: String = "" } // 中间层:分类(比如Water Based、Land Based) class SportSubCategory: Object { @Persisted var title: String = "" // 存储当前分类下的所有运动项目 @Persisted var category_content: List<SportItem> = List() } // 最外层:总分类(比如Sport Listings) class SportMainCategory: Object { @Persisted var title: String = "" // 存储所有子分类 @Persisted var category_content: List<SportSubCategory> = List() }
2. 准备UITableView的数据源
在你的ViewController里,定义一个数组来存储要展示的子分类(也就是Water/Land这一级),然后从Realm读取数据:
class SportsViewController: UIViewController { @IBOutlet weak var tableView: UITableView! // UITableView的数据源:所有子分类 private var subCategories: [SportSubCategory] = [] // Realm数据更新通知的token,用来监听数据变化 private var realmNotificationToken: NotificationToken? override func viewDidLoad() { super.viewDidLoad() setupTableView() loadRealmData() setupRealmNotification() } private func setupTableView() { tableView.delegate = self tableView.dataSource = self // 注册自定义的UITableViewCell tableView.register(UINib(nibName: "SportCategoryCell", bundle: nil), forCellReuseIdentifier: "SportCategoryCell") } private func loadRealmData() { let realm = try! Realm() // 取到最外层的Sport Listings分类 if let mainCategory = realm.objects(SportMainCategory.self).first { // 把Realm的List转成Swift数组(因为List是Realm专属集合) subCategories = Array(mainCategory.category_content) tableView.reloadData() } } // 监听Realm数据变化,自动刷新UI private func setupRealmNotification() { let realm = try! Realm() let mainCategories = realm.objects(SportMainCategory.self) realmNotificationToken = mainCategories.observe { [weak self] changes in guard let self = self else { return } switch changes { case .initial, .update: self.loadRealmData() case .error(let error): print("Realm数据更新出错:\(error.localizedDescription)") } } } deinit { // 销毁通知token,避免内存泄漏 realmNotificationToken?.invalidate() } } // 实现UITableView的数据源和代理 extension SportsViewController: UITableViewDataSource, UITableViewDelegate { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return subCategories.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SportCategoryCell", for: indexPath) as! SportCategoryCell let currentSubCategory = subCategories[indexPath.row] // 设置分类标题 cell.categoryTitleLabel.text = currentSubCategory.title // 把当前分类下的所有运动项目传给Cell里的CollectionView cell.sportItems = Array(currentSubCategory.category_content) return cell } // 可选:设置UITableViewCell的高度,比如固定高度或者自适应 func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 120 // 根据你的布局调整 } }
3. 自定义UITableViewCell,让它自己管理内部的CollectionView
这是关键!不要让ViewController同时管理TableView和所有CollectionView的数据源,而是让每个TableViewCell自己负责内部CollectionView的数据源和代理,这样每个Cell的数据都是独立的,不会串。
class SportCategoryCell: UITableViewCell { @IBOutlet weak var categoryTitleLabel: UILabel! @IBOutlet weak var collectionView: UICollectionView! // 当前分类下的运动项目,设置时自动刷新CollectionView var sportItems: [SportItem] = [] { didSet { collectionView.reloadData() } } override func awakeFromNib() { super.awakeFromNib() setupCollectionView() } private func setupCollectionView() { // 让当前Cell作为CollectionView的数据源和代理 collectionView.dataSource = self collectionView.delegate = self // 注册CollectionView的Cell collectionView.register(UINib(nibName: "SportItemCell", bundle: nil), forCellWithReuseIdentifier: "SportItemCell") // 设置CollectionView的布局为水平滚动 if let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.scrollDirection = .horizontal flowLayout.minimumLineSpacing = 10 flowLayout.minimumInteritemSpacing = 10 flowLayout.sectionInset = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10) } } } // 实现CollectionView的数据源 extension SportCategoryCell: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { // 直接返回当前分类下的运动项目数量 return sportItems.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SportItemCell", for: indexPath) as! SportItemCell let currentSport = sportItems[indexPath.item] cell.titleLabel.text = currentSport.title return cell } } // 可选:实现CollectionView的布局代理,自定义Cell大小 extension SportCategoryCell: UICollectionViewDelegateFlowLayout { func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { // 这里可以根据文字长度自适应宽度,或者固定宽度 let sportTitle = sportItems[indexPath.item].title let label = UILabel() label.text = sportTitle label.font = UIFont.systemFont(ofSize: 14) label.sizeToFit() // 给文字左右加一些边距 return CGSize(width: label.frame.width + 24, height: collectionView.frame.height - 10) } }
4. 最后是CollectionView的Cell
这个很简单,就是一个带UILabel的Cell,用来显示运动项目名称:
class SportItemCell: UICollectionViewCell { @IBOutlet weak var titleLabel: UILabel! override func awakeFromNib() { super.awakeFromNib() // 可以给Cell加一些圆角、阴影样式,模仿Netflix的风格 layer.cornerRadius = 8 layer.masksToBounds = true backgroundColor = .systemGray5 } }
关键注意点
- Realm List转Swift数组:Realm的
List是它自己的集合类型,不能直接当作Swift数组用,所以每次传递时要转成Array(list)。 - Cell独立管理CollectionView:每个TableViewCell自己负责内部CollectionView的数据源,这样不会出现滚动TableView时CollectionView数据串位的问题。
- Realm数据监听:如果你的Realm数据会动态更新,一定要用Realm的通知机制来自动刷新UI,避免手动刷新的麻烦。
这样一套流程下来,就能动态从Realm加载数据,给每个TableViewCell里的CollectionView传递对应的数据了,和你之前硬编码的效果一样,但完全是动态的。
内容的提问来源于stack exchange,提问作者teh_raab
相关产品推荐
相关产品推荐

