I've been trying to figure out what the issue is in this code for it to throw an index out of range error. However, I am unable to understand where the issue is.
Here is the code
import UIKit
import Alamofire
class MenuViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let cellId = "cellId"
let headerId = "headerId"
var itemCategories: [MenuItemCategory]?
var menuItem: [MenuItem]?
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = .white
navigationItem.titleView = UIImageView(image: #imageLiteral(resourceName: "mooyahLabelLogo"))
collectionView?.register(MenuViewControllerCell.self, forCellWithReuseIdentifier: cellId)
collectionView?.register(MenuViewControllerHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: headerId)
MenuItemCategory.fetchMenuItems { (itemCategories) in
self.itemCategories = itemCategories
self.collectionView?.reloadData()
}
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
if let count = itemCategories?.count {
print("Number of Sections: \(count)")
return count
}
return 0
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerId, for: indexPath) as! MenuViewControllerHeader
if let categoryName = itemCategories?[indexPath.section].name {
header.categoryNameLabel.text = categoryName
}
return header
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: view.frame.width, height: 44)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if let count = itemCategories?[section].items?.count {
print("Number of Items in Section: \(count)")
return count
}
return 0
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuViewControllerCell
if let category = itemCategories?[indexPath.item] {
print("Section: \(category.name!)")
if let itemsCount = category.items?.count {
for i in 0..<itemsCount {
print("Item: \(category.items?[i].name ?? "")")
cell.itemNameLabel.text = category.items?[i].name ?? ""
cell.itemDescriptionLabel.text = category.items?[i].desc ?? ""
if let price = category.items?[i].price {
cell.itemPriceLabel.text = "AED \(price)"
}
}
}
}
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: 85)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
Here is the debugger print where it shows that my number of sections are correct as well as the number of items in section is correct. I am not sure where the issue arises from?
Debugger screenshot
In override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
Shouldn't this line be
if let category = itemCategories?[indexPath.section] { .... }
Not
if let category = itemCategories?[indexPath.item] { .... }
I would suggest to use a guard to make sure that your items are available like this
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuViewControllerCell
guard let itemCategories = itemCategories, itemCategories.indices.contains(indexPath.item), let items = category.items else {
return cell
}
if let category = itemCategories[indexPath.item] {
print("Section: \(category.name!)")
if let itemsCount = items.count {
for i in 0..<itemsCount {
if items.indices.contains(i) {
print("Item: \(items[i].name ?? "")")
cell.itemNameLabel.text = items[i].name ?? ""
cell.itemDescriptionLabel.text = items[i].desc ?? ""
if let price = category.items?[i].price {
cell.itemPriceLabel.text = "AED \(price)"
}
}
}
}
}
return cell
}
Related
How to get the selected cells count and selected cells array from UICollectionView, I need to set the limit of selection. Below is my code which is not working. Please guide. Thanks
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
let indexPaths = collectionView.indexPathsForSelectedItems!
print(indexPaths)
if let selectedItems = collectionView.indexPathsForSelectedItems {
if selectedItems.count >= 3 {
collectionView.deselectItem(at: indexPath as IndexPath, animated: true)
return false
}
}
return true
}
Above code when written in didSelect method returns only selected cell index but here just skip to last line
return true
You can use the shouldSelectItemAt delegate method from the UICollectionViewDelegate to achieve that:
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return collectionView.indexPathsForSelectedItems?.count ?? 0 <= selectionLimit
}
A sample demo to demonstrate how it works:
class ViewController : UIViewController {
private var myCollectionView:UICollectionView?
private var selectionLimit = 4
override func viewDidLoad() {
super.viewDidLoad()
let view = UIView()
view.backgroundColor = .white
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 20, left: 10, bottom: 10, right: 10)
layout.itemSize = CGSize(width: UIScreen.main.bounds.width * 0.7, height: 60)
myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
myCollectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "MyCell")
myCollectionView?.backgroundColor = UIColor.white
myCollectionView?.allowsMultipleSelection = true
myCollectionView?.dataSource = self
myCollectionView?.delegate = self
view.addSubview(myCollectionView ?? UICollectionView())
self.view = view
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 20
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let myCell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath)
myCell.backgroundColor = UIColor.blue
return myCell
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Did select a cell at \(indexPath.row)")
let itemsLeftToSelect = selectionLimit - (collectionView.indexPathsForSelectedItems?.count ?? 0)
print("Have \(itemsLeftToSelect) items to select")
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
print("Did deselect a cell at \(indexPath.row)")
let itemsLeftToSelect = selectionLimit - (collectionView.indexPathsForSelectedItems?.count ?? 0)
print("Have \(itemsLeftToSelect) items to select")
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return collectionView.indexPathsForSelectedItems?.count ?? 0 < selectionLimit
}
}
When you reach the selection limit, shouldSelectItemAt will return false and didSelectItemAt function will not be called anymore. On the other hand if you click on the already selected cell, didDeselectItemAt will be called and you will be called, and you are able to select + 1 cell again, as the number of selected items will decrease with 1. See also the debug logs in the example.
A better approach is model based.
In your data model add a property
var isSelected = false
and re-/set it accordingly when a cell is de-/selected.
Then you can simply count the selected data source items – dataSource represents the data source array
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return dataSource.filter{$0.isSelected}.count < 3
}
Further there is no need to deselect a row in this method as you are preventing the user from selecting more than 2 rows.
I want to merge two different sections that uses two different cells respectively(cellA and cellB). Using the following code I could get a collection view with two sections.
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource{
#IBOutlet var testCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
self.testCollectionView.delegate = self
self.testCollectionView.dataSource = self
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 {
return 3
} else {
return 1
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "taskCell", for: indexPath) as! CellA
cell.layer.cornerRadius = 2
cell.layer.shadowColor = UIColor(red:0.82, green:0.82, blue:0.82, alpha:1.0).cgColor
cell.layer.shadowOffset = CGSize.zero
cell.layer.shadowOpacity = 1
cell.layer.shadowRadius = 4
cell.layer.masksToBounds = false
return cell
} else {
let cell2 = collectionView.dequeueReusableCell(withReuseIdentifier: "addNewCell", for: indexPath) as! CellB
return cell2
}
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = testCollectionView.bounds.width/2 - 30
let height = width
return CGSize(width: width, height: height)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(30, 20, 10, 20)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 20
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath.row)
}
}
And the output looks like this.
Now, I want the section 1(CellB) to be merged with section 0(CellA). Like this.
I couldn't find the solution to achieve this. Any help will be appreciated.
You can show both cell types in the same section with these changes:
Add properties for the number of aCells and bCells. This is better than putting the magic numbers 3 and 1 in your code because it documents what the numbers represent. If you ever need to change them, you can change them in one spot.
Change numberOfSections to return 1.
Change numberOfItemsInSection to return the sum of the number of A cells and the number of B cells: return aCells + bCells.
In cellForItemAt, compare indexPath.item to aCells to figure out when to switch over to B cells.
var aCells = 3
var bCells = 1
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return aCells + bCells
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item < aCells {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "taskCell", for: indexPath) as! CellA
cell.layer.cornerRadius = 2
cell.layer.shadowColor = UIColor(red:0.82, green:0.82, blue:0.82, alpha:1.0).cgColor
cell.layer.shadowOffset = CGSize.zero
cell.layer.shadowOpacity = 1
cell.layer.shadowRadius = 4
cell.layer.masksToBounds = false
return cell
} else {
let cell2 = collectionView.dequeueReusableCell(withReuseIdentifier: "addNewCell", for: indexPath) as! CellB
return cell2
}
}
Image 1:
I have a collection view and a table view in the same view controller. I want to update the data of my table view, based on the selected item in the collection view. So every time I click on any item of the collectionView, my tableView data should update.
My code:
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
for superSkill in skills!{
if superSkill.name == (skills?[indexPath.row].name)! {
childSkills = superSkill.skills!
}
}
DispatchQueue.main.async{
self.childSkillTableView.reloadData()
}
}
Is there any way i can achieve it
First thing is to make sure your childSkillTableView.dataSource = self and your superSkillsCollectionView.delegate = self
The second thing, there's no reason to use DispatchQueue.main.async({})
The third thing, though less important, instead of a for loop, you might use something like:
childSkills = skills?.first(where { superSkill in superSkill.name == (skills?[indexPath.row].name)! }
Though you should use some if let or guard let statements to check for optionals instead of force unwrapping.
extension PerformanceVC: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return skills.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "superSkillCell", for: indexPath) as! SuperSkillCell
cell.superSkillName.text = skills[indexPath.row].name
//loading image async
ImageAsyncLoader.loadImageAsync(url: (skills[indexPath.row].imageURL)!, imgView: cell.superSkillImage)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: skillCollections.frame.height * 0.9, height: skillCollections.frame.height) //use height whatever you wants.
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print((skills[indexPath.row].name)!)
for skill in skills{
if skill.name == (skills[indexPath.row].name)! {
childSkills = skill.skills!
}
}
self.subSkillTableView.reloadData()
}
}
extension PerformanceVC: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let openViewHeight: Int = 55
if (indexPath.row == selectedRowIndex.row && isExpanded == false){
isExpanded = true
return CGFloat(openViewHeight + 36 * (childSkills[indexPath.row].skills?.count)!)
} else if (indexPath.row == selectedRowIndex.row && isExpanded == true){
isExpanded = false
return CGFloat(openViewHeight)
} else {
isExpanded = false
return CGFloat(openViewHeight)
}
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return childSkills.count
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(childSkills[indexPath.row].name)
selectedRowIndex = indexPath
tableView.beginUpdates()
//let cell = tableView.dequeueReusableCell(withIdentifier: "SubSkillTableCell", for: indexPath) as! SubSkillTableCell
let cell = tableView.cellForRow(at: indexPath) as! SubSkillTableCell
for subview in cell.grandSkillStack.subviews {
subview.removeFromSuperview()
}
var grandSkillView: GrandChildSkillItem
grandChildSkills = (childSkills[indexPath.row].skills)!
for grandchildskill in grandChildSkills {
grandSkillView = GrandChildSkillItem(frame: CGRect(x: 0, y: 0, width: 300, height: 30))
grandSkillView.grandChildSkillNameLabel.text = grandchildskill.name
grandSkillView.grandChildSkillProgress.progress = Float(grandchildskill.percentage!)
cell.grandSkillStack.addArrangedSubview(grandSkillView)
}
tableView.endUpdates()
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
let cell = tableView.dequeueReusableCell(withIdentifier: "SubSkillTableCell", for: indexPath) as! SubSkillTableCell
cell.subSkillName.text = childSkills[indexPath.row].name
cell.subSkillProgress.progress = Float(childSkills[indexPath.row].percentage!)
if let uPoints = childSkills[indexPath.row].userPoints {
if let tPoints = childSkills[indexPath.row].totalPoints {
if let count = childSkills[indexPath.row].skills?.count {
cell.subSkillDetail.text = "\(uPoints)" + "/" + "\(tPoints)" + "XP \u{2022} " + "\(count)" + " subskills"
}
}
}
return cell
}
}
I have two different cell type classes and i'm trying to make the first indexpath with the class PayNowCell, have it remain displayed. While the other cells are of class CheckOutCell. Now the problem is in my func numberofitemsinsection. Currently i'm using return checkout.count but it missing the PayNowCell when view loads. If i make it return checkout.count+1 to always have the PayNowCell available; my program crashes giving me the error index out of bounds. The array checkout is a global var. Can someone explain why and provide a fix? Been stuck on this for a while. Code Below.
class CheckoutController: UICollectionViewCell, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
var inventoryTabController: InventoryTabController?
override init(frame: CGRect){
super.init(frame: frame)
setupViews()
NotificationCenter.default.addObserver(forName: .arrayValueChanged, object: nil, queue: OperationQueue.main) { [weak self] (notif) in
self?.collectionView.reloadData()
}
}
deinit {
NotificationCenter.default.removeObserver(self)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func loadItems() -> [Item]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: Item.ArchiveURL.path) as? [Item]
}
func saveItems() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(checkout, toFile: Item.ArchiveURL.path)
if !isSuccessfulSave {
print("Failed to save items...")
}
}
func addItem(item: Item) {
items.append(item)
collectionView.reloadData()
}
func editItem(item: Item, index: Int) {
items[index] = item
collectionView.reloadData()
}
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Identify which segue is occuring.
if segue.identifier == "ShowDetail" {
let itemDetailViewController = segue.destination as! AddItemController
// Get the cell that generated this segue.
if let selectedItemCell = sender as? InventoryCell {
let indexPath = collectionView.indexPath(for: selectedItemCell)!
let selectedItem = items[indexPath.row]
itemDetailViewController.item = selectedItem
}
}
else if segue.identifier == "AddItem" {
print("Adding new meal.")
}
}
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.dataSource = self
cv.delegate = self
cv.backgroundColor = UIColor.rgb(r: 247, g: 247, b: 247)
return cv
}()
let cellId = "cellId"
let paynow = "paynow"
func setupViews() {
backgroundColor = .brown
addSubview(collectionView)
addConstraintsWithFormat("H:|[v0]|", views: collectionView)
addConstraintsWithFormat("V:|[v0]|", views: collectionView)
collectionView.indicatorStyle = UIScrollViewIndicatorStyle.white
collectionView.register(PayNowCell.self, forCellWithReuseIdentifier: paynow)
collectionView.register(CheckoutCell.self, forCellWithReuseIdentifier: cellId)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return checkout.count //init number of cells
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "paynow", for: indexPath) as! PayNowCell //init cells
return cell
}else{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! CheckoutCell //init cells
print("Printing this \(checkout.count)")
cell.item = checkout[indexPath.item]
return cell
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
//let item = items[indexPath.item]
//inventoryController?.showItemDetailForItem(item: item, index: indexPath.item)
print("selected")
print(indexPath.item)
collectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item == 0 {
return CGSize(width:frame.width, height: 100) //each cell dimensions
}else{
return CGSize(width:frame.width, height: 150) //each cell dimensions
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
It's better to re-arrange your collection view to have 2 sections:
Section 0: PayNow cell (only 1 cell)
Section 1: Checkout cells (using checkout array list)
Then you don't have any confusion about the indexPath.item issue.
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return section == 0 ? 1 : checkout.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "paynow", for: indexPath) as! PayNowCell //init cells
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! CheckoutCell //init cells
print("Printing this \(checkout.count)")
cell.item = checkout[indexPath.item]
return cell
}
}
You need to subtract 1 from your cellForRow function. Since you are adding the PayNowCell you are adding one extra indexPath, but currently, you are using the default indexPath given by the dataSource function. That index will always be one higher than the count of your items. By subtracting 1 from the indexPath, you will be back in sync with your itemsArray, taking into account the PayNowCell.
cell.item = checkout[indexPath.item - 1]
I am using core data to populate a collection view. Now i want to check if it is empty or not. I want to do additional stuff if it is empty. How can I achieve that?
if myJokes.isEmpty == true {
noFavorites.isHidden = false
}
I have tried to check it likes this but it did not work:
class ViewController2: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDelegate, UICollectionViewDataSource {
var myJokes : [MyJokes] = []
override func viewDidLoad() {
super.viewDidLoad()
getData()
myCollectionView.reloadData()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return myJokes.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = myCollectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! CollectionViewCell2
if myJokes.isEmpty == true {
noFavorites.isHidden = false
}
cell.backView.layer.cornerRadius = 10
let task = myJokes[indexPath.row]
cell.textLabel.text = task.favoriteJokes!
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: myCollectionView.frame.width, height: 58)
}
func getData() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do {
myJokes = try context.fetch(MyJokes.fetchRequest())
} catch {}
}
#IBAction func menuBtnPressed(_ sender: Any) {
if menuShowing {
menuConstraint.constant = -171
UIView.animate(withDuration: 0.4, animations: {
self.view.layoutIfNeeded()
})
}