重用机制强的部分用UITableView或者UICollectionView

这两种的使用方法都是:

1.直接在需要显示重用部分的页面添加需要继承的协议,然后在Cell类中继承对应的协议并设置需要重用的部分的样式(如长宽等)。

  • TableView是UITableViewDelegate, UITableViewDataSource和UITableViewCell。
  • CollectionView则是和UICollectionViewCell, UICollectionViewDelegate, UICollectionViewDataSource.

2.同时要添加两个默认实现方法,一个是Cell的创建和设置,另一个是需要重用的次数。

CollectionView其实就是竖着自动排列View

以排列Label标签为例,创建一个Cell,排列标签。

先设置一下ContentViewCell

class CustomCell: UICollectionViewCell {
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    lazy var titleLabel: UILabel = {
        let titleLabel = UILabel(frame: .init(x: 0, y: 0, width: frame.width, height: frame.height))
        titleLabel.textAlignment = .center
        //等会改一下width试试会发生什么?
        return titleLabel
    }()
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

与TableView不同的是

  1. CollectionView需要在ViewController页面设置CollectionView的实例collectionView,也就是UICollectionView的矩形区域,包括宽高和大小。同样也要设置该参数的delegate代理和dataSource为自己
  2. CollectionView是需要注册的,注册就是.register
  3. 最后记得要把设置好的collectionView添加
  4. 在默认实现中创建cell
override func viewDidLoad() {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: ((self.view.frame.width - 32 - 10 * 2) / 3), height: ((self.view.frame.width - 32 - 10 * 2) / 3))
        let collectionView = UICollectionView(
            frame: CGRect(x: 16, y: 64, width: self.view.frame.width - 32, height: self.view.frame.height - 64),
            collectionViewLayout: layout
        )
       
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.alwaysBounceVertical = true
        self.view.addSubview(collectionView)
        super.viewDidLoad()
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ImageCell
        cell.imageView.image = UIImage(named: img_num[indexPath.row])
        return cell
    }

注意:Cell的大小在.itemSize里设置

用indexPath表示排列的Cell下标

单个cell的大小在CollectionView.itemSize里设置,和直接在对应的Cell内部设置有什么不同?
特性 在 itemSize 中设置 在 Cell 内部自适应设置
主导方 外部的 Layout(布局管理器) 内部的 Auto Layout 约束/内容
尺寸确定时机 列表渲染就已确定 列表渲染动态计算确定
所有 Cell 大小 默认全部一样(除非用代理单独改) 错落有致,每个都可以不一样
滚动性能 极高(顺畅丝滑) 中等(如果约束复杂,可能会微卡顿)

一般都是在Layout的itemSize中设置的,如果是瀑布流,则是在Layout里动态计算的。

练习一:模仿照片相册

用系统默认的Layout

这里创建collectionView和layout,指定layout为UICollectionViewFlowLayout

    override func viewDidLoad() {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: ((self.view.frame.width - 32 - 10 * 2) / 3), height: ((self.view.frame.width - 32 - 10 * 2) / 3))
        let collectionView = UICollectionView(
            frame: CGRect(x: 16, y: 64, width: self.view.frame.width - 32, height: self.view.frame.height - 64),
            collectionViewLayout: layout
        )
       
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.alwaysBounceVertical = true
        self.view.addSubview(collectionView)
        super.viewDidLoad()

图片就用资源库里的了

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
    let img_num: [String] = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"]
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        img_num.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ImageCell
        cell.imageView.image = UIImage(named: img_num[indexPath.row])
        return cell
    }

练习二:模仿小红书主页瀑布信息流

主要是三部分:

1.存放需要的View的UICollectionViewCell

2.自定义的Layout

3.继承了UICollectionDelegate和UICollectionDataSource的ViewController

1.存放需要的View的UICollectionViewCell

反正能确定下来的就先确定,不能确定的后面用第三方库SnapKit来调整位置,SnapKit是基于Swift的库,效果就跟SwiftUI差不多是相对位置的,对于位置不确定的或者会随着文本长度变化而变化的

lazy var title_label: UILabel = {
        let title_label = UILabel(frame: .init(x: 16, y: 0, width: frame.width - 16, height: 20))
        title_label.textColor = .black
        title_label.numberOfLines = 2
        title_label.font = .systemFont(ofSize: 12)
        return title_label
    }()
    
    lazy var head_img: UIImageView = {
        let head_img = UIImageView(frame: .init(x: 16, y: 0, width: 16, height: 16))
        head_img.layer.cornerRadius = 8
        head_img.layer.masksToBounds = true
        return head_img
    }()
    
    lazy var userNameLabel: UILabel = {
        let userNameLabel = UILabel(frame: .init(x: 16 + 16 + 4, y: 0, width: frame.width - CGFloat(16 + 16 + 4 + 50 + 16), height: 16))
        userNameLabel.textColor = .gray
        userNameLabel.font = .systemFont(ofSize: 10)
        return userNameLabel
    }()

然后用SnapKit来确定组件之间的相对位置

SnapKit的用法,同样要把需要的视图在初始化时候添加进去,然后再在下面用.snp.makeConstraints { make in …}来编辑这个组件的位置,大小等内容。

举例:

make.right.equalToSuperview().offset(-16)

就是把这个视图的右边缘,距离父视图的距离定为x(一个整数),后面的数字是正数就代表向右偏移(跟x一样),是负数就代表向左偏移。

make.bottom.equalToSuperview().offset(-8)

就是把这个视图的下边缘,距离父视图的距离定为y(一个整数),后面的数字逻辑和y一样,正就是往下,负就是往上。

make.top.equalTo(title_label.snp.bottom).offset(8)

把当前视图的上边缘和另一个视图(title_label.snp.bottom )拉近,并设置它们的距离为8.这个也是一样的正负规则,自己玩一下就知道了

这个也可以用来控制最大最小的宽度和高度。

override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(content_img)
        addSubview(title_label)
        addSubview(head_img)
        addSubview(userNameLabel)
        addSubview(head_img)
        addSubview(likes_label)
        addSubview(heart_img)
        //内容图
        content_img.snp.makeConstraints {
            make in
            make.top.left.right.equalToSuperview()
            make.bottom.equalTo(title_label.snp.top).offset(-8)
        }
//        头像。。。
        head_img.snp.makeConstraints {
                make in
                make.left.equalToSuperview().offset(8)
                make.bottom.equalToSuperview().offset(-8)
                make.width.height.equalTo(16)
            }
        title_label.snp.makeConstraints { make in
            make.left.equalToSuperview().offset(8)
            make.right.equalToSuperview().offset(-16)
            make.top.equalTo(content_img.snp.bottom).offset(8)
            make.bottom.equalTo(head_img.snp.top).offset(-8)
            make.height.lessThanOrEqualTo(32)
        }
        
        userNameLabel.snp.makeConstraints { make in
            make.left.equalTo(head_img.snp.right).offset(4)
            make.bottom.equalToSuperview().offset(-10)
            make.width.lessThanOrEqualTo(frame.width - (8 + 16 + 36 + 8 + 16 + 8))
        }
        likes_label.snp.makeConstraints { make in
            make.right.equalToSuperview().offset(-8)
            make.top.equalTo(title_label.snp.bottom).offset(8)
            make.width.lessThanOrEqualTo(36)

        }
        heart_img.snp.makeConstraints { make in
            make.right.equalTo(likes_label.snp.left).offset(-2)
            make.top.equalTo(title_label.snp.bottom).offset(8)
            make.width.height.equalTo(16)
        }
        
        }

2.自定义的Layout

这个部分就是信息流用的Layout,用高度来确定Cell位置的

//
//  WaterflowLayout.swift
//  CollectionView_homework2
//
//  Created by sakiko on 2026/6/3.
//

import UIKit

// 定义一个协议,让代理(通常是 ViewController)提供图片或内容的高度
protocol WaterflowLayoutDelegate: AnyObject {
    func waterflowLayout(_ layout: WaterflowLayout, heightForItemAt indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat
}

class WaterflowLayout: UICollectionViewLayout {
    
    weak var delegate: WaterflowLayoutDelegate?
    
    // 配置参数
    var columnCount: Int = 2 // 列数
    var columnMargin: CGFloat = 10 // 列间距
    var rowMargin: CGFloat = 10 // 行间距
    var edgeInsets: UIEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    
    // 缓存布局属性
    private var attrsArray: [UICollectionViewLayoutAttributes] = []
    // 记录每列当前的高度
    private var columnHeights: [CGFloat] = []
    
    // 1. 准备布局,计算每个 item 的 frame
    override func prepare() {
        super.prepare()
        // 清除之前的缓存
        attrsArray.removeAll()
        columnHeights = Array(repeating: edgeInsets.top, count: columnCount)
        
        guard let collectionView = collectionView else { return }
        let itemCount = collectionView.numberOfItems(inSection: 0)
        
        // 计算 item 的宽度
        let totalWidth = collectionView.frame.size.width
        let availableWidth = totalWidth - edgeInsets.left - edgeInsets.right - CGFloat(columnCount - 1) * columnMargin
        let itemWidth = availableWidth / CGFloat(columnCount)
        
        // 循环计算每个 item 的位置
        for i in 0..<itemCount {
            let indexPath = IndexPath(item: i, section: 0)
            let attrs = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            
            // 找出当前最短的那一列
            var minColumnIndex = 0
            var minColumnHeight = columnHeights[0]
            for (index, height) in columnHeights.enumerated() {
                if height < minColumnHeight {
                    minColumnHeight = height
                    minColumnIndex = index
                }
            }
            
            // 计算 X 和 Y
            let x = edgeInsets.left + CGFloat(minColumnIndex) * (itemWidth + columnMargin)
            let y = minColumnHeight
            
            // 从代理获取高度
            let itemHeight = delegate?.waterflowLayout(self, heightForItemAt: indexPath, itemWidth: itemWidth) ?? 100
            
            // 设置 frame
            attrs.frame = CGRect(x: x, y: y, width: itemWidth, height: itemHeight)
            attrsArray.append(attrs)
            
            // 更新当前列的高度
            columnHeights[minColumnIndex] = CGRect(x: x, y: y, width: itemWidth, height: itemHeight).maxY + rowMargin
        }
    }
    
    // 2. 返回可见区域内的布局属性
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return attrsArray.filter { $0.frame.intersects(rect) }
    }
    
    // 3. 返回特定 indexPath 的布局属性
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return attrsArray[indexPath.item]
    }
    
    // 4. 返回滚动范围
    override var collectionViewContentSize: CGSize {
        let maxColumnHeight = columnHeights.max() ?? 0
        return CGSize(width: collectionView?.frame.size.width ?? 0, height: maxColumnHeight + edgeInsets.bottom - rowMargin)
    }
}

3.继承了UICollectionDelegate和UICollectionDataSource的ViewController

在创建Cell的地方给这些会变的内容,就比如图片,文案之类的渲染上。

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! InfoCell
        cell.content_img.image = UIImage(named: contentImg[indexPath.row])
        cell.head_img.image = UIImage(named: headImg[indexPath.row])
        cell.likes_label.text = likesCount[indexPath.row]
        cell.title_label.text = titleName[indexPath.row]
        cell.userNameLabel.text = userName[indexPath.row]
        return cell
    }

然后把collectionView的Layout设置为刚刚写好的WaterFlowLayout,并设置列数

override func viewDidLoad() {
        super.viewDidLoad()
        let layout = WaterflowLayout()
        layout.delegate = self
        layout.columnCount = 2
        let collectionView = UICollectionView(frame: .init(x: 16, y: 64, width: self.view.frame.width - 32, height: self.view.frame.height - 64), collectionViewLayout: layout)
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(InfoCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.showsVerticalScrollIndicator = false
        self.view.addSubview(collectionView)
        // Do any additional setup after loading the view.
    }

信息流需要用到Cell高度,这个高度得手动算出来,这里是本地图片。如果是网络请求,会有图片的尺寸,就更加方便。

那就先用一个函数,根据图片算出高度(因为信息流的cell图片宽度是固定的,而高度是变化的)就都当做参数传进去。

func getNewHeight(imgName: String, fixed_width: CGFloat) -> CGFloat {
        let img = UIImage(named: imgName)
        if img == nil {
            return fixed_width
        } else {
            let o_width = img?.size.width ?? 0
            let o_height = img?.size.height ?? 0
            let newheight =  (o_height / o_width) * fixed_width
            return newheight
        }
    }

然后再在这个基础上,把高度和下面留空的地方加上,看个效果例如底部留空的位置为100

func waterflowLayout(_ layout: WaterflowLayout, heightForItemAt indexPath: IndexPath, itemWidth: CGFloat) -> CGFloat {
        let content = contentImg[indexPath.item]
        let imgHeight = getNewHeight(imgName: content, fixed_width: itemWidth)
//        let bottomGap: CGFloat = 16.0 + 8.0 + 8.0 + 20.0 + 8.0
        //下面的间隔是16(头像高), 8(底部距离), 8(头像和标题距离),20(标签高度) 8(标题和内容图片距离)
//        先固定100,搞一块大地方出来
//        16 + 8
        return imgHeight + 100.0
    }

在这里插入图片描述
在这里插入图片描述

更多推荐