UICollectionView: make first item's width different from the rest - swift

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

Related

Problem with creating Compositional Layout (UICollectionView)

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") }
}

Compositional Layout doesn't resize height correctly of “_UICollectionViewOrthogonalScrollerEmbeddedScrollView” causing a button to not receive events

I am having a problem with a collection view using a compositional layout! the Card section supposed to be auto-resizing for the height and it should be scrolling horizontally while the whole collection view vertically as this test project!
The issue I am having is that I set an estimated height for the item and the group but this object “_UICollectionViewOrthogonalScrollerEmbeddedScrollView” doesn't resize accordingly, Thus the portion of the cell outside the bounds of “_UICollectionViewOrthogonalScrollerEmbeddedScrollView” doesn't receive events
So, If you press the button on the longest card in the project or tried to scroll or touch the portion outside the “_UICollectionViewOrthogonalScrollerEmbeddedScrollView” the cell doesnt receive any events.
Check the screenshot attached… Does anybody have an idea how to solve this issue???
Link to the project: https://drive.google.com/file/d/1tksL0qZ1egxy7M4UD8eLMeY61H7U1ou1/view?usp=sharing
This is My Compositional layout code
struct CollectionViewLayout {
static func createLayout(collectionView: UICollectionView) -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex, _: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let sectionLayoutKind = ViewController.Section.allCases[sectionIndex]
return sectionLayout(for: sectionLayoutKind, collectionView: collectionView)
}
return layout
}
static func sectionLayout(for kind: ViewController.Section,
collectionView: UICollectionView) -> NSCollectionLayoutSection {
switch kind {
case .cards:
return cardsLayoutSection(collectionView: collectionView)
case .buttons:
return buttonsLayoutSection()
}
}
static func cardsLayoutSection(collectionView: UICollectionView) -> NSCollectionLayoutSection {
let etimationHeight: CGFloat = 300
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(etimationHeight))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let width = collectionView.frame.width - 40.0 // spacing from left and right 20 each
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(width),
heightDimension: .estimated(etimationHeight))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)
section.interGroupSpacing = 10
section.orthogonalScrollingBehavior = .groupPaging
return section
}
static func buttonsLayoutSection() -> NSCollectionLayoutSection {
let layoutSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(100))
let item = NSCollectionLayoutItem(layoutSize: layoutSize)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: layoutSize, subitem: item, count: 1)
let section = NSCollectionLayoutSection(group: group)
return section
}
}
And this my cell configurator
class CellConfigurator {
let collectionView: UICollectionView
private let numberOfCards = 3
init(collectionView: UICollectionView) {
self.collectionView = collectionView
}
var numberOfItems: Int {
return numberOfCards
}
var activeIndex: Int {
return 0
}
func cellForButtons(indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String.init(describing: Buttons.self), for: indexPath)
return cell
}
func cardCell(indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String.init(describing: Card.self), for: indexPath) as! Card
switch indexPath.row {
case 0:
cell.heightConstraint.constant = 300
case 1:
cell.heightConstraint.constant = 100
default:
cell.heightConstraint.constant = 500
}
return cell
}
}
After a lot of research it seems an apple bug! Anyways I found a workaround which is subcallsing the collectionView and fix the “_UICollectionViewOrthogonalScrollerEmbeddedScrollView” size and position. I got this form this post UICollectionViewCompositionalLayout bug on iOS 14.3
public final class CollectionView: UICollectionView {
override public func layoutSubviews() {
super.layoutSubviews()
guard #available(iOS 14.3, *) else { return }
subviews.forEach { subview in
guard
let scrollView = subview as? UIScrollView,
let minY = scrollView.subviews.map(\.frame.origin.y).min(),
let maxHeight = scrollView.subviews.map(\.frame.height).max(),
minY > scrollView.frame.minY || maxHeight > scrollView.frame.height
else { return }
scrollView.contentInset.top = -minY
scrollView.frame.origin.y = minY
scrollView.frame.size.height = maxHeight
}
}
}

Header in NSCollectionView is has wrong size

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)
}
}

Cannot get indexPath and selection on Collection View

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?

How do I clear images from my UIImageView

I have the following code for a compositionalLayout in Swift, but my images are not going away with the reuse of cells.
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet var collectionView: UICollectionView!
let myInset: CGFloat = 4.0
let dataColors = [UIColor.red, UIColor.blue, UIColor.green, UIColor.magenta, UIColor.purple, UIColor.orange, UIColor.red, UIColor.blue, UIColor.green, UIColor.magenta, UIColor.purple, UIColor.systemYellow, UIColor.red, UIColor.blue, UIColor.green, UIColor.magenta, UIColor.purple, UIColor.orange, UIColor.red, UIColor.blue, UIColor.green, UIColor.magenta, UIColor.purple, UIColor.systemYellow]
let theImages = [
"MEN_8882","002","003","004","005","006","001","002","003","004","005","006",
"MEN_8882","002","003","004","005","006","001","002","003","004","005","006"
]
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
collectionView.setCollectionViewLayout(createCustomLayout(), animated: false)
collectionView.backgroundColor = .white
self.collectionView.delegate = self
self.collectionView.dataSource = self
collectionView.register(QuickCell.self, forCellWithReuseIdentifier: "cellID")
//configureCollectionView()
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1//dataColors.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataColors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellID", for: indexPath) as? QuickCell {
cell.backgroundColor = dataColors[indexPath.row]
let mySubView = UIImageView()
mySubView.image = UIImage(named: theImages[indexPath.row])
cell.addSubview(mySubView)
mySubView.translatesAutoresizingMaskIntoConstraints = false
mySubView.topAnchor.constraint(equalTo: cell.topAnchor, constant: myInset).isActive = true
mySubView.leadingAnchor.constraint(equalTo: cell.leadingAnchor, constant: myInset).isActive = true
mySubView.trailingAnchor.constraint(equalTo: cell.trailingAnchor, constant: myInset * (-1)).isActive = true
mySubView.bottomAnchor.constraint(equalTo: cell.bottomAnchor, constant: myInset * (-1)).isActive = true
mySubView.clipsToBounds = true
// mySubView.layer.cornerRadius = 8
mySubView.contentMode = .scaleAspectFit
cell.clipsToBounds = true
cell.layoutIfNeeded()
//cell.layer.cornerRadius = 12
return cell
} else {
return UICollectionViewCell()
}
}
func createCustomLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (section: Int, environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let myItemInset: CGFloat = 2.0
let leadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)))
leadingItem.contentInsets = NSDirectionalEdgeInsets(top: myItemInset, leading: myItemInset, bottom: myItemInset, trailing: myItemInset)
let leadingGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.7), heightDimension: .fractionalHeight(1.0))
let leadingGroup = NSCollectionLayoutGroup.vertical(layoutSize: leadingGroupSize, subitem: leadingItem, count: 1)
let trailingGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(1.0))
let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: trailingGroupSize, subitem: leadingItem, count: 5)
let fullGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let fullGroup = NSCollectionLayoutGroup.horizontal(layoutSize: fullGroupSize, subitems: [leadingGroup, trailingGroup])
let section = NSCollectionLayoutSection(group: fullGroup)
section.orthogonalScrollingBehavior = .groupPagingCentered
section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0)
return section
}
return layout
}
}
The image "MEN..." is portrait while the rest are landscape, and as i scroll back and forth, i see overlapping images in the items.
the code for QuickCell is empty - I'm not sure what to put ther, some kind of initalization? But it should work anyways, right?
import UIKit
class QuickCell: UICollectionViewCell {
}
A subview of type UIImageView is added to your custom Cell (QuickCell) each time your collection view cell is resued. It happens in cellForRowAt delegate method.
So, you have to remove previously added image views from your cell first before adding a new one.
I suggest you move your cell configuration code to QuickCell.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellID", for: indexPath)
if let quickCell = quickCell as? QuickCell {
quickCell.backgroundColor = self.dataColors[indexPath.row]
quickCell.setImage(self.theImages[indexPath.row], insetBy: self.myInset)
return quickCell
}
return cell
}
Do your custom cell configurations here!
class QuickCell: UICollectionViewCell {
func setImage(_ image: UIImage, insetBy inset: CGFloat) {
// Remove previously added image views first if any
for subview in self.subviews where subview.isKind(of: UIImageView.self) {
subview.removeFromSuperview()
}
let imageView = UIImageView()
imageView.image = image
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFit
self.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: inset),
imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: inset),
imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: inset * (-1)),
imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: inset * (-1))
])
self.layoutIfNeeded()
}
}
To remove image from UIImageView do the following.
yourImageView.image = nil
The problem here is that you are adding a new UIImageView every time the cell is reused. The ones added in previous interations of the cellForItemAtIndexPath: method do not go away, so others are added on top.
These are the problem lines:
let mySubView = UIImageView()
mySubView.image = UIImage(named: theImages[indexPath.row])
cell.addSubview(mySubView)
It would be better to add the image view once when a cell is initialised (or in storyboard with an outlet) then just set the image in the cellForItemAtIndexPath: method.
You keep adding subviews inside cellForItemAt and this causes the overlapping as cells are dequeued , you need to create an outlet for the imageview or create it programmatically inside the cell like
class QuickCell: UICollectionViewCell {
let mySubView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
let myInset: CGFloat = 4.0
self.contentView.addSubview(mySubView)
mySubView.translatesAutoresizingMaskIntoConstraints = false
mySubView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: myInset).isActive = true
mySubView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: myInset).isActive = true
mySubView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: myInset * (-1)).isActive = true
mySubView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: myInset * (-1)).isActive = true
mySubView.clipsToBounds = true
// mySubView.layer.cornerRadius = 8
mySubView.contentMode = .scaleAspectFit
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
With
cell.mySubView.image = UIImage(named: theImages[indexPath.row])
or
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellID", for: indexPath) as? QuickCell {
cell.subviews.forEach {
if $0.tag == 333 {
$0.removeFromSuperview()
}
}
cell.backgroundColor = dataColors[indexPath.row]
let mySubView = UIImageView()
mySubView.tag = 333
This is checked by default for UICollectionView, try to uncheck it. It should work for you.