You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 03:56:22