I learned the UICollectionView inside UITableViewCell from this tutorial.
It works perfectly only when all of the collection views have the same numberOfItemsInSection, so it can scroll and won't cause Index out of range error, but if the collection view cells numberOfItemsInSection are different, when I scroll the collection view it crashes due to Index out of range.
I found the reason that when I scroll the tableview, the collection view cell index path updated according to the bottom one, but the collection view I scrolled is top one so it does't remember the numberOfItemsInSection, so it will crash due to Index out of range.
ps: it is scrollable, but only when that table cell at the bottom, because its numberOfItemsInSection is correspond to that cell and won't cause Index out of range.
This is the numberOfItemsInSection of My CollectionView, i actually have two collection view but i think this is not the problem
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
var numberOfItemsInSection: Int = 0
if collectionView == self.collectionview_categorymenu {
let categories = self.json_menucategory["menu_categories"].arrayValue
numberOfItemsInSection = categories.count
} else {
let menuitems = self.json_menucategory["menu_items"].arrayValue
let item_data = menuitems[self.itemimages_index].dictionaryValue
let menu_item_images = item_data["menu_item_images"]?.arrayValue
numberOfItemsInSection = (menu_item_images?.count)!
print(numberOfItemsInSection)
}
return numberOfItemsInSection
}
This is the cellForItemAt indexPath of my CollectionView:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == self.collectionview_categorymenu {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CategoryMenuCell", for: indexPath) as! CategoryMenuCell
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ItemImagesCell", for: indexPath) as! ItemImagesCell
let menuitems = self.json_menucategory["menu_items"].arrayValue
let item_data = menuitems[self.itemimages_index].dictionaryValue
let menu_item_images = item_data["menu_item_images"]?.arrayValue
print(menu_item_images!)
let itemimage_data = menu_item_images?[indexPath.item].dictionaryValue
print("===\(indexPath.item)===")
let itemimage_path = itemimage_data?["image"]?.stringValue
let itemimage_detail = itemimage_data?["detail"]?.stringValue
if let itemimage = cell.imageview_itemimage {
let image_url = itemimage_path?.decodeUrl()
URLSession.shared.dataTask(with: NSURL(string: image_url!) as! URL, completionHandler: { (data, response, error) -> Void in
if error != nil {
print("error")
return
}
DispatchQueue.main.async(execute: { () -> Void
in
itemimage.image = UIImage(data: data!)!
})
}).resume()
}
if let description = cell.textview_description {
description.text = itemimage_detail
}
if let view = cell.view_description {
view.isHidden = true
}
return cell
}
}
The problem is probaby that you are returning the same number every time for numberOfItemsInSection what you want to do is return the number of items in each array. From the link you sent they do this:
func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return model[collectionView.tag].count
}
The other possibility is that you are returning the wrong array when cellForItemAtIndexPath please check that you are using the tag attribute correctly
So they get the count of each array within the model. Without seeing any of your code its very difficult to actually see where your error is coming from.
Related
I have a Custom UITableViewCell with a UICollectionView in it.
I have the UICollectionView pinned to each side in its XIB file.
Within some of my cells, the content may carry down but with my current setup for dynamic heights, I am only seeing the top portion. I am adding images to my UICollectionView so in one cell there may be 20 while another may just be 5. Right now each row has the same height when some should be different.
To note, the UICollectionView in the cell will not scroll.
Here is what I am trying in my View Controller:
// Here is where I am getting the arr data, which is the folders
// that contains images.
func getDataForSections() {
let storageReference = Storage.storage()
let ref = storageReference.reference().child("abc/xyz/")
ref.listAll { (result, error) in
if let error = error {
// ...
}
for prefix in result.prefixes {
self.arr.append(prefix)
}
self.myTableView.reloadData()
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return arr.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = arr[indexPath.section]
let cell = tableView.dequeueReusableCell(withIdentifier: "collectionCell", for: indexPath) as! collectionCell
cell.getImagesFromData(data: data)
cell.frame = tableView.bounds
cell.layoutIfNeeded()
cell.collectionView.reloadData()
cell.collectionView.heightAnchor.constraint(equalToConstant: cell.collectionView.collectionViewLayout.collectionViewContentSize.height).isActive = true
cell.layoutIfNeeded()
return cell
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
// How Do I get the Custom size here? Or in heightForRowAt?
return 500.0
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}
/**
#param numberOfCellsInCollectionView the number of cells of your current collectioview
*/
func calculateHeight(numberOfCellsInCollectionView: Int) -> CGFloat {
let imageHeight: CGFloat = 40.0
let spaceBetweenRows: CGFloat = 20.0 /*Change as you please based on the space you put between your cells (if any) */
let rows: CGFloat = CGFloat(calculateRows(numberOfCellsInCollectionView: numberOfCellsInCollectionView))
/**
1) (rows*imageHeight) calculate the total space occupied by the pictures
2) (rows+2) assuming you want to put a little space between the top and the bottom of the collectionview i used +2. If you don't want to remove the +2
3) ((rows+2)*spaceBetweenRows) total space occupied by the spaces.
**/
let height = (rows*imageHeight)+((rows+2)*spaceBetweenRows)
return height
}
//Calculate the rows
func calculateRows(numberOfCellsInCollectionView: Int) -> Int {
let result = numberOfCellsInCollectionView/6 //Dividing the number of cells for the cells for row
let rest = numberOfCellsInCollectionView%6 //Calculating the module
if rest == 0 {
//If the rest is 0 (ie: you divide 18/6), then you get the result of the division (18/6 = 3)
return result
} else {
//If the rest is > 0 (ie: you divide 17/6), then you get the result of the division + 1 (17/6 = 3+1 = 4) so there's space for the last item
return result+1
}
}
Here is what I am trying in my Custom Cell that has a Collection View:
func getImagesFromData(prefix: String) {
let storageReference = Storage.storage()
let ref = storageReference.reference().child("abc/xyz/\(prefix)")
ref.listAll { (result, error) in
if let error = error {
// ...
}
self.folderImages.removeAll()
for item in result.items {
if !self.folderImages.contains(item) {
self.folderImages.append(item)
}
}
// Here is where I need to store (or retain) the count
// for each section and then calculate the height or pass
// this data back to the View Controller.
// But with Firebase I am not sure how to return this or
// use a completion.
// reload collection data
self.myCollectionView.reloadData()
}
}
extension customCollectionCell: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return folderImages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "imageCell", for: indexPath) as! imageCell
let theImage = folderImages[indexPath.item]
cell.imageView.sd_setImage(with: theImage, placeholderImage: UIImage(named: "blah")) { (image, error, cacheType, ref) in
if error != nil {
cell.imageView.image = UIImage(named: "blah")
}
}
return cell
}
}
Another Example:
Here instead of using a UITableView with the UICollectionViewCell, I made a UICollectionView with the UICollectionViewCell...
I also use a variation of the method within the cell in the UIViewController to get the count.
In this example, I can see the values prior to the return. I just need to know how to get that value within the return as the height.
Here's what I tried:
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let prefix = arr[indexPath.section]
getImageCountFromPrefix(prefix: prefix.name, completion: { (count, success) in
if success {
self.h = self.calculateHeight(numberOfCellsInCollectionView: count)
// self.h has a value!!! How do I get it in the return?
}
})
return CGSize(width: collectionView.bounds.size.width, height: self.h)
}
Create a function to determine the height of the pictures inside the cells then only use the heightForRowAtIndexPath function like this :
private func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat{
/* Here I'll just assume the height of the picture is 30. You'll have to put here your function and return the dimension. */
return 30.0
}
Note that you may want to add a constant value to the returned value to make the cell's appearance more clear. I usually add 70.0
Oh, and I don't know about images, but when you deal with text you have to call these as well in the cellForRowAt.
cell.textLabel?.sizeToFit()
cell.textLabel?.numberOfLines = 0
Here's how to calculate the height of your collection view
/**
#param numberOfCellsInCollectionView the number of cells of your current collectioview
*/
func calculateHeight(numberOfCellsInCollectionView: Int) -> CGFloat{
let imageHeight: CGFloat = 40.0
let spaceBetweenRows: CGFloat = 5.0 /*Change as you please based on the space you put between your cells (if any) */
let rows: CGFloat = CGFloat(calculateRows(numberOfCellsInCollectionView: numberOfCellsInCollectionView))
/**
1) (rows*imageHeight) calculate the total space occupied by the pictures
2) (rows+2) assuming you want to put a little space between the top and the bottom of the collectionview i used +2. If you don't want to remove the +2
3) ((rows+2)*spaceBetweenRows) total space occupied by the spaces.
**/
let height = (rows*imageHeight)+((rows+2)*spaceBetweenRows)
return height
}
//Calculate the rows
func calculateRows(numberOfCellsInCollectionView: Int) -> Int{
let result = numberOfCellsInCollectionView/6 //Dividing the number of cells for the cells for row
let rest = numberOfCellsInCollectionView%6 //Calculating the module
if rest == 0 {
//If the rest is 0 (ie: you divide 18/6), then you get the result of the division (18/6 = 3)
return result
} else {
//If the rest is > 0 (ie: you divide 17/6), then you get the result of the division + 1 (17/6 = 3+1 = 4) so there's space for the last item
return result+1
}
}
I'm currently making a screen that has an UITableView with many sections that have the content of cells is UICollectionView. Now I'm saving the selected indexPath of the collection into an array then save to UserDefaults (because the requirement is showing all cells has selected before when reopening view controller).
But I have the issues is when I reopen view controller all items in all sections with the same selected indexPath show the same state.
I know it occurs because I just save the only indexPath of the selected item without the section of UITableview which is holding the collection view. But I don't know how to check the sections. Can someone please help me to solve this problem? Thank in advance.
I'm following this solution How do I got Multiple Selections in UICollection View using Swift 4
And here is what I do in my code:
var usrDefault = UserDefaults.standard
var encodedData: Data?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
if let act = usrDefault.data(forKey: "selected") {
let outData = NSKeyedUnarchiver.unarchiveObject(with: act)
arrSelectedIndex = outData as! [IndexPath]
}else {
arrSelectedData = []
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let optionItemCell = collectionView.dequeueReusableCell(withReuseIdentifier: "optionCell", for: indexPath) as! SDFilterCollectionCell
let title = itemFilter[indexPath.section].value[indexPath.item].option_name
if arrSelectedIndex.contains(indexPath) {
optionItemCell.filterSelectionComponent?.bind(title: title!, style: .select)
optionItemCell.backgroundColor = UIColor(hexaString: SDDSColor.color_red_50.rawValue)
optionItemCell.layer.borderColor = UIColor(hexaString: SDDSColor.color_red_300.rawValue).cgColor
}else {
optionItemCell.backgroundColor = UIColor(hexaString: SDDSColor.color_white.rawValue)
optionItemCell.layer.borderColor = UIColor(hexaString: SDDSColor.color_grey_100.rawValue).cgColor
optionItemCell.filterSelectionComponent?.bind(title: title!, style: .unselect)
}
optionItemCell.layoutSubviews()
return optionItemCell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let strData = itemFilter[indexPath.section].value[indexPath.item]
let cell = collectionView.cellForItem(at: indexPath) as? SDFilterCollectionCell
cell?.filterSelectionComponent?.bind(title: strData.option_name!, style: .select)
cell?.backgroundColor = UIColor(hexaString: SDDSColor.color_red_50.rawValue)
cell?.layer.borderColor = UIColor(hexaString: SDDSColor.color_red_300.rawValue).cgColor
if arrSelectedIndex.contains(indexPath) {
arrSelectedIndex = arrSelectedIndex.filter{($0 != indexPath)}
arrSelectedData = arrSelectedData.filter{($0 != strData)}
}else {
arrSelectedIndex.append(indexPath)
arrSelectedData.append(strData)
encodedData = NSKeyedArchiver.archivedData(withRootObject: arrSelectedIndex)
usrDefault.set(encodedData, forKey: "selected")
}
if let delegate = delegate {
if itemFilter[indexPath.section].search_key.count > 0 {
if (strData.option_id != "") {
input.add(strData.option_id!)
let output = input.componentsJoined(by: ",")
data["search_key"] = itemFilter[indexPath.section].search_key.count > 0 ? itemFilter[indexPath.section].search_key : strData.search_key;
data["option_id"] = output
}
}else {
data["search_key"] = itemFilter[indexPath.section].search_key.count > 0 ? itemFilter[indexPath.section].search_key : strData.search_key;
data["option_id"] = strData.option_id
}
delegate.filterTableCellDidSelectItem(item: data, indexPath: indexPath)
}
}
This will only work based on the assumption that both your parent table view and child collection views both are not using multiple sections with multiple rows and you only need to store one value for each to represent where an item is located in each respective view.
If I am understanding correctly, you have a collection view for each table view cell. You are storing the selection of each collection view, but you need to also know the position of the collection view in the parent table? A way to do this would be to add a property to your UICollectionView class or use the tag property and set it corresponding section it is positioned in the parent table. Then when you save the selected IndexPath, you can set the section to be that collection view's property you created(or tag in the example) so that each selected indexPath.section represents the table view section, and the indexPath.row represents the collection view's row.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//...
let collectionView = UICollectionView()
collectionView.tag = indexPath.section
//...
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
indexPath.section = collectionView.tag
let strData = itemFilter[indexPath.section].value[indexPath.item]
//...
}
Basically each selected index path you save will correspond to the following:
indexPath.section = table view section
indexPath.row = collection view row
IndexPath(row: 5, section: 9) would correlate to:
--table view cell at IndexPath(row: 0, section: 9) .
----collection view cell at IndexPath(row: 5, section: 0)
Edit: This is how you can use the saved index paths in your current code
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//...
let tempIndexPath = IndexPath(row: indexPath.row, section: collectionView.tag)
if arrSelectedIndex.contains(tempIndexPath) {
//...
} else {
//...
}
//...
}
Your statement arrSelectedIndex.contains(indexPath) in the cellForItemAt method is not correct.
Each time a UICollectionView in a UITableView's section is loaded, this will called the cellForItemAt for ALL cells.
Here is the error :
In your GIF example the first cell is selected in the first collectionView, you will store (0, 0) in the array.
But when the second collectionView will loads its cells, it will check if the indexPath (0, 0) is contained into your array. It is the case, so the backgroundColor will be selected.
This error will be reproduced on every collectionView stored in your tableView sections.
You should probably also store the sectionIndex of your UITableView into your array of IndexPath.
I have single array which contain game`s informations. My Json has 12 items in a page. I did created 4 sections which has 3 rows. It is repeating first 3 items of array in every sections.
Screenshot from app
I want to use like that;
Total Items = 12
Section = 1 2 3
Section = 4 5 6
Section = 7 8 9
Section = 10 11 12
How can I do that ? Thanks in advance :)
func numberOfSections(in collectionView: UICollectionView) -> Int {
return id.count / 3
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "lastAddedCell", for: indexPath) as! lastAddedCell
cell.gameName.text = name[indexPath.row]
cell.gameImage.sd_setImage(with: URL(string:resimUrl[indexPath.row]))
return cell
}
I don't think it's such a good idea. I would rather create the section separately by making a section manager than making them from the same array. But, if you want to do it the way you are doing it right now. Here is an easy fix:
func numberOfSections(in collectionView: UICollectionView) -> Int {
return id.count / 3
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "lastAddedCell", for: indexPath) as! lastAddedCell
let index = indexPath.row + (indexPath.section * 3) // The index is then based on the section which is being presented
cell.gameName.text = name[index]
cell.gameImage.sd_setImage(with: URL(string:resimUrl[indexPath.row]))
return cell
}
Consider this case below and implement it,
var array = [1,2,3,4,5,6,7,8,9,10,11,12]
//Statcially you can slice them like this
var arr2 = array[0...2] {
didSet {
//reload your collection view
}
}
var arr3 = array[3...5]
var arr4 = array[6...8]
var arr5 = array[9...array.count - 1]
Above you manually sliced the dataSource for each UICollectionView but the problem is this is really risky and eventually can lead to an Index Out of Range crash, so we dynamically slice the array thru looping in it using the index of each element in range of +3 indexes to append to the new UICollectionView data source.
// loop thru the main array and slice it based on indexes
for(index, number) in array.enumerated() {
if 0...2 ~= index { // if in range
arr2.append(number)
} else
if index <= 5 {
arr3.append(number)
} else
if index <= 8 {
arr4.append(number)
} else
if index <= 11 {
arr5.append(number)
}
}
Finally : in your numberOfItemsInSection check the UICollectionView and set return its data source like,
if collectionView = myMainCollectionView {
return arr3.count
}
And goes the same for the cellForItemAt
Heads Up : make sure your dataSource arrays are empty initially,
let arr2: [Int] = [] {
didSet{
//reload your collectionView
}
}
I have working uicollectionview codes with CustomCollectionViewLayout , and inside have a lot of small cells but user cannot see them without zoom. Also all cells selectable.
I want to add my collection view inside zoom feature !
My clear codes under below.
class CustomCollectionViewController: UICollectionViewController {
var items = [Item]()
override func viewDidLoad() {
super.viewDidLoad()
customCollectionViewLayout.delegate = self
getDataFromServer()
}
func getDataFromServer() {
HttpManager.getRequest(url, parameter: .None) { [weak self] (responseData, errorMessage) -> () in
guard let strongSelf = self else { return }
guard let responseData = responseData else {
print("Get request error \(errorMessage)")
return
}
guard let customCollectionViewLayout = strongSelf.collectionView?.collectionViewLayout as? CustomCollectionViewLayout else { return }
strongSelf.items = responseData
customCollectionViewLayout.dataSourceDidUpdate = true
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
strongSelf.collectionView!.reloadData()
})
}
}
}
extension CustomCollectionViewController {
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return items.count
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items[section].services.count + 1
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CustomCollectionViewCell
cell.label.text = items[indexPath.section].base
return cell
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath cellForItemAtIndexPath: NSIndexPath) {
print(items[cellForItemAtIndexPath.section].base)
}
}
Also my UICollectionView layout properties under below you can see there i selected maxZoom 4 but doesnt have any action !
Thank you !
You don't zoom a collection like you'd zoom a simple scroll view. Instead you should add a pinch gesture (or some other zoom mechanism) and use it to change the layout so your grid displays a different number of items in the visible part of the collection. This is basically changing the number of columns and thus the item size (cell size). When you update the layout the collection can animate between the different sizes, though it's highly unlikely you want a smooth zoom, you want it to go direct from N columns to N-1 columns in a step.
I think what you're asking for looks like what is done in the WWDC1012 video entitled Advanced Collection Views and Building Custom Layouts (demo starts at 20:20) https://www.youtube.com/watch?v=8vB2TMS2uhE
You basically have to add pinchGesture to you UICollectionView, then pass the pinch properties (scale, center) to the UICollectionViewLayout (which is a subclass of UICollectionViewFlowLayout), your layout will then perform the transformations needed to zoom on the desired cell.
I would like to set the numberOfItemsInSection of my collectionView at runtime. I will be changing the value programmatically at runtime quite often and would like to know how.
I have an array of images to display in my UICollectionView (1 image per UICollectionViewCell), and the user can change the category of the images to display, which will also change the number of images to display. When the view loads, the numberOfItemsInSection is set to the count of the allClothingStickers array. But number this is too high. The array that does get displayed is the clothingStickersToDisplay array which is a subset of the allClothingStickers array.
There is this error after some scrolling:
fatal error: Array index out of range
This is because the number of items has become smaller, but the UICollectionView numberOfItemsInSectionproperty has not changed to be a smaller number.
I have this function that sets the number of cells in the UICollectionView before runtime.
func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return self.numberOfItems
}
This function to set the stickersToDisplay array (and I want to update the numberOfItemsInSection property here):
func setStickersToDisplay(category: String) {
clothingStickersToDisplay.removeAll()
for item in self.allClothingStickers {
let itemCategory = item.object["category"] as! String
if itemCategory == category {
clothingStickersToDisplay.append(item)
}
}
self.numberOfItems = clothingStickersToDisplay.count
}
This is the function that returns the cell to display:
func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath)
-> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(
identifier,forIndexPath:indexPath) as! CustomCell
let sticker = clothingStickersToDisplay[indexPath.row]
let name = sticker.object["category"] as! String
var imageView: MMImageView =
createIconImageView(sticker.image, name: name)
cell.setImageV(imageView)
return cell
}
EDIT: Oh yeah, and I need to reload the UICollectionView with the new clothingStickersToDisplay at the same place that I update it's numberOfItemsInSection
I think what you should do is to clothingStickersToDisplay a global array declaration.
Instead of using that self.numberOfItems = clothingStickersToDisplay.count
Change this function to
func setStickersToDisplay(category: String) {
clothingStickersToDisplay.removeAll()
for item in self.allClothingStickers {
let itemCategory = item.object["category"] as! String
if itemCategory == category {
clothingStickersToDisplay.append(item) //<---- this is good you have the data into the data Structure
// ----> just reload the collectionView
}
}
}
In the numberOfItemsInSection()
return self.clothingStickersToDisplay.count