I have an ViewController with a NSCollectionView with Headers and Items. At a first glance everything looks fine:
As soon as I resize the window, one header becomes the size of an item – and the other headers disappear:
This is my code:
extension ViewController:NSCollectionViewDataSource {
static let picItem = "PictureItem"
static let headerItem = "HeaderItem"
func numberOfSections(in collectionView: NSCollectionView) -> Int {
return 3
}
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return 15
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let itemView = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: ViewController.picItem), for: indexPath)
return itemView
}
func collectionView(_ collectionView: NSCollectionView, viewForSupplementaryElementOfKind kind: NSCollectionView.SupplementaryElementKind, at indexPath: IndexPath) -> NSView {
let headerView = collectionView.makeSupplementaryView(ofKind: kind, withIdentifier: NSUserInterfaceItemIdentifier(rawValue: ViewController.headerItem), for: indexPath)
return headerView
}
}
extension ViewController:NSCollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> NSSize {
return NSSize(width: 0, height: 20)
}
}
The storyboard:
What's going on here? What did I miss?
Additional Code
HeaderItem.swift
class HeaderItem: NSCollectionViewItem {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
view.wantsLayer = true
view.layer?.backgroundColor = NSColor.green.cgColor
}
}
HeaderItem.xib
The outlet view of the HeaderItem object is the CustomView.
I think the best way is follow Apple guidelines for the new approach . First create UICollectionView Layout where specify all geometry params for header, cells and footers, for example:
import UIKit
import Combine
import Resolver
class DeliveryController: UIViewController {
var dataSource: UICollectionViewDiffableDataSource<Order, OrderItem>! = nil
func createLayout() -> UICollectionViewLayout {
let itemWidth: CGFloat = 140.0
let itemHeight: CGFloat = 140.0
let headerHeight: CGFloat = 140.0
let footerHeight: CGFloat = 68.0
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 10.0
let layout = UICollectionViewCompositionalLayout(sectionProvider: {
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let order = self.orders[sectionIndex]
let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
widthDimension: .absolute(itemWidth), heightDimension: .absolute(itemHeight)))
item.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 10)
let containerGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(itemWidth), heightDimension: .estimated(itemHeight)), subitems: [item])
let section = NSCollectionLayoutSection(group: containerGroup)
section.orthogonalScrollingBehavior = UICollectionLayoutSectionOrthogonalScrollingBehavior.continuous
let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(headerHeight)),
elementKind: DeliveryController.sectionHeaderElementKind,
alignment: .top)
let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(footerHeight)),
elementKind: DeliveryController.sectionFooterElementKind,
alignment: .bottom)
if order.hasDriver {
section.boundarySupplementaryItems = [sectionHeader, sectionFooter]
} else {
section.boundarySupplementaryItems = [sectionHeader]
}
return section
}, configuration: config)
return layout
}
Then configureHierarchy using given layout:
func configureHierarchy(in view: UIView, top: UIView, bottom: UIView) {
collectionView = UICollectionView(frame: .zero /* create with zero frame and use constarins after we adding collectionView as subview */, collectionViewLayout: createLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .white
view.addSubview(collectionView)
collectionView.anchor(top: top.bottomAnchor, left: view.leftAnchor, bottom: bottom.topAnchor, right: view.rightAnchor, paddingTop: 8, paddingLeft: 0, paddingBottom: 2, paddingRight: 0)
collectionView.delegate = self
}
And then DataSource:
func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<OrderItemCell, OrderItem> { (cell, indexPath, item) in
cell.item = item
}
dataSource = UICollectionViewDiffableDataSource<Order, OrderItem>(collectionView: collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, item: OrderItem) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
let headerRegistration = UICollectionView.SupplementaryRegistration
<OrderHeaderView>(elementKind: "Header") {
(supplementaryView, string, indexPath) in
let order = self.orders[indexPath.section]
supplementaryView.order = order
}
let footerRegistration = UICollectionView.SupplementaryRegistration
<OrderFooterView>(elementKind: "Footer") {
(supplementaryView, string, indexPath) in
let order = self.orders[indexPath.section]
supplementaryView.order = order
supplementaryView.delegate = self
}
dataSource.supplementaryViewProvider = { (view, kind, index) in
let order = self.orders[index.section]
if kind == DeliveryController.sectionHeaderElementKind {
return self.collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: index)
} else if order.hasDriver {
return self.collectionView.dequeueConfiguredReusableSupplementary(using: footerRegistration, for: index)
}
return nil
}
var snapshot = NSDiffableDataSourceSnapshot<Order, OrderItem>()
orders.forEach { order in
snapshot.appendSections([order])
snapshot.appendItems(order.items, toSection: order)
}
dataSource.apply(snapshot, animatingDifferences: false)
}
}
Related
I'm currently trying to achieve the following layout using NSCollectionLayoutSection. Do you have any advice on only making the first item 50px wide while keeping the rest of the items 100px (could be any number of items)? The solution has to be an NSCollectionLayoutSection.
I'm currently displaying them in the same width using which is not the desired result:
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)))
item.contentInsets = NSDirectionalEdgeInsets(top: 0,
leading: 0,
bottom: 0,
trailing: 8)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(100),
heightDimension: .absolute(100)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 16,
leading: 16,
bottom: 16,
trailing: 16)
section.orthogonalScrollingBehavior = .continuous
I've also tried using absolute widths but didn't have much luck with that approach.
Thank you!
You can achieve this by creating two separate item groups (one for the first cell which has a half width and second for the rest of the cells). Then combine those two groups and add it to the section.
class ViewController: UIViewController {
#IBOutlet var collectionView: UICollectionView!
let headerId = "headerId"
let categoryHeaderId = "categoryHeaderId"
let colorarray = [UIColor.yellow, UIColor.green, UIColor.green, UIColor.green, UIColor.green, UIColor.green, UIColor.green, UIColor.green]
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "My Albums"
collectionView.register(AlbumItemCell.self, forCellWithReuseIdentifier: AlbumItemCell.reuseIdentifer)
collectionView.register(CategoryHeaderView.self, forSupplementaryViewOfKind: categoryHeaderId, withReuseIdentifier: headerId)
collectionView.collectionViewLayout = createCompositionalLayout()
collectionView.reloadData()
}
private func createCompositionalLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout { (sectionNumber, env) -> NSCollectionLayoutSection? in
self.firstLayoutSection()
}
}
private func firstLayoutSection() -> NSCollectionLayoutSection {
let cellWidth : CGFloat = 200
let cellHeight : CGFloat = 200
let smallItemSize = NSCollectionLayoutSize(widthDimension: .absolute(cellWidth/2), heightDimension: .absolute(cellHeight))
let smallItem = NSCollectionLayoutItem(layoutSize: smallItemSize)
let smallGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(cellWidth/2), heightDimension: .absolute(cellHeight))
let smallGroup = NSCollectionLayoutGroup.horizontal(layoutSize: smallGroupSize, subitem: smallItem, count: 1)
smallGroup.contentInsets = .init(top: 0, leading: 8, bottom: 0, trailing: 8)
let largeItemSize = NSCollectionLayoutSize(widthDimension: .absolute(cellWidth), heightDimension: .absolute(cellHeight))
let largeItem = NSCollectionLayoutItem(layoutSize: largeItemSize)
largeItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8)
let largeGroupSize = NSCollectionLayoutSize(widthDimension: .absolute(CGFloat((cellWidth + 8) * CGFloat((colorarray.count - 1)))), heightDimension: .absolute(cellHeight))
let largeGroup = NSCollectionLayoutGroup.horizontal(layoutSize: largeGroupSize, subitem: largeItem, count: colorarray.count - 1 )
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(cellWidth * CGFloat(colorarray.count)), heightDimension: .absolute(cellHeight))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [smallGroup, largeGroup])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .groupPaging
return section
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
colorarray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: AlbumItemCell.reuseIdentifer, for: indexPath) as! AlbumItemCell
cell.configure(color: colorarray[indexPath.row])
return cell
}
}
class AlbumItemCell: UICollectionViewCell {
static let reuseIdentifer = "album-item-cell-reuse-identifier"
let view = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(color: UIColor) {
view.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(view)
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
view.topAnchor.constraint(equalTo: contentView.topAnchor),
view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
view.backgroundColor = color
}
}
Result
I want to build the following layout using UIKit.
Currently I'm using an UICollectionView in combination with a Composotional Layout. The following code produces this result:
Relevant Method:
private static func createCompositionalLayout() -> UICollectionViewCompositionalLayout {
let headerItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(35)
)
)
let horizontalGroupItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.95),
heightDimension: .fractionalHeight(1.0)
)
)
let horizontalMainGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(100)
),
subitems: [horizontalGroupItem]
)
horizontalMainGroup.interItemSpacing = .fixed(10)
let group = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(135)
),
subitems: [headerItem, horizontalMainGroup])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 10
section.orthogonalScrollingBehavior = .continuous
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
So the main problem is, that the top element doesn't take up the full width of the section. Instead it's limited to the width of group. Does anyone know how to get the desired result? :-)
Full Example:
import UIKit
class ViewController: UIViewController {
private let colors: [UIColor] = [.systemTeal, .systemBlue, .systemGray, .systemOrange]
private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: ViewController.createCompositionalLayout())
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.backgroundColor = .systemBackground
collectionView.frame = view.bounds
collectionView.dataSource = self
collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: MyCollectionViewCell.reuseID)
}
private static func createCompositionalLayout() -> UICollectionViewCompositionalLayout {
let headerItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(35)
)
)
let horizontalGroupItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.95),
heightDimension: .fractionalHeight(1.0)
)
)
let horizontalMainGroup = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(100)
),
subitems: [horizontalGroupItem]
)
horizontalMainGroup.interItemSpacing = .fixed(10)
let group = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(135)
),
subitems: [headerItem, horizontalMainGroup])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 10
section.orthogonalScrollingBehavior = .continuous
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCollectionViewCell.reuseID, for: indexPath) as? MyCollectionViewCell
cell?.backgroundColor = colors[indexPath.item]
cell?.label.text = "\(indexPath.item)"
return cell ?? UICollectionViewCell()
}
}
//MARK: - CollectionViewCell
class MyCollectionViewCell: UICollectionViewCell {
static let reuseID = "myCell"
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(label)
label.frame = contentView.bounds
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Currently you add HeaderItem to the group, that's why it is limited to the width of the group.
You could achieve the desired effect with a boundarySupplementaryItems for a NSCollectionLayoutSection.
To do it you need to update your HeaderItem:
let headerItem = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(35)
),
elementKind: "section-header",
alignment: .top
)
Add it to layout section:
section.boundarySupplementaryItems = [headerItem]
Register it:
collectionView.register(HeaderRV.self, forSupplementaryViewOfKind: "section-header", withReuseIdentifier: "HeaderRV")
Create HeaderRV class:
class HeaderRV: UICollectionReusableView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(label)
label.frame = self.bounds
}
required init?(coder: NSCoder) {
fatalError("Not happening")
}
}
Finally you need to add collectionView(_:viewForSupplementaryElementOfKind:at:) in your UICollectionViewDataSource:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderRV", for: indexPath) as! HeaderRV
header.backgroundColor = .red
header.label.text = "\(indexPath.item)"
return header
}
Full code:
import UIKit
class ViewController: UIViewController {
private let colors: [UIColor] = [.systemTeal, .systemBlue, .systemGray, .systemOrange]
private let collectionView = UICollectionView(frame: .zero, collectionViewLayout: ViewController.createCompositionalLayout())
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.backgroundColor = .systemBackground
collectionView.frame = CGRect(x: 5, y: 0, width: view.bounds.width - 10, height: view.bounds.height)
collectionView.dataSource = self
collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: MyCollectionViewCell.reuseID)
collectionView.register(HeaderRV.self, forSupplementaryViewOfKind: "section-header", withReuseIdentifier: "HeaderRV")
}
private static func createCompositionalLayout() -> UICollectionViewCompositionalLayout {
let headerItem = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(50)
),
elementKind: "section-header",
alignment: .top
)
headerItem.contentInsets.bottom = 5
let horizontalGroupItem = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0)
)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(0.65),
heightDimension: .absolute(135)
),
subitems: [horizontalGroupItem])
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 50
section.boundarySupplementaryItems = [headerItem]
section.orthogonalScrollingBehavior = .continuous
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderRV", for: indexPath) as! HeaderRV
header.backgroundColor = .red
header.label.text = "\(indexPath.item)"
return header
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCollectionViewCell.reuseID, for: indexPath) as? MyCollectionViewCell
cell?.backgroundColor = colors[indexPath.item]
cell?.label.text = "\(indexPath.item)"
return cell ?? UICollectionViewCell()
}
}
//MARK: - CollectionViewCell
class MyCollectionViewCell: UICollectionViewCell {
static let reuseID = "myCell"
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(label)
label.frame = contentView.bounds
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//MARK: - CollectionReusableView
class HeaderRV: UICollectionReusableView {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(label)
label.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height - 5)
}
required init?(coder: NSCoder) { fatalError("Not happening") }
}
I have a CollectionView where all of my cells are aligned to the center of the view, utilising a custom flow layout. Here is the code for the collectionViewController:
let columnLayout = FlowLayout(
minimumInteritemSpacing: 10,
minimumLineSpacing: 10,
sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
)
var dataSource: [String] = ["USA", "Brazil", "China","Really long long long", "Unite Kingdom", "Japan", "Mexico", "India", "Really long long", "Short", "USA", "Brazil", "China","Really long long long", "Unite Kingdom", "Japan", "Mexico", "India", "Really long long"]
override func viewDidLoad() {
super.viewDidLoad()
collectionView.collectionViewLayout = columnLayout
collectionView.contentInsetAdjustmentBehavior = .always
collectionView.register(TestCell.self, forCellWithReuseIdentifier: "Cell")
collectionView.dragInteractionEnabled = true
collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.delegate = self
collectionView.dataSource = self
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// dataArary is the managing array for your UICollectionView.
let item: String = dataSource[indexPath.row]
return CGSize(width: item.widthOfString(usingFont: .systemFont(ofSize: 16)) + 25, height: 40)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? TestCell
cell?.textLabel.text = dataSource[indexPath.row]
return cell!
}
fileprivate func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
if let item = coordinator.items.first,
let sourceIndexPath = item.sourceIndexPath {
collectionView.performBatchUpdates ({
self.dataSource.remove(at: sourceIndexPath.item)
self.dataSource.insert(item.dragItem.localObject as! String, at: destinationIndexPath.item)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
}, completion: nil)
collectionView.reloadData()
coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
}
Code for the FlowLayout Class:
class FlowLayout: UICollectionViewFlowLayout {
required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
super.init()
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
sectionInsetReference = .fromSafeArea
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
guard scrollDirection == .vertical else { return layoutAttributes }
// Filter attributes to compute only cell attributes
let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
// Group cell attributes by row (cells with same vertical center) and loop on those groups
for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
// Get the total width of the cells on the same row
let cellsTotalWidth = attributes.reduce(CGFloat(0)) { (partialWidth, attribute) -> CGFloat in
partialWidth + attribute.size.width
}
// Calculate the initial left inset
let totalInset = collectionView!.safeAreaLayoutGuide.layoutFrame.width - cellsTotalWidth - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(attributes.count - 1)
var leftInset = (totalInset / 2 * 10).rounded(.down) / 10 + sectionInset.left
// Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
for attribute in attributes {
attribute.frame.origin.x = leftInset
leftInset = attribute.frame.maxX + minimumInteritemSpacing
}
}
return layoutAttributes
}
}
I then added a UICollectionViewDragDelegate and a UICollectionViewDropDelegate extensions in order to enable the user to reorder the cells. The two extensions:
extension CollectionViewController: UICollectionViewDragDelegate {
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let item = self.dataSource[indexPath.row]
let itemProvider = NSItemProvider(object: item as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
}
extension CollectionViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
if collectionView.hasActiveDrag {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
return UICollectionViewDropProposal(operation: .forbidden)
}
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
var destinationindexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationindexPath = indexPath
} else {
let row = collectionView.numberOfItems(inSection: 0)
destinationindexPath = IndexPath(item: row - 1, section: 0)
}
if coordinator.proposal.operation == .move {
self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationindexPath, collectionView: collectionView)
}
}
}
For some strange reason the cells glitch out in the app when reordering them:
https://imgur.com/dmEQLqh
Thanks for the help
I'm facing a problem with my collection view. I'm trying to pick the indexPath of an item to show more info on it after the tap, but I can't get it and figure out why.
I tried to allows collection View selection, but nothing happened.
I also tried to add a tap gesture on the cell, but don't know how to get the indexPath from there.
I add the same code on another VC, where the collection view is on the entire screen, and it works ( here, the collection View is half the screen ).
Here is the (updated) code for the collection View :
class SearchRunnerVC: UIViewController {
//MARK: - Objects
var lastNameTextField = RSTextField(placeholder: "Nom du coureur", returnKeyType: .next,
keyboardType: .default)
var firstNameTextField = RSTextField(placeholder: "Prénom", returnKeyType: .next,
keyboardType: .default)
var numberTextField = RSTextField(placeholder: "Numéro de dossard", returnKeyType: .next,
keyboardType: .numberPad)
var raceTextField = RSTextField(placeholder: "Course", returnKeyType: .done,
keyboardType: .default)
var searchButton = RSButton(backgroundColor: .systemPink, title: "Rechercher")
var collectionView : UICollectionView! = nil
var emptyLabel = UILabel()
let hud = JGProgressHUD(style: .dark)
lazy var textFields = [lastNameTextField, firstNameTextField, numberTextField, raceTextField]
//MARK: - Properties
let padding = CGFloat(20)
let runnersCollection = Firestore.firestore().collection("Runner")
var runners = [Runner]()
//MARK: - Lifecycle Methods
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.prefersLargeTitles = true
title = "Rechercher"
}
}
extension SearchRunnerVC {
fileprivate func setupUI() {
configureSuperView()
configureTextFields()
configureCollectionView()
configureButton()
configureConstraints()
}
fileprivate func configureSuperView() {
view.backgroundColor = .systemBackground
let viewTap = UITapGestureRecognizer(target: self, action: #selector(hideKeyboard(_:)))
view.addGestureRecognizer(viewTap)
}
fileprivate func configureConstraints() {
for textField in textFields {
view.addSubview(textField)
textField.leftToSuperview(view.leftAnchor, offset: padding)
textField.rightToSuperview(view.rightAnchor, offset: -padding)
textField.height(50)
textField.delegate = self
}
firstNameTextField.topToSuperview(view.safeAreaLayoutGuide.topAnchor, offset: padding, usingSafeArea: true)
lastNameTextField.topToBottom(of: firstNameTextField, offset: padding)
numberTextField.topToBottom(of: lastNameTextField, offset: padding)
raceTextField.topToBottom(of: numberTextField, offset: padding)
searchButton.topToBottom(of: raceTextField, offset: padding)
searchButton.leftToSuperview(view.leftAnchor, offset: padding)
searchButton.rightToSuperview(view.rightAnchor, offset: -padding)
searchButton.height(50)
collectionView.topToBottom(of: searchButton, offset: padding)
collectionView.edgesToSuperview(excluding: .top)
}
fileprivate func configureTextFields() {
firstNameTextField.tag = 0
lastNameTextField.tag = 1
numberTextField.tag = 2
raceTextField.tag = 3
}
fileprivate func configureButton() {
view.addSubview(searchButton)
searchButton.addTarget(self, action: #selector(searchRunners), for: .touchUpInside)
}
fileprivate func createLayout() -> UICollectionViewLayout {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .estimated(100))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)
let spacing = CGFloat(10)
group.interItemSpacing = .fixed(spacing)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = spacing
let padding = CGFloat(10)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: padding, bottom: 0, trailing: padding)
let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(80))
let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
section.boundarySupplementaryItems = [header]
let layout = UICollectionViewCompositionalLayout(section: section)
return layout
}
fileprivate func configureCollectionView() {
collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
collectionView.autoresizingMask = [.flexibleHeight]
collectionView.backgroundColor = .systemBackground
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(RunnerCell.self, forCellWithReuseIdentifier: RunnerCell.reuseID)
collectionView.register(Header.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: Header.reuseID)
view.addSubview(collectionView)
}
}
extension SearchRunnerVC : UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return runners.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RunnerCell.reuseID, for: indexPath) as? RunnerCell else { fatalError("Unable to dequeue runner cell")}
cell.configure(runner: runners[indexPath.row])
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: Header.reuseID, for: indexPath) as? Header else { fatalError("Unable to dequeue header")}
header.title.text = "Résultats"
header.seeButton.addTarget(self, action: #selector(seeAllTapped), for: .touchUpInside)
header.separator.alpha = 0
header.backgroundColor = collectionView.backgroundColor
return header
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath.item)
}
}
Seems like your problem relates to the UITapGestureRecognizer you are adding to your view in configureSuperView(). The touches that would usually trigger the collectionView didSelectItemAt... delegate function are being sent to the gesture recognizer's handler, which is hideKeyboard().
Comment the line with view.addGestureRecognizer(viewTap) and your code will work. If so, I can also help you with achieving the hideKeyboard functionality, just let me know.
Do not use tap gesture on cells because
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
does excatly what you expect. The problem is probably in your collectionView configuration/setup. Could you show the whole ViewController code?
I am refactoring my ViewControllers and one of them contains a collectionView but now the DataSource is not getting called anymore.
My ViewController:
class CoinPageVC: UIViewController, DependencyInjectionVC, Storyboarded {
lazy var mainView: CoinPageV = {
let v = CoinPageV()
v.collectionView.delegate = self
return v
}()
var coin: Coin!
var selectedBase: String!
var viewContainer: [UIView]!
var collectionViewViewDataSource: CollectionViewCoinPageDatasource?
func injectDependencys(dependency: CoinPageDependency) {
self.coin = dependency.coin
self.selectedBase = dependency.base
self.viewContainer = dependency.views
}
override func viewDidLoad() {
super.viewDidLoad()
self.collectionViewViewDataSource = CollectionViewCoinPageDatasource(data: viewContainer)
self.mainView.collectionView.dataSource = self.collectionViewViewDataSource
self.mainView.collectionView.reloadData()
}
}
extension CoinPageVC: SetMainView {}
extension CoinPageVC: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width:CGFloat = collectionView.bounds.width
let height:CGFloat = collectionView.bounds.height
// - (tabBarHeight + menuBar.frame.height + heightNavigationBarTop)
let output = Utility.shared.CGSizeMake(width, height)
return output
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let index = Int(targetContentOffset.pointee.x / view.frame.width)
let indexPath = IndexPath(item: index, section: 0)
mainView.menuBar.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
}
}
My Datasource class:
class CollectionViewCoinPageDatasource: NSObject, UICollectionViewDataSource {
let data: [UIView]
init(data: [UIView]){
self.data = data
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
return data.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//let outputCell: UICollectionViewCell
let row = indexPath.item
let outputCell = collectionView.dequeueReusableCell(withReuseIdentifier: Identifier.coinPageCollectionViewOverviewCell.rawValue, for:indexPath) as! CollectionViewCellView
outputCell.view = data[row]
return outputCell
}
}
My collectionView setup:
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let frame = CGRect(x: 0, y: 0, width: 0, height: 0)
let cv = UICollectionView(frame: frame, collectionViewLayout: layout)
cv.register(CollectionViewCellView.self, forCellWithReuseIdentifier: Identifier.coinPageCollectionViewOverviewCell.rawValue)
if let flowLayout = cv.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.scrollDirection = .horizontal
flowLayout.minimumLineSpacing = 0
}
cv.backgroundColor = .green
cv.isPagingEnabled = true
//cv.backgroundColor = .blue
return cv
}()
What did I miss?
I set up the datasource and also connect it to the datasource of the collectionView, but the methods do not get called.
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let frame = CGRect(x: 0, y: 0, width: 0, height: 0)
let cv = UICollectionView(frame: frame, collectionViewLayout: layout)
cv.register(CollectionViewCellView.self, forCellWithReuseIdentifier: Identifier.coinPageCollectionViewOverviewCell.rawValue)
if let flowLayout = cv.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.scrollDirection = .horizontal
flowLayout.minimumLineSpacing = 0
}
cv.backgroundColor = .green
cv.isPagingEnabled = true
//cv.backgroundColor = .blue
cv.delegate = self
return cv
}()