当前位置: > > > Swift - 实现表格tableViewCell里嵌套collectionView(附样例)

Swift - 实现表格tableViewCell里嵌套collectionView(附样例)

(本文代码已升级至Swift4)

有时要实现一个复杂的页面布局,单单使用 UITableView 实现不了,需要通过 UITableView UICollectionView 结合实现,即每个单元格 tableViewCell 中都嵌套一个 collectionView。下面通过样例演示如何实现。


1,效果图

(1)表格中每一个单元格对应一个月份的图书列表。
(2)单元格中头部显示月份标题。内部通过 collectionView 显示当月所有书籍封面图片,数量不定。整个单元格高度自适应。
(3)这个样例其实只用多 section collectionView 也能实现(点击查看)。本文使用 UITableView + UICollectionView 演示如何实现同样的功能,

2,如何实现单元格高度自适应

(1)我们要对 collectionView 设置个高度约束。当在单元格中更新 collectionView 的数据时,要获取这个 collectionView 的真实的内容高度(contentSize.height),然后用 contentSize.height 来更新 collectionView 的高度约束。这样就实现了单元格内部 collectionView 的高度自适应。
(2)而对于单元格 tableViewCell 的高度自适应,是通过 AutoLayout 特性实现的。利用内容将 cell 撑起来。
//设置estimatedRowHeight属性默认值
self.tableView!.estimatedRowHeight = 44.0
//rowHeight属性设置为UITableViewAutomaticDimension
self.tableView!.rowHeight = UITableViewAutomaticDimension

3,实现步骤

(1)新建一个自定义的 collectionView 单元格类:MyCollectionViewCell,同时勾选“Also create XIB file

(2)在 MyCollectionViewCell.xib 中添加一个 ImageView,并设置好约束。同时在对应的类中作关联

(3)MyCollectionViewCell.swift 代码如下:
import UIKit

class MyCollectionViewCell: UICollectionViewCell {

    //用于显示封面缩略图
    @IBOutlet weak var imageView: UIImageView!
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }
}

(4)新建一个自定义的 tableView 单元格类:MyTableViewCell,同时勾选“Also create XIB file

(5)在 MyTableViewCell.xib 中添加一个 Label 和一个 CollectionView,并设置好约束。同时在对应的类中作关联。
其中 Label 设置的是上、下、左、右4个约束:

CollectionView 设置的是左、右、下以及高度这个4个约束:

同时调整下 CollectionViewCell SizeMin Spacing

(6)MyTableViewCell.swift 代码如下:
import UIKit

class MyTableViewCell: UITableViewCell, UICollectionViewDelegate, UICollectionViewDataSource
{
    
    //单元格标题
    @IBOutlet weak var titleLabel: UILabel!
    
    //封面图片集合列表
    @IBOutlet weak var collectionView: UICollectionView!
    
    //collectionView的高度约束
    @IBOutlet weak var collectionViewHeight: NSLayoutConstraint!
    
    //封面数据
    var images:[String] = []
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        //设置collectionView的代理
        self.collectionView.delegate = self
        self.collectionView.dataSource = self
        
        // 注册CollectionViewCell
        self.collectionView!.register(UINib(nibName:"MyCollectionViewCell", bundle:nil),
                                      forCellWithReuseIdentifier: "myCell")
    }
    
    //加载数据
    func reloadData(title:String, images:[String]) {
        //设置标题
        self.titleLabel.text = title
        //保存图片数据
        self.images = images
        
        //collectionView重新加载数据
        self.collectionView.reloadData()
        
        //更新collectionView的高度约束
        let contentSize = self.collectionView.collectionViewLayout.collectionViewContentSize
        collectionViewHeight.constant = contentSize.height
        
        self.collectionView.collectionViewLayout.invalidateLayout()
    }
    
    //返回collectionView的单元格数量
    func collectionView(_ collectionView: UICollectionView,
                        numberOfItemsInSection section: Int) -> Int {
        return images.count
    }
    
    //返回对应的单元格
    func collectionView(_ collectionView: UICollectionView,
                        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell  = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell",
                                                for: indexPath) as! MyCollectionViewCell
        cell.imageView.image = UIImage(named: images[indexPath.item])
        return cell
    }
    
    //绘制单元格底部横线
    override func draw(_ rect: CGRect) {
        //线宽
        let lineWidth = 1 / UIScreen.main.scale
        //线偏移量
        let lineAdjustOffset = 1 / UIScreen.main.scale / 2
        //线条颜色
        let lineColor = UIColor(red: 0xe0/255, green: 0xe0/255, blue: 0xe0/255, alpha: 1)
        
        //获取绘图上下文
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }
        
        //创建一个矩形,它的所有边都内缩固定的偏移量
        let drawingRect = self.bounds.insetBy(dx: lineAdjustOffset, dy: lineAdjustOffset)
        
        //创建并设置路径
        let path = CGMutablePath()
        path.move(to: CGPoint(x: drawingRect.minX, y: drawingRect.maxY))
        path.addLine(to: CGPoint(x: drawingRect.maxX, y: drawingRect.maxY))
        
        //添加路径到图形上下文
        context.addPath(path)
        
        //设置笔触颜色
        context.setStrokeColor(lineColor.cgColor)
        //设置笔触宽度
        context.setLineWidth(lineWidth)
        
        //绘制路径
        context.strokePath()
    }
    
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
}


(7)在 StoryBoard 主视图中添加一个 TableView,并设置好约束。同时在对应的类中作关联。

(8)ViewController.swift 代码如下:
import UIKit

//每月书籍
struct BookPreview {
    var title:String
    var images:[String]
}

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    //所有书籍数据
    let books = [
        BookPreview(title: "五月新书", images: ["0.jpg", "1.jpg","2.jpg", "3.jpg",
                                                    "4.jpg","5.jpg","6.jpg"]),
        BookPreview(title: "六月新书", images: ["7.jpg", "8.jpg", "9.jpg"]),
        BookPreview(title: "七月新书", images: ["10.jpg", "11.jpg", "12.jpg", "13.jpg"])
    ]
    
    //显示内容的tableView
    @IBOutlet weak var tableView: UITableView!
    
    override func loadView() {
        super.loadView()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //设置tableView代理
        self.tableView!.delegate = self
        self.tableView!.dataSource = self
        
        //去除单元格分隔线
        self.tableView!.separatorStyle = .none
        
        //创建一个重用的单元格
        self.tableView!.register(UINib(nibName:"MyTableViewCell", bundle:nil),
                                 forCellReuseIdentifier:"myCell")
        
        //设置estimatedRowHeight属性默认值
        self.tableView!.estimatedRowHeight = 44.0
        //rowHeight属性设置为UITableViewAutomaticDimension
        self.tableView!.rowHeight = UITableViewAutomaticDimension
    }
    
    //在本例中,只有一个分区
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1;
    }
    
    //返回表格行数
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.books.count
    }
    
    
    //创建各单元显示内容(创建参数indexPath指定的单元)
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
        -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "myCell")
            as! MyTableViewCell
        
        //下面这两个语句一定要添加,否则第一屏显示的collection view尺寸,以及里面的单元格位置会不正确
        cell.frame = tableView.bounds
        cell.layoutIfNeeded()
        
        //重新加载单元格数据
        cell.reloadData(title:books[indexPath.row].title,
                        images: books[indexPath.row].images)
        return cell
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}
源码下载hangge_1591.zip

功能修改:每个单元格只显示一行封面图片

上面的样例中,每个单元格内的图片是全部显示出来,有多少显示多少,高度自适应。
我们还可以换种展示方法,每个单元格,即每个 collectionView 只显示一行数据,图片如果多的话可以通过左右滑动查看。

1,效果图

         

2,实现原理

我们只需要把 collectionView 的滚动方向改成水平方向即可。

如果不需要显示横向的滚动条,可以去掉“Shows Horizontal Indicator”的勾选。
评论10
  • 10楼
    2018-10-16 22:36
    呆呆

    我發現
    多條的情況下
    將第一個一到最後一張圖
    會有問題

    站长回复

    我又多加了几条数据测试了下,一切正常。不知你指的问题是什么。

  • 9楼
    2018-02-08 10:25
    diqqxu

    非常不错,收藏了

    站长回复

    谢谢夸奖。

  • 8楼
    2018-01-09 17:33
    小曹

    能实现一行cell中多行collectionCell,同是多的部分类似一行左右滑动显示吗?

    站长回复

    当然可以,首先按照我文章末尾内容把 collectionView 的滚动方向改成水平方向。接着把collectionView的高度调成你需要显示的行数。比如我原来是100只能显示1行,你可以改成200显示两行。

  • 7楼
    2017-12-22 13:00
    leviluo

    首先 collectionViewHeight.constant = contentSize.height 会报错,后来我修正了collectionViewHeight = NSLayoutConstraint(item: self.collectionView, attribute: NSLayoutAttribute.height, relatedBy:NSLayoutRelation.equal, toItem:nil, attribute:NSLayoutAttribute.notAnAttribute, multiplier:0.0, constant:400),运行没有报错,但是cell的大小并没有自动适应,collectionView 区域有滚动条

    站长回复

    你直接下载运行我文章里提供的项目也会报错吗?看你描述像是xib里的collectionView的高度约束没设对,或者高度约束没关联对。

  • 6楼
    2017-09-23 09:40
    诚观

    我想单点图片能打开一本书,但是自定义单元格不能用 navigation,尝试用代理,但不知道在哪里实例化单元格类,站长能解答一下吗

    站长回复

    自定义单元格里是可以获取到navigation的。具体可以参考我之前写的这篇文章:Swift - 通过UIView对象找到其所在的UIViewController

  • 5楼
    2017-09-21 12:11
    诚观

    如何实现imagview点击事件?点击一下打开对应的书

    站长回复

    CollectionView单元格点击后会调用didSelectRowAt方法,你可以在这个方法里获取到点击的indexPath。
    然后通过这个indexPath索引获取对应的书籍数据,然后调转到新页面显示详情即可。

  • 4楼
    2017-06-21 22:55
    丽水

    博主,你有oc版的吗?

    站长回复

    没有啊。

  • 3楼
    2017-04-24 20:03
    Han

    站长,我发现一个问题,这种嵌套方式下,如果选中collection中的一个cell想要触发跳转的话,只能用拖拽的方式,没法用performseguewithidentifier的方式,因为好像performsegue是controller才有的方法,但tableviewcell不能继承controller。我现在遇到的情况是一个cell可能根据不同的情况跳转到不同页面,所以拖拽肯定是不行了,一个cell不能拖拽到2个页面,但performseguewithidentifier又用不了,想问下站长有没有什么好的解决办法,谢谢!

    站长回复

    可以先获取到cell所在的controller,接着调用这个controller的performsegue方法就可以了。

    如何获取controller可以参考我之前的这篇文章:Swift - 通过UIView对象找到其所在的UIViewController

  • 2楼
    2017-04-03 21:28
    麥民

    航歌大大你好,我很喜歡你的教學,你的文章帶給我很多幫助,非常感謝!

    這邊我有個問題,如果我要在tableviewcell中,運用展開cell,裡面再嵌套一個tableview,形成點擊cell,會再展開數個cell,請問我該如實作做呢?

    這邊我的構思如下:我有兩個tableviewA跟B,A的cell包裹著B,所以我在storyboard中拉了一個tableviewA,然後新增一個xib,裡面放了另一個tableviewB,並且在裡面的datasourse中return 3個高度總共120的cell,之後在viewcontroller中A register 新增的xib(uitableviewcell),之後就是參考你的另一篇文章,“实现点击UITableView单元格时自动展开单元格”,但結果就是點擊cell毫無反應,我看報錯是說tableviewcell constrain的問題,我該如何解決呢?

    站长回复

    你好。很高兴你能喜欢我的文章。

    要在cell中嵌套tableview,其实同我之前写的cell中嵌套collectionView的实现差不多(Swift - 实现表格tableViewCell里嵌套collectionView
    不过你提到,还需要点击cell可以展开,我有些不确定最终要实现的效果。目前市面上是否有哪款App有类似的展示方式,可以告知我下。或者直接提供些截图发我邮箱。我研究下,如果能实现的话到时我会写篇相关的文章。

  • 1楼
    2017-04-01 15:50
    joy

    你好,我想问下,为什么我用同样的办法创建的cell嵌套collectionView,但是横向滚动显示的时候就有问题呀?我找不到是什么原因,弄这个的时候需要注意什么吗?

    站长回复

    最多添加约束的时候要注意下,如果仔细按我写的步骤来做,应该是没问题的。