TableViewCell 内的垂直 CollectionView 工作不正常 - UIKit 和 Storyboard

问题描述 投票:0回答:1

我有: TableViewCell 内的水平 CollectionView 工作良好,符合预期

我需要什么: TableViewCell 内的垂直 CollectionView 就像 Android 版本应用程序一样

我尝试过的:

在故事板中:

  1. 将 CollectionView 流切换为垂直
  2. 将 CollectionViewCell 的估计大小设置为 None
  3. 禁用滚动
  4. 将类 DynamicHeightCollectionView 设置为我的 CollectionView
class DynamicHeightCollectionView: UICollectionView {
  
    override func layoutSubviews() {
        super.layoutSubviews()
        
        if bounds.size != intrinsicContentSize {
            self.invalidateIntrinsicContentSize()
        }
    }
    
    override var intrinsicContentSize: CGSize {
        return collectionViewLayout.collectionViewContentSize
    }
    
}

表格视图单元格

class DayEmptySeatTableViewCell: UITableViewCell, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    
    @IBOutlet weak var labelDate: UILabel!
    @IBOutlet weak var labelWeekday: UILabel!
    @IBOutlet weak var collectionViewEmptySeats: UICollectionView!
    
    var dayEmptySeat: DayEmptySeat? {
        didSet {
            setDayEmptySeat()
        }
    }
    
    var delegate: DayEmptySeatCellDelegate?
    
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        labelDate.text = ""
        labelWeekday.text = ""
        
        collectionViewEmptySeats.dataSource = self
        collectionViewEmptySeats.delegate = self
        collectionViewEmptySeats.register(UINib(nibName: "EmptySeatCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "EmptySeatCollectionViewCell")
        
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
    func setDayEmptySeat() {
        guard let dayEmptySeat else { return }
        
        labelDate.text = dayEmptySeat.date
        labelWeekday.text = dayEmptySeat.weekDay
        
        collectionViewEmptySeats.reloadData()
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 80, height: 80)
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
           return UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 15)
        }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return dayEmptySeat?.emptySeats.count ?? 0
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionViewEmptySeats.dequeueReusableCell(withReuseIdentifier: "EmptySeatCollectionViewCell", for: indexPath) as? EmptySeatCollectionViewCell else {
            return UICollectionViewCell()
        }
        guard let dayEmptySeat else {
            return UICollectionViewCell()
        }
        let emptySeat = dayEmptySeat.emptySeats[indexPath.row]
        cell.emptySeat = emptySeat
        return cell
    }
    
}

奇怪的行为是 CollectionView 的下一行传输错误。通过第一次调用 ViewController 我收到了这个

然后关闭它,第二次打电话,我收到了这个

有时第 4、7 次等调用会影响另一个 TableViewCell 中的 CollectionView。随机的。

任何想法都会有帮助。谢谢你。

swift uikit storyboard tableview collectionview
1个回答
0
投票

A

UICollectionView
旨在根据其框架布局其单元格,具有自动滚动功能,通过重用单元格来优化内存使用,并且 not 创建不可见的单元格。

尝试使用“自调整大小”集合视图通常是一场失败的战斗,因为该代码试图根据单元格布局设置框架。将它们嵌入到表视图单元格中时,您会遇到更多问题,因为在表视图设置表视图单元格的框架之前不会设置集合视图框架宽度。

提供一种不同的方法——让我们摆脱表格视图,转而使用带有自定义

UICollectionViewLayout
的集合视图。

对于自定义布局的基础知识,我们生成一个单元格框架数组,然后告诉集合视图根据需要用单元格填充这些框架。

看起来您的布局将使用固定大小的单元格框架,这使得该过程相当简单。

因此,取而代之的是在表视图的单元格中嵌入集合视图:

我们可以使用单个集合视图来做到这一点:

我在这里帮助其他人完成了类似但不同的布局任务:https://stackoverflow.com/a/75902340/6257435...并做了一些修改。

我们将从数据结构开始:

struct EmptySeatStruct {
    var date: Date = Date()
    var timeSlots: [Date] = []
}

标题视图将如下所示:

并添加一些颜色,以便我们可以看到元素框架:

非常基础的课程

UICollectionReusableView
:

class TimeSlotsSectionHeaderView: UICollectionReusableView {
    static let identifier: String = "TimeSlotsSectionHeaderView"
    
    let labelDate: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        v.font = .systemFont(ofSize: 18.0, weight: .bold)
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    let labelWeekday: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        v.font = .systemFont(ofSize: 16.0, weight: .light)
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        addSubview(labelDate)
        addSubview(labelWeekday)
        let g = self
        NSLayoutConstraint.activate([
            
            labelDate.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
            labelDate.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
            labelDate.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            
            labelWeekday.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
            labelWeekday.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            labelWeekday.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
            
        ])
    }
    // for development, so we can see the framing
    var showHeaderFrame: Bool = false {
        didSet {
            backgroundColor = showHeaderFrame ? .systemBlue : .clear
            labelDate.backgroundColor = showHeaderFrame ? .yellow : .clear
            labelWeekday.backgroundColor = showHeaderFrame ? .yellow : .clear
        }
    }
}

和单元格视图:

class TimeSlotsCell: UICollectionViewCell {
    static let identifier: String = "TimeSlotsCell"
    
    public let theLabel: UILabel = {
        let v = UILabel()
        v.textAlignment = .center
        v.textColor = .init(red: 0.0, green: 0.1, blue: 0.9, alpha: 1.0)
        v.font = .systemFont(ofSize: 16.0, weight: .regular)
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    private var shadowLayer: CAShapeLayer = {
        let thisLayer = CAShapeLayer()
        thisLayer.fillColor = UIColor.white.cgColor
        thisLayer.shadowColor = UIColor.black.cgColor
        thisLayer.shadowOffset = .zero
        thisLayer.shadowOpacity = 0.25
        thisLayer.shadowRadius = 2
        thisLayer.shouldRasterize = true
        return thisLayer
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        layer.addSublayer(shadowLayer)
        contentView.addSubview(theLabel)
        let g = contentView
        NSLayoutConstraint.activate([
            theLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            theLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
        ])
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        shadowLayer.path = UIBezierPath(roundedRect: bounds.insetBy(dx: 8.0, dy: 8.0), cornerRadius: 8.0).cgPath
        shadowLayer.shadowPath = shadowLayer.path
    }

    // if we want to change the appearance when cell is selected
    //  for example, changing the background to cyan
    //override var isSelected: Bool {
    //  didSet {
    //      shadowLayer.fillColor = isSelected ? UIColor.cyan.cgColor : UIColor.white.cgColor
    //  }
    //}
    
    // for development, so we can see the framing
    var showCellFrame: Bool = false {
        didSet {
            contentView.layer.borderColor = showCellFrame ? UIColor.blue.cgColor : UIColor.clear.cgColor
            contentView.layer.borderWidth = showCellFrame ? 1 : 0
            theLabel.backgroundColor = showCellFrame ? .green : .clear
        }
    }
}

这是自定义的

UICollectionViewLayout
类:

protocol TimeSlotsGridLayoutDelegate: AnyObject {
    func collectionView(_ collectionView: UICollectionView, heightForSectionHeaderAt section: Int) -> CGFloat
    func collectionView(_ collectionView: UICollectionView, insetsForSectionHeaderAt section: Int) -> UIEdgeInsets
    func itemSize(_ collectionView: UICollectionView) -> CGSize
    func itemSpacing(_ collectionView: UICollectionView) -> CGFloat
    func rowSpacing(_ collectionView: UICollectionView) -> CGFloat
}
// make custom delegate funcs optional
extension TimeSlotsGridLayoutDelegate {
    func collectionView(_ collectionView: UICollectionView, heightForSectionHeaderAt section: Int) -> CGFloat {
        return 0.0
    }
    func collectionView(_ collectionView: UICollectionView, insetsForSectionHeaderAt section: Int) -> UIEdgeInsets {
        return .zero
    }
    func itemSize(_ collectionView: UICollectionView) -> CGSize {
        return .init(width: 50.0, height: 50.0)
    }
    func itemSpacing(_ collectionView: UICollectionView) -> CGFloat {
        return 0.0
    }
    func rowSpacing(_ collectionView: UICollectionView) -> CGFloat {
        return 0.0
    }
}

class TimeSlotsGridLayout: UICollectionViewLayout {
    
    weak var delegate: TimeSlotsGridLayoutDelegate?
    
    // some defaults
    private let defaultItemSize: CGSize = .init(width: 50.0, height: 50.0)
    private let defaultItemSpacing: CGFloat = 0.0
    private let defaultRowSpacing: CGFloat = 0.0
    private let defaultHeaderHeight: CGFloat = 0.0
    private let defaultHeaderInsets: UIEdgeInsets = .zero
    
    // internal properties
    private var headerCache: [UICollectionViewLayoutAttributes] = []
    private var itemCache: [UICollectionViewLayoutAttributes] = []
    
    private var nextY: CGFloat = 0.0
    private var contentHeight: CGFloat = 0
    
    private var contentWidth: CGFloat {
        guard let collectionView = collectionView else {
            return 0
        }
        let insets = collectionView.contentInset
        return collectionView.bounds.width - (insets.left + insets.right)
    }
    
    override var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: contentHeight)
    }
    
    override func prepare() {
        
        guard let collectionView = collectionView else { return }
        
        let cellSize: CGSize = delegate?.itemSize(collectionView) ?? defaultItemSize
        let itemSpacing: CGFloat = delegate?.itemSpacing(collectionView) ?? defaultItemSpacing
        let rowSpacing: CGFloat = delegate?.rowSpacing(collectionView) ?? defaultRowSpacing

        let numCols: Int = Int(contentWidth / (cellSize.width + itemSpacing))
        
        var thisFrame: CGRect = .zero
        
        itemCache = []
        headerCache = []
        
        nextY = 0.0
        
        for section in 0..<collectionView.numberOfSections {
            
            let headerHeight = delegate?.collectionView(collectionView, heightForSectionHeaderAt: section) ?? defaultHeaderHeight
            let headerInsets = delegate?.collectionView(collectionView, insetsForSectionHeaderAt: section) ?? defaultHeaderInsets
            
            let attrHeader = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, with: IndexPath(row: 0, section: section))
            attrHeader.frame = CGRect(x: headerInsets.left, y: nextY + headerInsets.top, width: contentWidth - (headerInsets.left + headerInsets.right), height: headerHeight)
            nextY += headerHeight + headerInsets.top + headerInsets.bottom
            headerCache.append(attrHeader)
            
            let y: CGFloat = nextY
            
            var curCol: Int = 0
            var curRow: Int = 0
            
            let itemsInSection: Int = collectionView.numberOfItems(inSection: section)
            let rowsInSection: Int = itemsInSection / numCols
            
            // we want to know how many cells will be in each "row" in the section
            //  so we can horizontally center the rows
            var rowItemCounts: [Int] = Array(repeating: numCols, count: rowsInSection)
            if itemsInSection % numCols != 0 {
                rowItemCounts.append(itemsInSection % numCols)
            }
            
            for item in 0..<itemsInSection {
                let indexPath = IndexPath(item: item, section: section)
                
                // if we're at the last column
                if curCol == numCols {
                    // increment the row
                    curRow += 1
                    // restart at column Zero
                    curCol = 0
                }
                
                // horizontal row centering
                let interItemSpace: CGFloat = CGFloat(rowItemCounts[curRow] - 1) * itemSpacing
                let cellsWidth: CGFloat = CGFloat(rowItemCounts[curRow]) * cellSize.width
                let rowWidth: CGFloat = interItemSpace + cellsWidth
                let xOffset: CGFloat = (contentWidth - rowWidth) * 0.5
                
                let xPos: CGFloat = curCol == 0 ? 0.0 : CGFloat(curCol) * (cellSize.width + itemSpacing)
                let yPos: CGFloat = curRow == 0 ? 0.0 : CGFloat(curRow) * (cellSize.height + rowSpacing)
                
                thisFrame = .init(x: xPos + xOffset, y: y + yPos, width: cellSize.width, height: cellSize.height)

                let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
                attributes.frame = thisFrame
                itemCache.append(attributes)
                
                // increment the column
                curCol += 1
                
            }
            
            // we need to account for sections with Zero items
            //  so don't update nextY if there were no items in this section
            if collectionView.numberOfItems(inSection: section) > 0 {
                // next section starts after the bottom of
                //  primary Frame or the last secondary Frame
                //  whichever is greatest
                nextY = thisFrame.maxY
            }
            
        }
        
        contentHeight = nextY
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        super.layoutAttributesForElements(in: rect)
        
        var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
        
        for attributes in itemCache {
            if attributes.frame.intersects(rect) {
                visibleLayoutAttributes.append(attributes)
            }
        }
        
        for attributes in headerCache {
            if attributes.frame.intersects(rect) {
                visibleLayoutAttributes.append(attributes)
            }
        }
        
        return visibleLayoutAttributes
    }
    
    override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath)
        return headerCache.count > indexPath.section ? headerCache[indexPath.section] : nil
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        super.layoutAttributesForItem(at: indexPath)
        return itemCache.count > indexPath.row ? itemCache[indexPath.row] : nil
    }
    
}

最后是视图控制器:

class TimeSlotsViewController: UIViewController {

    var collectionView: UICollectionView!
    
    var myData: [EmptySeatStruct] = []
    
    // for development purposes
    var showCellFrame: Bool = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
        view.backgroundColor = .init(red: 0.95, green: 0.95, blue: 0.96, alpha: 1.0)
        let gl = TimeSlotsGridLayout()
        gl.delegate = self
        collectionView = UICollectionView(frame: .zero, collectionViewLayout: gl)
        
        collectionView.backgroundColor = view.backgroundColor
        
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(collectionView)
        
        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            collectionView.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            collectionView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
            collectionView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            collectionView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            
        ])
        
        collectionView.register(TimeSlotsCell.self, forCellWithReuseIdentifier: TimeSlotsCell.identifier)
        collectionView.register(TimeSlotsSectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: TimeSlotsSectionHeaderView.identifier)

        collectionView.dataSource = self
        collectionView.delegate = self

        // let's create some sample data, starting on current date
        //  each day will have n number of half-hour "time slots"
        //  starting at 09:00

        let countOfTimeSlots: [Int] = [6, 3, 9, 7, 8, 11, 4, 5, 3, 1, 9, 13, 9, 10, 13, 5, 5, 10, 1, 13]
        
        // this date manipulation should never fail, but in general,
        //  we should safely unwrap optionals...
        
        guard let startDate = Calendar.current.date(bySettingHour: 9, minute: 00, second: 0, of: Date())
        else { fatalError("Date failure 1 !!! (this shouldn't happen)") }
        
        for day in 0..<countOfTimeSlots.count {
            
            guard let thisDate = Calendar.current.date(byAdding: .day, value: day, to: startDate)
            else { fatalError("Date failure 2 !!! (this shouldn't happen)") }
            
            var timeSlots: [Date] = []
            
            for n in 0..<countOfTimeSlots[day] {
                
                guard let thisTime = Calendar.current.date(byAdding: .minute, value: n * 30, to: thisDate)
                else { fatalError("Date failure 3 !!! (this shouldn't happen)") }
                
                timeSlots.append(thisTime)
                
            }
            
            myData.append(EmptySeatStruct(date: thisDate, timeSlots: timeSlots))
            
        }
        
        // for use during development
        //  double-tap anywhere to toggle framing colors
        let dt = UITapGestureRecognizer(target: self, action: #selector(toggleFraming(_:)))
        dt.numberOfTapsRequired = 2
        view.addGestureRecognizer(dt)

    }
    
    // for use during development
    @objc func toggleFraming(_ sender: Any?) {
        self.showCellFrame.toggle()
        collectionView.backgroundColor = showCellFrame ? .cyan : view.backgroundColor
        self.collectionView.reloadData()
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        coordinator.animate(
            alongsideTransition: { [unowned self] _ in
                self.collectionView.collectionViewLayout.invalidateLayout()
                self.collectionView.reloadData()
            },
            completion: { [unowned self] _ in
                // if we want to do something after the size transition
            }
        )
    }
    
}

// "standard" collection view DataSource funcs
extension TimeSlotsViewController: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return myData.count
    }
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return myData[section].timeSlots.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let c = collectionView.dequeueReusableCell(withReuseIdentifier: TimeSlotsCell.identifier, for: indexPath) as! TimeSlotsCell
        
        //c.theImageView.image = myData[indexPath.section][indexPath.item]
        // any other cell data configuration
        
        let t = myData[indexPath.section].timeSlots[indexPath.item]
        
        let df = DateFormatter()
        df.dateFormat = "HH:mm"
        c.theLabel.text = df.string(from: t)
        
        // this is here only during development
        c.showCellFrame = self.showCellFrame
        
        return c
    }
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: TimeSlotsSectionHeaderView.identifier, for: indexPath) as! TimeSlotsSectionHeaderView
        
        switch kind {
        case UICollectionView.elementKindSectionHeader:
            //headerView.label.text = "Section \(indexPath.section): \(myData[indexPath.section].count) items"
            let df = DateFormatter()
            df.dateFormat = "dd MMMM"
            headerView.labelDate.text = df.string(from: myData[indexPath.section].date)
            df.dateFormat = "cccc"
            headerView.labelWeekday.text = df.string(from: myData[indexPath.section].date)
            headerView.showHeaderFrame = showCellFrame
            
        default:
            assert(false, "Unexpected element kind")
        }
        
        return headerView
    }
}

// "standard" collection view Delegate funcs
extension TimeSlotsViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("Selected item at:", indexPath)
    }
}

// our custom Layout delegate funcs
extension TimeSlotsViewController: TimeSlotsGridLayoutDelegate {
    func collectionView(_ collectionView: UICollectionView, heightForSectionHeaderAt section: Int) -> CGFloat {
        return 64.0
    }
    func collectionView(_ collectionView: UICollectionView, insetsForSectionHeaderAt section: Int) -> UIEdgeInsets {
        // let's add a little "padding" above the section header views
        //  0-points for the First one (Section: 0)
        // and 8-points between the bottom of a section
        //  and the top of the next section header
        let t: CGFloat = section == 0 ? 0.0 : 8.0
        return .init(top: t, left: 0.0, bottom: 0.0, right: 0.0)
    }
    func itemSize(_ collectionView: UICollectionView) -> CGSize {
        return .init(width: 80.0, height: 80.0)
    }
    func itemSpacing(_ collectionView: UICollectionView) -> CGFloat {
        // for this example, we'll add 2-points between items (cells)
        return 2.0
    }
    func rowSpacing(_ collectionView: UICollectionView) -> CGFloat {
        // for this example, we'll add 2-points between "cell rows"
        return 2.0
    }

}

一切都是通过代码完成的 - 没有

@IBOutlet
连接 - 只需将
TimeSlotsViewController
指定为新的
UIViewController
的自定义类。

运行时看起来像这样:

向下滚动一点:

双击任意位置即可切换“开发模式着色”,以便我们可以看到框架:

以及设备旋转时的外观:

注意:这只是示例代码!!!它并不旨在“生产就绪”。希望您会发现它很有帮助,并可以将其用作适合您特定设计需求的代码的基础...

© www.soinside.com 2019 - 2024. All rights reserved.