How to get the index path of a UICollectionView header? - ios

Using the view's indexPathForItemAtPoint, I will get an index path for a cell, but never a UICollectionReusableView (header/footer) -- as it always returns nil.

override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) as! HeaderCollectionReusableView
let gestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didSelectSection(gesture:)))
headerView.addGestureRecognizer(gestureRecognizer)
return headerView
}
}
Now in didSelectSection :
func didSelectSection(gesture: UITapGestureRecognizer) {
let indexPaths = self.collectionView?.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionElementKindSectionHeader)
for indexPath in indexPaths! {
if (gesture.view as! HeaderCollectionReusableView) == collectionView?.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: indexPath){
print("found at : \(indexPath)")
break
}
}
}

You can add extension for UICollectionView where pass reference of supplementary view and kind of this view (UICollectionView.elementKindSectionHeader or UICollectionView.elementKindSectionFooter)
extension UICollectionView {
func indexPathForSupplementaryElement(_ supplementaryView: UICollectionReusableView, ofKind kind: String) -> IndexPath? {
let visibleIndexPaths = self.indexPathsForVisibleSupplementaryElements(ofKind: kind)
return visibleIndexPaths.first(where: {
self.supplementaryView(forElementKind: kind, at: $0) == supplementaryView
})
}
}
This method doesn't work if supplementary view isn't visible!

Related

How to get IndexPath for UICollectionView Header?

View: HeaderView
class HeaderCell: UICollectionViewCell {
//MARK: - Properties
....
lazy var clearButton: UIButton = {
let bt = UIButton(type: .custom)
bt.backgroundColor = .clear
bt.addTarget(self, action: #selector(handleClearButton(button:)), for: .touchUpInside)
return bt
}()
weak var delegate: HeaderCellDelegate?
//MARK: - Init
required init?(coder aDecoder: NSCoder) {
return nil
}
override init(frame: CGRect) {
super.init(frame: frame)
configureCell()
addViews()
setConstraints()
}
....
//MARK: - Handlers
#objc func handleClearButton(button: UIButton) {
delegate?.expandSelectedHeader(self)
}
Protocol: HeaderCellDelegate
protocol HeaderCellDelegate: class {
func expandSelectedHeader(_ header: UICollectionViewCell)
}
Controller: SomeController
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: reuseIdentifierForHeader, for: indexPath) as! HeaderCell
header.toMenuControllerDelegate = self
return header
case UICollectionView.elementKindSectionFooter:
let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: reuseIdentifierForFooter, for: indexPath) as! MenuFooterCell
return footer
default:
return UICollectionReusableView()
}
}
extension SomeController:HeaderCellDelegate {
func expandSelectedHeader(_ header: UICollectionViewCell) {
let indexPath = collectionView.indexPath(for: header)
print(indexPath)
}
}
I am trying to using the clearButton inside headerView that when the button is tapped it will send itself(UIcollectionViewCell) to the controller. So once the controller receives information about the cell, I can use collectionView.indexPath(for cell:UICollectionViewCell) to get the indexPath of header in which the button is tapped. But with the code above, I am only getting nil for print(indexPath).
How can I go about the problem???
I appreciate your help in advance.
Your delegate function, expandSelectedHeader() does not know about its indexPath unless you pass it to. A workaround is you declare a index path property within your HeaderCell class and pass the value when you are creating the supplementary view:
class HeaderCell: UICollectionViewCell {
//MARK: - Properties
var indexPath: IndexPath?
// rest of the code
}
//MARK: - Handlers
#objc func handleClearButton(button: UIButton) {
delegate?.expandSelectedHeader(self, indexPath: self.indexPath)
}
Also change the protocol a bit:
protocol HeaderCellDelegate: class {
func expandSelectedHeader(_ header: UICollectionViewCell, indexPath: IndexPath?)
}
When you are creating the view assign the index path:
// ...
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: reuseIdentifierForHeader, for: indexPath) as! HeaderCell
// I'd suggest do not force down case the view as HeaderCell, use if..let
header.toMenuControllerDelegate = self
header.indexPath = indexPath
// ...
Finally:
extension SomeController:HeaderCellDelegate {
func expandSelectedHeader(_ header: UICollectionViewCell, indexPath: IndexPath) {
print(indexPath)
}
}
Instead of sending whole cell and then get index path. Send IndexPath in protocol and get header from that is a good approach
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) as! HeaderCollectionReusableView
let gestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didSelectSection(gesture:)))
headerView.addGestureRecognizer(gestureRecognizer)
return headerView
}
}
Now in didSelectSection :
#objc func didSelectSection(_ gestureRecognizer: UITapGestureRecognizer) {
let indexPaths = self.collectionView?.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionElementKindSectionHeader)
for indexPath in indexPaths! {
if (gestureRecognizer.view as! HeaderCollectionReusableView) == collectionView?.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: indexPath){
print("found at : \(indexPath)")
break
}
}
}

Cell's delegate is nil in didSelectItemAt

I have a UICollectionViewCell with an item Desk. Desk has a Bool property that I want to change when a cell is tapped.
I'm trying to better familiarize myself with the delegate pattern, and want to avoid using something like a transparent button overlaid on the cell. I thought it would work to assign the cell's delegate in cellForItemAt and trigger the delegate method in didSelectItemAt, but the cell's delegate is nil when I check in didSelectItemAt.
Struct:
struct Desk: Equatable, Codable {
var name: String
var wasTouched: Bool = false
}
Protocol:
protocol TouchTheDesk: AnyObject {
func youTouchedIt(cell: DeskCell)
}
Cell:
import UIKit
class DeskCell: UICollectionViewCell {
#IBOutlet weak var deskLbl: UILabel!
weak var delegate: TouchTheDesk?
var desk: Desk? {
didSet {
updateViews()
}
}
func updateViews() {
self.deskLbl.text = desk?.name
}
}
Conform VC to Protocol and define delegate method:
extension NotAShoppingListVC: TouchTheDesk {
func youTouchedIt(cell: DeskCell) {
if cell.desk?.wasTouched != nil {
cell.desk!.wasTouched = !cell.desk!.wasTouched
} else {
cell.desk!.wasTouched = true
}
collectionView.reloadData()
}
}
cellForItemAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? DeskCell else {return UICollectionViewCell()}
cell.desk = deskDealer.desks[indexPath.item]
if cell.desk!.wasTouched { //assigned on line above
cell.backgroundColor = .red
} else {
cell.backgroundColor = .green
}
cell.delegate = self
return cell
}
didSelectItemAt:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? DeskCell else {return}
#warning("delegate is nil")
cell.delegate?.youTouchedIt(cell: cell)
}
edit: If I call the delegate method directly in didSelectItemAt and pass in the cell, the indexPath for the cell is nil in the delegate method
Inside didSelectItemAt Replace
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? DeskCell else {return}
with
guard let cell = collectionView.cellForItem(at: indexPath) as? DeskCell else {return}
when you use dequeueReusableCell outside of cellForItemAt it will return a different cell other than the clicked one

UICollectionView inside UITableViewCell returning empty always even though shows as selected

I am using a UICollectionView inside UITableViewCell. I am able to select the cells inside the UICollectionView. But when i try to get the UICollectionView or selected cells, the result is always null.I have been stuck on this for a long time. i included my code below for your reference.
class WeekDaysSelCell: UITableViewCell,UICollectionViewDelegate, UICollectionViewDataSource,UICollectionViewDelegateFlowLayout {
var weekdays = ["S", "M", "T", "W", "T", "F", "S"]
var weekdaysSelected = [String]()
#IBOutlet var weeklyDaysColView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
self.weeklyDaysColView.delegate = self
self.weeklyDaysColView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 7
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell : WeekDaysCollCell = weeklyDaysColView.dequeueReusableCell(withReuseIdentifier: "weekday", for: indexPath) as! WeekDaysCollCell
cell.weekDayLabel.text = weekdays[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell : WeekDaysCollCell = self.weeklyDaysColView.cellForItem(at: indexPath) as! WeekDaysCollCell
if (cell.backgroundColor == UIColor.gray) {
cell.backgroundColor = UIColor.clear
weekdaysSelected.removeAll { $0 == String(indexPath.row)}
//print("Removed from weekdaysSelected:", indexPath.row)
} else {
cell.backgroundColor = UIColor.gray
cell.isSelected = true
//weeklyDaysColView.selectItem(at: indexPath, animated: true, scrollPosition: [])
weekdaysSelected.append(String(indexPath.row))
//print("Added to weekdaysSelected:", indexPath.row)
}
}
}
// Trying to get the collection view from inside a willMove(toParent parent: UIViewController?) method.
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if parent == nil
{
if let delegate = self.delegate {
print("Inside If condition")
// Code that i use to get the cell
let cell3 = tableView.dequeueReusableCell(withIdentifier: "cell3") as! WeekDaysSelCell
print(cell3.weekdaysSelected)
print(cell3.weeklyDaysColView.indexPathsForSelectedItems)
// Trying to pass selected cells
//delegate.repeatCustomSelection(selectedIdx: String(lastSelection.row),repeatCustomSel: repeatCustomSelection)
}
}
}
You are trying to get a reusable cell in willMove(toParent parent: UIViewController?) , this is not going to return you a expected cell.
You need to get the cell , using a indexPath .
func cellForRow(at indexPath: IndexPath) -> UITableViewCell?
#andyPaul, is right you are generating the new cell in willMove(toParent parent: UIViewController?). Instead of that you have to pass the indexpath pf collection view when ever user selected any cell to your controller from the tableView Cell Class.
Now What is TypeAlias you can read from this link about type alias:- https://www.programiz.com/swift-programming/typealias
Create the typeAlias on above of your tableViewCell Class like this:-
typealias closureBlock = (_ isCapture : AnyObject?) ->()
class tableViewCellClass: UITableViewCell {
var callBack: closureBlock?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
Just Go to CollectionView didSelectItemAt Method and use this code after your coding
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell : WeekDaysCollCell = self.weeklyDaysColView.cellForItem(at: indexPath) as! WeekDaysCollCell
if (cell.backgroundColor == UIColor.gray) {
cell.backgroundColor = UIColor.clear
weekdaysSelected.removeAll { $0 == String(indexPath.row)}
//print("Removed from weekdaysSelected:", indexPath.row)
} else {
cell.backgroundColor = UIColor.gray
cell.isSelected = true
//weeklyDaysColView.selectItem(at: indexPath, animated: true, scrollPosition: [])
weekdaysSelected.append(String(indexPath.row))
//print("Added to weekdaysSelected:", indexPath.row)
}
guard let callBackClosure = self.callBack else {
return
}
callBackClosure(indexPath as AnyObject)
// You can pass any value here either indexpath or Array.
}
}
Now you have to initialise this closure so that it can check whether in which controller it will return the value when you assign the value from CollectionView didSelectItemAt Method.
Go to your ViewController Class where you have added the tableview and their datasources.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//You have to pass your tableview Cell Instance here and their reuse Identifier
let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCellClass", for: indexPath) as! tableViewCellClass
cell.callBack = { [weak self] (selectedIndexPath) -> ()in
// You will get the current selected index path of collection view here, Whenever you pass any index path from collectionView did SelectItem Method.
print(selectedIndexPath)
}
return cell
}

what is the string submitted to getting the reusableview in collection view section

In collection view section, for getting the particular section we have suplementaryView function. but what is the value should be passed for "forElementKind" ?
collectionView.supplementaryView(forElementKind: "???", at: 0)
It is type of View.
if it is header then you can use UICollectionElementKindSectionHeader or if footer then UICollectionElementKindSectionFooter
let indexHeaderForSection = NSIndexPath(row: 0, section: indexPath.section)
let heder = collectionViewArea?.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: indexHeaderForSection as IndexPath ) // you need to pass indexpath instead of integer value.
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if collectionView == self.collectionCar {
return UICollectionReusableView()
}
let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "PersonFooter", for: indexPath) as! PersonFooter
return footer
}

Displaying checkmark on collectionview

I have a collection view where when each cell is tapped a larger version of the cell image pops up and disappears when tapped again. On top of this I'd like to be able to select a view in the corner of the cell that displays a blue checkmark(SSCheckMark View) or a greyed out checkmark when tapped again. My current code is:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as! PhotoCell
cell.backgroundColor = .clear
cell.imageView.image = UIImage(contentsOfFile: imagesURLArray[indexPath.row].path)
cell.checkmarkView.checkMarkStyle = .GrayedOut
cell.checkmarkView.tag = indexPath.row
cell.checkmarkView.checked = false
let tap = UITapGestureRecognizer(target: self, action: #selector(checkmarkWasTapped(_ :)))
cell.checkmarkView.addGestureRecognizer(tap)
return cell
}
func checkmarkWasTapped(_ sender: SSCheckMark) {
let indexPath = IndexPath(row: sender.tag, section: 1)
if sender.checked == true {
sender.checked = false
} else {
sender.checked = true
}
collectionView.reloadItems(at: [indexPath])
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
addZoomedImage(indexPath.row)
addGestureToImage()
addBackGroundView()
view.addSubview(selectedImage)
}
But when I run and select the checkmark view I get the error:
unrecognized selector sent to instance on the first line of checkmarkWasTapped() i can see that it doesn't like sender but i don't know why. Any help would be great.
UITapGestureRecognizer tap's sender is the gesture. The checkmarkWasTapped method definition is wrong. And you can get the checkmarView using sender.view. Try this.
func checkmarkWasTapped(_ sender: UIGestureRecognizer) {
let checkmarkView= sender.view as? SSCheckMark
let indexPath = IndexPath(row: checkmarkView.tag, section: 1)
if checkmarkView.checked == true {
checkmarkView.checked = false
} else {
checkmarkView.checked = true
}
collectionView.reloadItems(at: [indexPath])
}

Resources