So this problem is fairly straightforward. I have a UICollectionViewController (MyProfile.swift) with a header section (MyProfileHeader.swift). Within the latter, I have a UISegmentedControl to return different numbers of items AND items in the collection view cells (I don't want to initialize an instance of the latter's class within the UICollectionViewController). This is my code for MyProfile.swift class. I tried adding a target in the viewForSupplementaryElementOfKind method to return different queries (which works), but I ultimately have to access the segmented control within the numberOfItemsInSection and cellForItemAtIndexPath methods. The "testObjects" and "writeObjects" are array values that are queried via the addTarget method in the viewForSupplementaryElementOfKind. I set the indexPath but it returns an error for obvious reasons... How can I access segmented control?
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
var numberOfItems: Int? = 0
let indexPath = NSIndexPath(forItem: 0, inSection: 0)
let header = collectionView.dequeueReusableSupplementaryViewOfKind(UICollectionElementKindSectionHeader, withReuseIdentifier: "header", forIndexPath: indexPath) as! MyProfileHeader
if header.userContent.selectedSegmentIndex == 1 {
numberOfItems = textObjects.count
} else if header.userContent.selectedSegmentIndex == 2 {
numberOfItems = 0
} else {
numberOfItems = photoObjects.count
}
print("F: \(numberOfItems!)")
return numberOfItems!
}
-1st create a UICollectionReusableView subclass and name it SegmentedHeader
-2nd Inside the SegmentedHeader class add a protocol to keep track of which segment was selected. When a segment is selected inside the collectionView's header the protocol/delegate will get passed the value of that segment
-3rd make sure you set the delegate weak var delegate: SegmentedHeaderDelegate?
-4th when programmatically creating the segmentedControl add a target named selectedIndex(_ sender: UISegmentedControl). When a segment is pressed, you pass the value of that segment to the protocols trackSelectedIndex() function
protocol SegmentedHeaderDelegate: class {
func trackSelectedIndex(_ theSelectedIndex: Int)
}
class SegmentedHeader: UICollectionReusableView {
//MARK:- Programmatic Objects
let segmentedControl: UISegmentedControl = {
let segmentedControl = UISegmentedControl(items: ["Zero", "One", "Two"])
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
segmentedControl.tintColor = UIColor.red
segmentedControl.backgroundColor = .white
segmentedControl.isHighlighted = true
segmentedControl.addTarget(self, action: #selector(selectedIndex(_:)), for: .valueChanged)
return segmentedControl
}()
//MARK:- Class Property
weak var delegate: SegmentedHeaderDelegate?
//MARK:- Init Frame
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setupAnchors()
}
//MARK:- TargetAction
#objc func selectedIndex(_ sender: UISegmentedControl){
let index = sender.selectedSegmentIndex
switch index {
case 0: // this means the first segment was chosen
delegate?.trackSelectedIndex(0)
break
case 1: // this means the middle segment was chosen
delegate?.trackSelectedIndex(1)
break
case 2: // this means the last segment was chosen
delegate?.trackSelectedIndex(2)
break
default:
break
}
}
fileprivate func setupAnchors(){
addSubview(segmentedControl)
segmentedControl.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0).isActive = true
segmentedControl.rightAnchor.constraint(equalTo: self.rightAnchor, constant: 0).isActive = true
segmentedControl.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
segmentedControl.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Inside the class that has the UICollectionViewController:
IMPORTANT - Make sure you set the delegate inside viewForSupplementaryElementOfKind or none of this will work
// MAKE SURE YOU INCLUDE THE SegmentedHeaderDelegate so the class conforms to it
class ViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, SegmentedHeaderDelegate{
// add a class property for the header identifier
let segmentedHeaderIdentifier = "segmentedHeader"
// add a class property to keep track of which segment was selected. This gets set inside the tracktSelectedIndex() function. You will need this for cellForRowAtIndexPath so you can show whatever needs to be shown for each segment
var selectedSegment: Int?
// register the SegmentedHeader with the collectionView
collectionView.register(SegmentedHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: segmentedHeaderIdentifier)
// inside the collectionView's delegate below add the header
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
var header: UICollectionReusableView?
if kind == UICollectionElementKindSectionHeader{
let segmentedHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: segmentedHeaderIdentifier, for: indexPath) as! SegmentedHeader
// IMPORTANT >>>>MAKE SURE YOU SET THE DELEGATE or NONE OF THIS WILL WORK<<<<
segmentedHeader.delegate = self
// when the scene first appears there won't be any segments chosen so if you want a default one to show until the user picks one then set it here
// for eg. when the scene first appears the last segment will show
segmentedHeader.segmentedControl.selectedSegmentIndex = 2
header = segmentedHeader
}
return header!
}
// inside cellForRowAtIndexPath check the selectedSegmented class property to find out which segment was chosen
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TheCell, for: indexPath) as! TheCell
// selectedSegment is the class property that gets set inside trackSelectedIndex()
switch selectedSegment {
case 0:
// have the cell display something for the first segment
break
case 1:
// have the cell display something for the middle segment
break
case 2:
// have the cell display something for the last segment
break
default:
break
}
return cell
}
// whenever a segment is selected, this delegate function will get passed the segment's index. It runs a switch statement on theSelectedIndex argument/parameter. Based on that result it will set the selectedIndex class property to match the value from theSelectedIndex argument/parameter
func trackSelectedIndex(_ theSelectedIndex: Int) {
switch theSelectedIndex {
case 0: // this means the first segment was chosen
// set the selectedSegment class property so you can use it inside cellForRowAtIndexPath
selectedSegment = 0
print("the selected segment is: \(theSelectedIndex)")
break
case 1: // this means the middle segment was chosen
selectedSegment = 1
print("the selected segment is: \(theSelectedIndex)")
break
case 2: // this means the last segment was chosen
selectedSegment = 2
print("the selected segment is: \(theSelectedIndex)")
break
default:
break
}
}
When the header is retrieved for the collection view in viewForSupplementaryElementOfKind, you can store a weak reference to it in MyProfile.
class MyProfile: UICollectionViewController {
...
...
weak var header: MyProfileHeader?
...
...
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
header = collectionView.dequeueReusableSupplementaryViewOfKind(UICollectionElementKindSectionHeader, withReuseIdentifier: "header", forIndexPath: indexPath) as! MyProfileHeader
return header
}
You can then access this from any other function in your UICollectionViewController.
Note that numberOfItemsInSection and cellForItemAtIndexPath can be called before the header has been created in viewForSupplementaryElementOfKind, so when you access it in numberOfItemsInSection, cellForItemAtIndexPath, or anywhere else you should check for null and then assume that the segmented control is on the default value (as it will be since this is the first time the view is being displayed). Something like
let selectedSegmentIndex = header?.userContent.selectedSegmentIndex ?? 0 //0 is the default value here
Related
I implement a simple drag and drop sample.
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var collectionView: UICollectionView?
var colors: [UIColor] = [
.link,
.systemGreen,
.systemBlue,
.red,
.systemOrange,
.black,
.systemPurple,
.systemYellow,
.systemPink,
.link,
.systemGreen,
.systemBlue,
.red,
.systemOrange,
.black,
.systemPurple,
.systemYellow,
.systemPink
]
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.itemSize = CGSize(width: view.frame.size.width/3.2, height: view.frame.size.width/3.2)
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
//collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
let customCollectionViewCellNib = CustomCollectionViewCell.getUINib()
collectionView?.register(customCollectionViewCellNib, forCellWithReuseIdentifier: "cell")
collectionView?.delegate = self
collectionView?.dataSource = self
collectionView?.backgroundColor = .white
view.addSubview(collectionView!)
let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture))
collectionView?.addGestureRecognizer(gesture)
}
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
guard let collectionView = collectionView else {
return
}
switch gesture.state {
case .began:
guard let targetIndexPath = collectionView.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
return
}
collectionView.beginInteractiveMovementForItem(at: targetIndexPath)
case .changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: collectionView))
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = view.bounds
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = colors[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.size.width/3.2, height: view.frame.size.width/3.2)
}
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = colors.remove(at: sourceIndexPath.row)
colors.insert(item, at: destinationIndexPath.row)
}
}
However, I notice that, if my UICollectionViewCell is created with XIB, it will randomly exhibit flickering behaviour, during drag and drop.
The CustomCollectionViewCell is a pretty straightforward code.
CustomCollectionViewCell.swift
import UIKit
extension UIView {
static func instanceFromNib() -> Self {
return getUINib().instantiate(withOwner: self, options: nil)[0] as! Self
}
static func getUINib() -> UINib {
return UINib(nibName: String(describing: self), bundle: nil)
}
}
class CustomCollectionViewCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
Flickering
By using the following code
let customCollectionViewCellNib = CustomCollectionViewCell.getUINib()
collectionView?.register(customCollectionViewCellNib, forCellWithReuseIdentifier: "cell")
It will have the following random flickering behaviour - https://youtu.be/CbcUAHlRJKI
No flickering
However, if the following code is used instead
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
Things work fine. There are no flickering behaviour - https://youtu.be/QkV2HlIrXK8
May I know why it is so? How can I avoid the flickering behaviour, when my custom UICollectionView is created from XIB?
Please note that, the flickering behaviour doesn't happen all the time. It happens randomly. It is easier to reproduce the problem using real iPhone device, than simulator.
Here's the complete sample code - https://github.com/yccheok/xib-view-cell-cause-flickering
While we are rearranging cells in UICollectionView (gesture is active), it handles all of the cell movements for us (without having us to worry about changing dataSource while the rearrange is in flight).
At the end of this rearrange gesture, UICollectionView rightfully expects that we will reflect the change in our dataSource as well which you are doing correctly here.
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = colors.remove(at: sourceIndexPath.row)
colors.insert(item, at: destinationIndexPath.row)
}
Since UICollectionView expects a dataSource update from our side, it performs following steps -
Call our collectionView(_:, moveItemAt:, to:) implementation to provide us a chance to reflect the changes in dataSource.
Call our collectionView(_:, cellForItemAt:) implementation for the destinationIndexPath value from call #1, to re-create a new cell at that indexPath from scratch.
Okay, but why would it perform step 2 even if this is the correct cell to be at that indexPath?
It's because UICollectionView doesn't know for sure whether you actually made those dataSource changes or not. What happens if you don't make those changes? - now your dataSource & UI are out of sync.
In order to make sure that your dataSource changes are correctly reflected in the UI, it has to do this step.
Now when the cell is being re-created, you sometimes see the flicker. Let the UI reload the first time, put a breakpoint in the cellForItemAt: implementation at the first line and rearrange a cell. Right after rearrange completes, your program will pause at that breakpoint and you can see following on the screen.
Why does it not happen with UICollectionViewCell class (not XIB)?
It does (as noted by others) - it's less frequent. Using the above steps by putting a breakpoint, you can catch it in that state.
How to solve this?
Get a reference to the cell that's currently being dragged.
Return this instance from cellForItemAt: implementation.
var currentlyBeingDraggedCell: UICollectionViewCell?
var willRecreateCellAtDraggedIndexPath: Bool = false
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
guard let cv = collectionView else { return }
let location = gesture.location(in: cv)
switch gesture.state {
case .began:
guard let targetIndexPath = cv.indexPathForItem(at: location) else { return }
currentlyBeingDraggedCell = cv.cellForItem(at: targetIndexPath)
cv.beginInteractiveMovementForItem(at: targetIndexPath)
case .changed:
cv.updateInteractiveMovementTargetPosition(location)
case .ended:
willRecreateCellAtDraggedIndexPath = true
cv.endInteractiveMovement()
default:
cv.cancelInteractiveMovement()
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if willRecreateCellAtDraggedIndexPath,
let currentlyBeingDraggedCell = currentlyBeingDraggedCell {
self.willRecreateCellAtDraggedIndexPath = false
self.currentlyBeingDraggedCell = nil
return currentlyBeingDraggedCell
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.contentView.backgroundColor = colors[indexPath.item]
return cell
}
Will this solve the problem 100%?
NO. UICollectionView will still remove the cell from it's view hierarchy and ask us for a new cell - we are just providing it with an existing cell instance (that we know is going to be correct according to our own implementation).
You can still catch it in the state where it disappears from UI before appearing again. However this time there's almost no work to be done, so it will be significantly faster and you will see the flickering less often.
BONUS
iOS 15 seems to be working on similar problems via UICollectionView.reconfigureItems APIs. See an explanation in following Twitter thread.
Whether these improvements will land in rearrange or not, we will have to see.
Other Observations
Your UICollectionViewCell subclass' XIB looks like following
However it should look like following (1st one is missing contentView wrapper, you get this by default when you drag a Collection View Cell to the XIB from the View library OR create a UICollectionViewCell subclass with XIB).
And your implementation uses -
cell.backgroundColor = colors[indexPath.row]
You should use contentView to do all the UI customization, also note the indexPath.item(vs row) that better fits with cellForItemAt: terminology (There are no differences in these values though). cellForRowAt: & indexPath.row are more suited for UITableView instances.
cell.contentView.backgroundColor = colors[indexPath.item]
UPDATE
Should I use this workaround for my app in production?
NO.
As noted by OP in the comments below -
The proposed workaround has 2 shortcomings.
(1) Missing cell
(2) Wrong content cell.
This is clearly visible in https://www.youtube.com/watch?v=uDRgo0Jczuw Even if you perform explicit currentlyBeingDraggedCell.backgroundColor = colors[indexPath.item] within if block, wrong content cell issue is still there.
The flickering is caused by the cell being recreated at its new position. You can try holding to the cell.
(only the relevant code is shown)
// keeps a reference to the cell being dragged
private weak var draggedCell: UICollectionViewCell?
// the flag is set when the dragging completes
private var didInteractiveMovementEnd = false
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
// keep cell reference
draggedCell = collectionView.cellForItem(at: targetIndexPath)
collectionView.beginInteractiveMovementForItem(at: targetIndexPath)
case .ended:
// reuse the cell in `cellForItem`
didInteractiveMovementEnd = true
collectionView.performBatchUpdates {
collectionView.endInteractiveMovement()
} completion: { completed in
self.draggedCell = nil
self.didInteractiveMovementEnd = false
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// reuse the dragged cell
if didInteractiveMovementEnd, let draggedCell = draggedCell {
return draggedCell
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
...
}
I have a complicated situation and I need your help to figure out what should I do.
I have one prototype UIcollectionView, this prototype should be created 4 times for each style type.
I defined these style type as an enum:
enum Colors {
case black, blue, red, green
}
var color = Colors.black
Inside of CollectionViewCell I have also a tableView that has one prototype that contain a label. And there are four arrays that TableViews should be filled by these arrays:
var black = ["black1","black2","black3"]
var blue = ["blue1","blue2","blue3"]
var red = ["red1","red2","red3"]
var green = ["green1","green2","green3"]
now, I tried to make a connection between these TableViews and collectionViews
first for UICollectionView
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
switch indexPath.row {
case 0,1,2,3:
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "colors", for: indexPath) as? colorsCell {
switch indexPath.row {
case 1:
self.color = .black
case 2:
self.color = .blue
case 3:
self.color = .red
case 4:
self.color = .green
default:
break
}
return cell
}
default:
break
}
return UICollectionViewCell()
}
Then, for TableView
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.row {
case 0,1,2,3:
if let cell = tableView.dequeueReusableCell(withIdentifier: "colCell", for: indexPath) as? colCellDashboard {
switch self.color {
case .black:
cell.title.text = black[indexPath.row]
case .blue:
cell.title.text = blue[indexPath.row]
case .red:
cell.title.text = red[indexPath.row]
case .green:
cell.title.text = green[indexPath.row]
}
return cell
}
return UITableViewCell()
}
The result isn't good enough, the first three tableview in the first three collectionview were filled by blue array, and the last one is correct that filled with green array.
I will be appreciated if you can help me on this.
When the tableView is nested inside the collection you should use
class CollectionCell:UICollectionViewCell,UITableViewDelegate,UITableViewDataSource {
var tableArr = [String]() // table dataSource array
func configure(_ res:[String]) {
tableArr = arr
self.tableView.reloadData()
}
///////
here implement the cellForRowAt and numberOfRows for the nested tableView
}
Inside the vc that contains the collectionView declare the array like this
let arr = [["black1","black2","black3"] , ["blue1","blue2","blue3"] ,
["red1","red2","red3"] , ["green1","green2","green3"]]
Then inside cellForItemAt
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "colors", for: indexPath) as? colorsCell {
cell.configure(arr[indexPath.row])
Ahh, The issue with you code is that in your cellForItemAt delegate method of collection view you are assigning color to a class variable self.color. This will be overwritten for each call to the delegate. So when cellForRowAt delegate for tableview is called it will have the overwritten value of self.color (whatever will be the most updated value) and as a result you will see unexpected entry in your table.
Having tableviews inside collectionViews is kinda common problem. The simplest approach to solve it is:
-Put your datasouce for tableview inside the collectionViewCell.(create a variable in UICollectionViewCell subclass.)
-Assign value to your datasource for each tableView inside cellForItemAt delegate of CollectionView.
-Write all tableViewDelegates inside collectionViewCell and use the datasource varaible of CollectionViewCell to fill tableView.
The key is to underStand that tableView is inside CollectionViewCell and therefore, your CollectionViewCell is responsible to create the tableView.
The CollectionViewCell Structure given be #Sh_Khan looks fine(So not putting similiar code).
I read similar questions such as how to have multiple collection view in multiple table view cells and I connected my collection views cells and use identifier names for them but I don't know why I receive this Error:
* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'could not dequeue a view of kind: UICollectionElementKindCell with identifier extera_infoCollectionViewCell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'
* First throw call stack:
**Remember that I read Similar questions and the first table view cell with collection view working well and the problem is for second one **
here is my code for main view controller that has a table view and the table view has two cells
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView == fieldOfActivityCell().fieldofActivitiesCollectionView {
let fullfields : String = self.adv.resultValue[0].work_field!
let fullfieldsArr : [String] = fullfields.components(separatedBy: ",")
print(fullfieldsArr)
return fullfieldsArr.count
} else {
let extera_infofields : String = self.adv.resultValue[0].extera_info!
let extera_infofieldsArr : [String] = extera_infofields.components(separatedBy: ",")
print(extera_infofieldsArr)
return extera_infofieldsArr.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == fieldOfActivityCell().fieldofActivitiesCollectionView {
let fieldsCells = collectionView.dequeueReusableCell(withReuseIdentifier: "fieldOfActivityCollectionViewCell", for: indexPath) as! fieldOfActivityCollectionViewCell
let fullfields : String = self.adv.resultValue[0].work_field!
let fullfieldsArr : [String] = fullfields.components(separatedBy: ",")
fieldsCells.title.text = fullfieldsArr[indexPath.row]
return fieldsCells
}
else {
let extera_infoCells = collectionView.dequeueReusableCell(withReuseIdentifier: "extera_infoCollectionViewCell", for: indexPath) as! extera_infoCollectionViewCell
let extera_info : String = self.adv.resultValue[0].extera_info!
let extera_infoArr : [String] = extera_info.components(separatedBy: ",")
extera_infoCells.infoText.text = extera_infoArr[indexPath.row]
return extera_infoCells
}
}
and here is the table view codes in same view controller:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0{
let fieldCell = self.showAdvTableView.dequeueReusableCell(withIdentifier: "fieldOfActivityCell", for: indexPath) as! fieldOfActivityCell
return fieldCell
} else {
let fieldCell = self.showAdvTableView.dequeueReusableCell(withIdentifier: "extera_infoCell", for: indexPath) as! extera_infoCell
return fieldCell
}
here is table view first cell class:
class fieldOfActivityCell: UITableViewCell {
#IBOutlet weak var fieldofActivitiesCollectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
if let flowLayout = fieldofActivitiesCollectionView.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize.init(width: 1.0, height: 1.0) }
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
extension fieldOfActivityCell {
func setCollectionViewDataSourceDelegate
<D: UICollectionViewDelegate & UICollectionViewDataSource>
(_ dataSourceDelegate:D , forRow row : Int )
{
fieldofActivitiesCollectionView.delegate = dataSourceDelegate
fieldofActivitiesCollectionView.dataSource = dataSourceDelegate
fieldofActivitiesCollectionView.reloadData()
}
}
and here is the second tableview cell class:
#IBOutlet weak var extra_infoCollectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
if let flowLayout = extra_infoCollectionView.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize.init(width: 1.0, height: 1.0) }
}
}
extension extera_infoCell {
func setCollectionViewDataSourceDelegate
<D: UICollectionViewDelegate & UICollectionViewDataSource>
(_ dataSourceDelegate:D , forRow row : Int )
{
extra_infoCollectionView.delegate = dataSourceDelegate
extra_infoCollectionView.dataSource = dataSourceDelegate
extra_infoCollectionView.reloadData()
}
}
First step: using Tags - you just need to use tag for them and use if else to choose which collection view has selected with tag so the answer is this :
if collectionView.tag == 1 {
do some thing//////
}else {
do some thing else}
and you should use this in both cellForRowAtIndexPath and numberOfRows methods you can use this for table view too
Second step: you have to change the name of 'collection view' that you are dequeueing inside the cellForRowAt method in CollectionView data source:
if collectionView.tag == 1 {
let cell = yourFirstCollectionView.dequeueReusableCell(...) as yourCell
....
return cell
} else {
let cell = yourSecondCollectionView.dequeueReusableCell(...) as yourCell
....
return cell
}
According to your error your reuse identifier doesn't match any cell in your storyboard. Click on your extera_info collectionView cell in interface builder. Select the attributes inspector tab. Under reuse identifier make sure you put in extera_infoCollectionViewCell
If you take the other tableview cell In different class , with NSObject features of storyboard it can help you , And it is easy to maintain .
Saeed's tag option above is likely the simplest answer, but found his description a little short so adding a more complete answer below for those who've never used tags before...
If abiding by MVC and placing collectionView dataSource methods inside the UITableView class (instead of inside the UITableViewCell classes), and wanting to avoid this " error:
Each Collection View you use will need its own dequeueReusableCell identifier:
In interface-builder, name all your identifiers for your collection view cells. CatPicCell & DogPicCell for instance.
In your CellForItemAt collectionView method, set up if-statements or switch statement such that each reuse identifier is set equal to the identifiers you created in interface-builder (step 1). If using switch/case, your value can be set to collectionView.tag. Tags can be numbered to identify each different collectionView. The tags are like turning your set of collectionViews into a dictionary or array, such that each collectionView gets its own unique key/index.
Go back into interface-builder, and go into your storyboard and select each collection view (each of which should be inside its own tableView cell). In Xcode's "attribute inspector" scroll down to the "View" section and 3 spaces down (Xcode 11, Swift 5) you'll see a field called "Tag". Assign an integer value to that collection view, and then repeat this process for each collection view which is going to be embedded in your UITableView cells.
Once you have all the collection views tagged with unique integers, you simply set your cases to the integers, and give each dequeueReusableCell identifier the same integer index as you provided in the storyboard.
Now when you tableView cell calls on the collectionView you've outletted in the TableViewCell classes, it will be able to acquire the proper dequeueReusable ID. You can put your data inside each switch case.
Voila, you now have ONE collectionView datasource set of required methods, but serving ALL of your collection views. EVEN BETTER, when someone expands the project and adds another collectionView it will be as easy as adding another case to the switch and identifier in the storyboard.
Example code could look like this:
// I need a switch statement which will set the correct (of the 3 collectionViews) dequeueReusable IDENTIFIER for the collectionView
switch collectionView.tag {
//if tableView is doing cell == 1, then "CatsCell"
//if ... cell == 3, then "DogsCell"
//if ... cell == 5, then "BirdsCell"
case 1:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CatsCell", for: indexPath) as! CatsCVCell
// put your required data here
return cell
case 3:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DogCell", for: indexPath) as! DogsCVCell
// example data
let dogs = dogController.fetch()
cell.name = dogs[indexPath.item].dogName
if let image = UIImage(data: groups[indexPath.item].image!) {
cell.image = image
}
return cell
case 5:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BirdCell", for: indexPath) as! BirdCVCell
// put data code here for birds collection view cells
return cell
default:
return UICollectionViewCell() // or write a fatalError()
}
note: you have two options for your default to the switch statement...
1. like above, a generic but empty cell instance
2. throw an error. The error should never throw bc you'll have all the cases, but an error could occur if someone else improves your code and add another collectionView but forgets to to add the switch case-- so make your error statement explain what's wrong precisely.
I am Trying to add a Tap Gesture Recognizer to the header of my UICollection view, but no matter what, I can't get the numberOfPostsViewTapped() function to fire off. I've been trying for hours, and have tried using other UI elements such as other views or labels in the header view, but nothing is helping. Some guidance would be much appreciated.
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader: // only checking header - no footer on this view
// use an external class for the header UICollectionViewCell in order to set outlets on a non-reusable cell
// if you try to set outlets on a reusable cell, such as a header, it will fail
let headerView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "Header", forIndexPath: indexPath) as! ProfileCollectionViewHeader
// dynamically set user profile information
headerView.usernameTextLabel.text = user?.name
headerView.numberOfPostsTextLabel.text = user?.numberOfPosts != nil ? "\(user!.numberOfPosts!)" : "0"
let numberOfPostsViewSelector : Selector = #selector(self.numberOfPostsViewTapped)
let viewPostsViewGesture = UITapGestureRecognizer(target: self, action: numberOfPostsViewSelector)
viewPostsViewGesture.numberOfTapsRequired = 1
viewPostsViewGesture.delaysTouchesBegan = true
headerView.numberOfPostsTextLabel.userInteractionEnabled = true;
headerView.numberOfPostsTextLabel.addGestureRecognizer(viewPostsViewGesture)
return headerView
default:
assert(false, "Unexpected element kind")
}
}
func numberOfPostsViewTapped(sender: UITapGestureRecognizer){
print("HErE")
}
Check your frames. Is your UICollectionView within its frame?
If a view is out of its frame, and doesn't clip to its bounds, it will show the view's contents, though user interaction will not work with it.
I think you need to set userInteractionEnabled to true to your headerView so that the tapping would reach your label which is a child of your headerView.
headerView.userInteractionEnabled = true
Made few changes to your code and this worked for me
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
guard
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "AddLinksReusableView", for: indexPath) as? AddLinksReusableView else {
fatalError("Invalid view type")
}
let numberOfPostsViewSelector : Selector = #selector(self.imgVuTapped)
let viewPostsViewGesture = UITapGestureRecognizer(target: self, action: numberOfPostsViewSelector)
headerView.profileImg.isUserInteractionEnabled = true
viewPostsViewGesture.numberOfTapsRequired = 1
viewPostsViewGesture.delaysTouchesBegan = true
headerView.profileImg.addGestureRecognizer(viewPostsViewGesture)
return headerView
default:
assert(false, "Invalid element type")
}
}
#objc func imgVuTapped (sender: UITapGestureRecognizer){
print("HErE, Its working")
}
Happy Codding
I created a collection view controller from story board, and set its custom class to ItemCollectionVC, the custom class of its cell to ItemCell, and set its reuse identifier to Cell
Here's my ItemCollectionVC class:
import UIKit
private let reuseIdentifier = "Cell"
class ItemCollectionVC: UICollectionViewController {
var dataSourceItems: [Items] = []
var counterBuildItems: [Items] {
let weaponItemArray = WeaponItems.weaponItems as [Items]
let defenseItemArray = DefenseItems.defenseItems as [Items]
return weaponItemArray + defenseItemArray
}
var freeBuildItems = WeaponItems.weaponItems as [Items]
var captureKrakenItems: [Items] {
let weaponItemArray = WeaponItems.weaponItems as [Items]
let abilityItemArray = AbilityItems.abilityItems as [Items]
return weaponItemArray + abilityItemArray
}
override func viewDidAppear(_ animated: Bool) {
switch self.presentingViewController!.title! {
case "CounterBuildVC":
dataSourceItems = counterBuildItems
case "FreeBuildVC":
dataSourceItems = freeBuildItems
case "CaptureKrakenVC":
dataSourceItems = captureKrakenItems
default:
break
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSourceItems.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ItemCell
cell.cellImage.image = dataSourceItems[indexPath.row].image
print(dataSourceItems.count)
return cell
}
}
When the collection view controller is presented, it's empty, what could cause the problem?
One of three things caused this problem, pretty much every time I have encountered it, in a TableView or CollectionView:
1) Your ViewController is not the dataSource of your UICollectionView
2) numberOfRows or numberOfSections method returns 0
3) The height of your cell is 0, either due to constraint problems, or a heightForCell method being not/improperly implemented.
It's impossible to say which of these is your problem, and it's always possible that you've encountered something strange. Make certain that none of these is your problems, before exploring less likely options.
If you are pretty sure that the dataSource of the collectionView is connected to the viewController (it should be by default), then you should reloadData() because the collectionView reading from dataSourceItems. To understand the case, add a break point in cellForItemAt and add another one in viewDidAppear and check which one is called first?
override func viewDidAppear(_ animated: Bool) {
switch self.presentingViewController!.title! {
case "CounterBuildVC":
dataSourceItems = counterBuildItems
case "FreeBuildVC":
dataSourceItems = freeBuildItems
case "CaptureKrakenVC":
dataSourceItems = captureKrakenItems
default:
break
}
collectionView.reloadData()
}
Hope that helped.
Some things that you could easily miss if you use storyboard
1) Don't forget that content in cell must have connected Top, Bottom constraint and
content view must have height, by this cell will know to set height for cell. If you don't have these, cell height will be 0, and function cellForItemAt will never get called.
2) You can use cell layout to set dynamic cell and height for cell if you use this function:
func collectionView(_ collectionView: UICollectionView,layout collectionViewLayout:
UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) ->
CGSize {return CGSize(width: 20.00, height: 20.00)}
I fixed my problem by initializing UICollectionView properly like the following:
fileprivate let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: UICollectionViewFlowLayout.init())
Check if you get the right number of sections in the numberOfItemsInSection method for the collection view.
If you are adding a flow layout to the collection view, remove the flow layout, and check if the collection view cells show now.
If they do, just adjust your collection view flow layout code it should look like this
let _flowLayout = UICollectionViewFlowLayout()
_flowLayout.sectionInset = UIEdgeInsets(top:0, left: 0, bottom: 0, right: 0)
_flowLayout.scrollDirection = .vertical
yourCollectionView.collectionViewLayout = _flowLayout
you can set the inset to fit your use.
You have to add your code in viewWillAppear, then it will work properly.
/*
override func viewDidAppear(_ animated: Bool) {
switch self.presentingViewController!.title! {
case "CounterBuildVC":
dataSourceItems = counterBuildItems
case "FreeBuildVC":
dataSourceItems = freeBuildItems
case "CaptureKrakenVC":
dataSourceItems = captureKrakenItems
default:
break
}
}
Like this :-
override func viewWillAppear(_ animated: Bool) {
switch self.presentingViewController!.title! {
case "CounterBuildVC":
dataSourceItems = counterBuildItems
case "FreeBuildVC":
dataSourceItems = freeBuildItems
case "CaptureKrakenVC":
dataSourceItems = captureKrakenItems
default:
break
}
}
*/
In my case there was a problem with the collection view contentInset, try adding below code in your collection view sub class.
override func layoutSubviews() {
super.layoutSubviews()
self.contentInset = UIEdgeInsetsMake(0, 0, 0, 0)
}
override func reloadData() {
DispatchQueue.main.async {
super.reloadData()
}
}