我有: TableViewCell 内的水平 CollectionView 工作良好,符合预期
我需要什么: TableViewCell 内的垂直 CollectionView 就像 Android 版本应用程序一样
我尝试过的:
在故事板中:
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。随机的。
任何想法都会有帮助。谢谢你。
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
的自定义类。
运行时看起来像这样:
向下滚动一点:
双击任意位置即可切换“开发模式着色”,以便我们可以看到框架:
以及设备旋转时的外观:
注意:这只是示例代码!!!它并不旨在“生产就绪”。希望您会发现它很有帮助,并可以将其用作适合您特定设计需求的代码的基础...