How to get IndexPath for UICollectionView Header? - ios

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

Related

Show view at taped button inside UICollectionView Cell

I'm working on an iOS app which contain a collectionview, inside this collectionview I have a button, as follow:
ViewController
-UICollectionView(myColl)
--UICollectionViewCell
---UIButton (myButton)
-UIView (myView)
What I want to do is to show myView under myButton when I tapped it, what I try is:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TestCell",
for: indexPath) as! TestCell
cell.myButton.addTarget(self, action: #selector(self.showEditView), for: .touchUpInside)
}
and in showEditView()
#objc func showEditView(sender:UIButton!) {
let position: CGPoint = sender.convert(CGPoint.zero, to: self.myColl)
myView.center = position
}
But that wasn't worked, what can I do to get that?
#objc func showEditView(sender:UIButton!) {
let position: CGPoint = sender.convert(CGPoint.zero, to: self.collectionview)
let indexPath = self.collectionview.indexPathForItem(at: position)
if indexPath != nil {
myView.center = position
//if your view is hidden
myView.isHidden = false
}
}
Actually you can use protocol in your UICollectionViewCell for it. It's healthier than use forge using.
protocol YourProtocolDelegate {
func showEditView()
}
class YourCVCell: UICollectionViewCell {
var delegate: YourProtocolDelegate!
var button = UIButton()
override init(frame: CGRect) {
button.addTapGestureRecognizer {
self.delegate.showEditView()
}
}
}
Of course you must take inherit from your protocol to your ViewController class
class YourViewController: UIViewController, YourProtocolDelegate {
}
After that you can reference your cell's protocol to your viewcontroller.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "TestCell",
for: indexPath) as! TestCell
cell.delegate = self
}

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

Swift Thread 1: Fatal error: init(coder:) has not been implemented (Calling super solution doesn't work)

Hey guys I have searched all around and still cannot find a solution to my problem. I have a custom class here :
import UIKit
/**
DatasourceController is simply a UICollectionViewController that
allows you to quickly create list views.
In order to render our items in your list, simply provide it with a
Datasource object.
*/
open class DatasourceController: UICollectionViewController,
UICollectionViewDelegateFlowLayout {
open let activityIndicatorView: UIActivityIndicatorView = {
let aiv = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
aiv.hidesWhenStopped = true
aiv.color = .black
return aiv
}()
open var datasource: Datasource? {
didSet {
if let cellClasses = datasource?.cellClasses() {
for cellClass in cellClasses {
collectionView?.register(cellClass, forCellWithReuseIdentifier: NSStringFromClass(cellClass))
}
}
if let headerClasses = datasource?.headerClasses() {
for headerClass in headerClasses {
collectionView?.register(headerClass, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: NSStringFromClass(headerClass))
}
}
if let footerClasses = datasource?.footerClasses() {
for footerClass in footerClasses {
collectionView?.register(footerClass, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: NSStringFromClass(footerClass))
}
}
collectionView?.reloadData()
}
}
public init() {
super.init(collectionViewLayout: UICollectionViewFlowLayout())
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
let defaultCellId = "lbta_defaultCellId"
let defaultFooterId = "lbta_defaultFooterId"
let defaultHeaderId = "lbta_defaultHeaderId"
override open func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = .white
collectionView?.alwaysBounceVertical = true
view.addSubview(activityIndicatorView)
activityIndicatorView.anchorCenterXToSuperview()
activityIndicatorView.anchorCenterYToSuperview()
collectionView?.register(DefaultCell.self, forCellWithReuseIdentifier: defaultCellId)
collectionView?.register(DefaultHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: defaultHeaderId)
collectionView?.register(DefaultFooter.self, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: defaultFooterId)
}
override open func numberOfSections(in collectionView: UICollectionView) -> Int {
return datasource?.numberOfSections() ?? 0
}
override open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return datasource?.numberOfItems(section) ?? 0
}
//need to override this otherwise size doesn't get called
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 50)
}
override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: DatasourceCell
if let cls = datasource?.cellClass(indexPath) {
cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(cls), for: indexPath) as! DatasourceCell
} else if let cellClasses = datasource?.cellClasses(), cellClasses.count > indexPath.section {
let cls = cellClasses[indexPath.section]
cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(cls), for: indexPath) as! DatasourceCell
} else if let cls = datasource?.cellClasses().first {
cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(cls), for: indexPath) as! DatasourceCell
} else {
cell = collectionView.dequeueReusableCell(withReuseIdentifier: defaultCellId, for: indexPath) as! DatasourceCell
}
cell.controller = self
cell.datasourceItem = datasource?.item(indexPath)
return cell
}
override open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let reusableView: DatasourceCell
if kind == UICollectionElementKindSectionHeader {
if let classes = datasource?.headerClasses(), classes.count > indexPath.section {
reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: NSStringFromClass(classes[indexPath.section]), for: indexPath) as! DatasourceCell
} else if let cls = datasource?.headerClasses()?.first {
reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: NSStringFromClass(cls), for: indexPath) as! DatasourceCell
} else {
reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: defaultHeaderId, for: indexPath) as! DatasourceCell
}
reusableView.datasourceItem = datasource?.headerItem(indexPath.section)
} else {
if let classes = datasource?.footerClasses(), classes.count > indexPath.section {
reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: NSStringFromClass(classes[indexPath.section]), for: indexPath) as! DatasourceCell
} else if let cls = datasource?.footerClasses()?.first {
reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: NSStringFromClass(cls), for: indexPath) as! DatasourceCell
} else {
reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: defaultFooterId, for: indexPath) as! DatasourceCell
}
reusableView.datasourceItem = datasource?.footerItem(indexPath.section)
}
reusableView.controller = self
return reusableView
}
open func getRefreshControl() -> UIRefreshControl {
let rc = UIRefreshControl()
rc.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
return rc
}
#objc open func handleRefresh() {
}
open var layout: UICollectionViewFlowLayout? {
get {
return collectionViewLayout as? UICollectionViewFlowLayout
}
}
}
Which is parent class to a UICollectionView controller in my storyboard. I make a controller class for it here :
import LBTAComponents
class homeView: DatasourceController {
override func viewDidLoad() {
super.viewDidLoad()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Issue i get Thread 1: Fatal error: init(coder:) has not been implemented when running this code I have already tried the solution :
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
this does not work. Any other suggestions? I got this framework from cocoapod library if you would like to see for your self :
pod 'LBTAComponents'
I suspect the issue is something to do with a collection view and the storyboard but i cant figure it out.
Whenever ViewController initialising form Storyboard/XIB, it doing it by init(coder: )
Your base class DatasourceController override initialisers
public init() {
super.init(collectionViewLayout: UICollectionViewFlowLayout())
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
So when you calling super.init(coder:...) in your HomeController, it will actually call fatalError("init(coder:) has not been implemented") Seems whoever wrote this class, is not a great storyboard/xib lover.
You can delete remove both initialisers in DatasourceController, but make sure that you setting up Flow layout in storyboard. Or you can change them to call super.
If you can't change base class, you can't load your VC from Storyboard.

How to present a ViewController after pressing a button inside of a CollectionViewCell

Hey i'm new to programming and my problem is, i have a UICollectionViewController with 4 cells that are horizontal scrollable. Inside of the 4th cell i have a UIButton(optionsButton) on top of a UIView (ProfileContainerView).
The UIViewController I want to present is called ProfileEditViewController and is set up in Main.storyboard.
How can i present a UIViewController after pressing this button?
ProfileCell:
class ProfileCell: UICollectionViewCell {
let profileContainerView: UIView = {
let view = UIView()
return view
}()
lazy var optionsButton: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(#imageLiteral(resourceName: "Settings"), for: UIControlState.normal)
btn.addTarget(self, action: #selector(handleOptionsButton), for: UIControlEvents.touchUpInside)
return btn
}()
#objc func handleOptionsButton() {
print("Button pressed")
}
}
HomeViewController:
class HomeViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let profileCelId = "profileCell"
override func viewDidLoad() {
super.viewDidLoad()
setupSwipeView()
}
func setupSwipeView() {
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cell)
collectionView?.register(ProfileCell.self, forCellWithReuseIdentifier: profileCelId)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item == 3 {
return collectionView.dequeueReusableCell(withReuseIdentifier: profileCelId, for: indexPath)
}
return cell
}
}
You can use delegates to implement this.
Below is the code to implement this
protocol ProfileCollectionViewCellDelegate {
func buttonPressedAtIndexPath(inCell: ProfileCell)
}
class ProfileCell: UICollectionViewCell {
var delegate : ProfileCollectionViewCellDelegate?
let profileContainerView: UIView = {
let view = UIView()
return view
}()
lazy var optionsButton: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(#imageLiteral(resourceName: "Settings"), for: UIControlState.normal)
btn.addTarget(self, action: #selector(handleOptionsButton), for: UIControlEvents.touchUpInside)
return btn
}()
#objc func handleOptionsButton() {
if self.delegate != nil {
self.delegate?.buttonPressedAtIndexPath(self)
}
}
}
For your HomeViewController
class HomeViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, ProfileCollectionViewCellDelegate {
let profileCelId = "profileCell"
override func viewDidLoad() {
super.viewDidLoad()
setupSwipeView()
}
func setupSwipeView() {
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cell)
collectionView?.register(ProfileCell.self, forCellWithReuseIdentifier: profileCelId)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: profileCelId, for: indexPath)
cell.delegate = self
return cell
}
fun buttonPressedAtIndexPath(inCell: ProfileCell) {
let indexOfCell = self.collectionView.indexPath(for: cell)
if indexOfCell.row == 3 {
//Do your work here
}
}
}
You can present your ProfileEditViewController, which is styled in your Main.storyboard the following way:
1) Give your ProfileEditViewController a StoryBoard ID. E.g. "ProfileEditViewController" - Some question regarding this is here: What is a StoryBoard ID and how can i use this?
2) Register the UIViewController for the action on the UIButton or offer an appropriate callback functionality.
As your HomeViewController is also your Collection View's datasource, you can easily extend your DataSource method
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell`
Implementation could look like:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item == 3 {
let profileCell = collectionView.dequeueReusableCell(withReuseIdentifier: profileCelId, for: indexPath)
if let _cell = cell as? ProfileCell,
let button = _cell.optionsButton as? UIButton {
button.addTarget(self, action: #selector(handleOptionsButton), forControlEvents: UIControlEvents.TouchUpInside)
}
return profileCell;
}
return cell
}
Make sure that your buttons Action is now also being implemented by your HomeViewController
#objc func handleOptionsButton() {
print("Button pressed")
}
3) Now in HomeViewController.handleOptionsButton you need to provide a functionality to support the transition to that specific Controller with the desired StoryboardID:
let storyboard = UIStoryboard(name: "Main", bundle:Bundle.main)
let controller = storyboard.instantiateViewController(withIdentifier: "ProfileEditViewController")
self.present(controller, animated: true, completion: nil)
let proCell = ProfileCell()
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cell, for: indexPath)
if indexPath.item == 3 {
let profileCell = collectionView.dequeueReusableCell(withReuseIdentifier: profileCelId, for: indexPath)
let button = proCell.optionsButton
button.addTarget(self, action: #selector(handleOptionsButton), for: UIControlEvents.touchUpInside)
return profileCell;
}
return cell
}

How to get the index path of a UICollectionView header?

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!

Resources