I Know same question has been asked before , but it did not help.
I am trying to add section titles to my UITableView. I am able to create the sections and count the number of elements in each section properly, , the cells are repeated the data in all the sections.
I am posting only relevant code -
My model is -
import UIKit
struct Product:Equatable {
let imagename: UIImage }
var productarray = [Product(imagename:#imageLiteral(resourceName: "CakeImage")),
Product( imagename:#imageLiteral(resourceName: "PeasImge")),Product(imagename:#imageLiteral(resourceName: "vectorlogo")),
Product(imagename: #imageLiteral(resourceName: "blue"))]
The ProductViewController is -
import UIKit
class ProductViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let sections = ["Section A", "Section B","Section C", "Section D"]
let rowspersection = [1, 1,1,1]
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rowspersection[section]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let data = productarray[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductTableViewCell") as! ProductTableViewCell
cell.imageView?.image = data.imagename
cell.myParent = self
return cell
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 44
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch(section) {
case 0:return "Section A"
case 1:return "Section B"
case 2:return "Section C"
case 3 :return "Section D"
default :return ""
}
}
}
Now, in the above only the first image of the productarray i.e. "[Product(imagename:#imageLiteral(resourceName: "CakeImage"))," is repeated in all the sections as shown in the image below:-
I want all the images/cell to be in the respective sections and not just one image/cell to be repeated in all the sections.
Any help will be appreciated.
Reason:
Since you've a single row in each section, everytime you use indexPath.row which is 0 for the 1st row, you end up accessing productarray[0] for each row in every section.
That's the reason all the rows are same because all of them are filled with productarray[0].
Solution:
Simply use indexPath.section instead of indexPath.row
let data = productarray[indexPath.section]
Note: Instead of making 3 different arrays, you can create a single array of custom type and use that as the dataSource of your tableView. Example:
struct Section {
let name: String
let products: [Product]
}
let sections = [
Section(name: "Section A", products: [Product(imagename:#imageLiteral(resourceName: "CakeImage"))]),
Section(name: "Section B", products: [Product(imagename:#imageLiteral(resourceName: "PeasImge"))]),
Section(name: "Section C", products: [Product(imagename:#imageLiteral(resourceName: "vectorlogo"))]),
Section(name: "Section D", products: [Product(imagename:#imageLiteral(resourceName: "blue"))]),
]
Use sections array as the dataSource. This will avoid a lot of confusion.
in every section indexPath start with zero so it display first index of productarray.
let data = productarray[indexPath.row]
replace with
let data = productarray[indexPath.row + indexPath.section]
Edit
var index = indexPath.row
if indexPath.section != 0, rowspersection.count > indexPath.section - 1{
index += rowspersection[indexPath.section - 1]
}
if index < productarray.count{
let data = productarray[index]
cell.imageView?.image = data.imagename
}
Edit Checkout
Replace this methods
func updateCart(cell: ProductTableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
var index = indexPath.row
if indexPath.section != 0, rowspersection.count > indexPath.section - 1{
index += rowspersection[indexPath.section - 1]
}
if index < productarray.count{
let product = productarray[index]
//Update Cart with product
cart.updateCart(with: product)
self.navigationItem.rightBarButtonItem?.title = "Checkout (\(cart.items.count))"
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductTableViewCell") as! ProductTableViewCell
cell.delegate = self // original issue was here, now resolved.
var index = indexPath.row
if indexPath.section != 0, rowspersection.count > indexPath.section - 1{
index += rowspersection[indexPath.section - 1]
}
if index < productarray.count{
let data = productarray[index]
cell.name?.text = data.name
cell.imageView?.image = data.imagename
let product = productarray[index]
cell.setButton(state: self.cart.contains(product: product))
}
return cell
}
Related
I want to expand and collpase the multilevel array in uitableview like the following
Cat1
SubCat1
Info 1
Info 2
SubCat2
Info 1
Info 2
SubCat3
Info 1
Info 2
Cat2
SubCat1
Info 1
Info 2
For that purpose I have done the following code.
struct CellData {
var opened = Bool()
var subCatTitle = String()
var subCatList = [String]()
}
struct MainModel {
var opened = Bool()
var categoryTitle = String()
var categoryList = [CellData]()
}
I have made the list
#IBOutlet var expandableThreeStageTableView: UITableView!
var arrayList = [CellData]()
var expandableList = [MainModel]()
func loadData(){
arrayList.append(CellData(opened: false, subCatTitle: "SubCat1", subCatList: ["Info1","Info2","Info3"]))
arrayList.append(CellData(opened: false, subCatTitle: "SubCat2", subCatList: ["Info1","Info2","Info3"]))
arrayList.append(CellData(opened: false, subCatTitle: "SubCat3", subCatList: ["Info1","Info2"]))
arrayList.append(CellData(opened: false, subCatTitle: "SubCat4", subCatList: ["Info1"]))
expandableList.append(MainModel(opened: true, categoryTitle: "Cat1", categoryList: arrayList))
expandableList.append(MainModel(opened: false, categoryTitle: "Cat2", categoryList: arrayList))
expandableList.append(MainModel(opened: false, categoryTitle: "Cat3", categoryList: arrayList))
}
And delegate, datasource methods are given below
extension TextFieldAsSearchVC : UITableViewDataSource{
func numberOfSections(in tableView: UITableView) -> Int {
return expandableList.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section:
Int) -> Int {
if expandableList[section].opened{
if expandableList[section].categoryList[section].opened{
return
expandableList[section].categoryList[section].subCatList.count////which extra count should return here
}else{
print("COUNT ",expandableList[section].categoryList.count)
return expandableList[section].categoryList.count +
1///here +1 is for catname + subcatname
}
}else{
return 1
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
if indexPath.row == 0{
let cell =
expandableThreeStageTableView.dequeueReusableCell(withIdentifier:
"TextFieldAsSearchVCCell", for: indexPath) as! TextFieldAsSearchVCCell
cell.lblValue.text =
expandableList[indexPath.section].categoryTitle
return cell
}else if indexPath.row <=
expandableList[indexPath.section].categoryList.count{
let cell =
expandableThreeStageTableView.dequeueReusableCell(withIdentifier:
"SectionDataCell", for: indexPath) as! SectionDataCell
cell.rowLabel.text =
expandableList[indexPath.section].categoryList[indexPath.row -
1].subCatTitle
return cell
}
else{
let cell =
expandableThreeStageTableView.dequeueReusableCell(withIdentifier:
"SectionDataCell", for: indexPath) as! SectionDataCell
cell.rowLabel.text =
expandableList[indexPath.section].categoryList[indexPath.row].
subCatList[indexPath.row]//how to access rows in subcategories
return cell
}
}
}
extension TextFieldAsSearchVC : UITableViewDelegate{
func tableView(_ tableView: UITableView, didSelectRowAt indexPath:
IndexPath) {
if indexPath.row == 0{
if expandableList[indexPath.section].opened{
expandableList[indexPath.section].opened = false
//now reload the section
let sections = IndexSet(integer: indexPath.section)
expandableThreeStageTableView.reloadSections(sections,
with: .automatic)
}else{
expandableList[indexPath.section].opened = true
//now reload sections
let sections = IndexSet(integer: indexPath.section)
expandableThreeStageTableView.reloadSections(sections,
with: .automatic)
}
}else {
if
expandableList[indexPath.section].categoryList[indexPath.row].opened{
expandableList[indexPath.section].categoryList[indexPath.row].opened =
false
expandableThreeStageTableView.reloadRows(at:
[IndexPath(index: indexPath.row)], with: .automatic)
}else{
expandableList[indexPath.section].categoryList[indexPath.row].opened =
true
expandableThreeStageTableView.reloadRows(at:
[IndexPath(index: indexPath.row)], with: .automatic)
}
}
}
}
From above code I can expand and collapse the Categories but not Subcategories.. When I tried to click on Subcategories it gives me an error
*** Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'Invalid index path for use
with UITableView. Index paths passed to table view must contain exactly
two indices specifying the section and row. Please use the category on
NSIndexPath in NSIndexPath+UIKitAdditions.h if possible.'
How to deal with such type of logic?
The specific error you are getting occurs in this line:
expandableThreeStageTableView.reloadRows(at:
[IndexPath(index: indexPath.row)], with: .automatic)
An IndexPath needs both, a row and a section; you're only providing a row. So it should be something like this:
expandableThreeStageTableView.reloadRows(at:
[IndexPath(row: indexPath.row, section: indexPath.section)], with: .automatic)
If you really only need to reload the current indexPath, simply call it like this:
expandableThreeStageTableView.reloadRows(at:
[indexPath], with: .automatic)
This would fix the error you are getting, but I don't know if that solves your problem or not.
I am working on an app in which requirement is to create a hamburger menu with submenu in it like
I tried different method using table inside table view cell etc but unable to create this menu.
if someone has a solution then recommend me
You can create such an item structure as your data source
struct Item {
let text: String
var subItems: [String]?
var isExpanded = false
init(_ text: String, items: [String]? = nil) {
self.text = text
self.subItems = items
}
}
Usage
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet var tableView: UITableView!
private let imgOpen = UIImage(named: "open")
private let imgClose = UIImage(named: "close")
private var dataSource = [Item]()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "groupCell")
self.tableView.dataSource = self
self.tableView.delegate = self
self.dataSource.append(Item("HOME"))
self.dataSource.append(Item("ABOUT US"))
self.dataSource.append(Item("OUR PROJECTS", items: ["Project-1", "Project-2", "..."]))
self.dataSource.append(Item("BAHRIA TOWN PHASE 1 - 7"))
self.dataSource.append(Item("BAHRIA TOWN PHASE 8"))
self.tableView.reloadData()
}
func numberOfSections(in tableView: UITableView) -> Int {
return self.dataSource.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let item = self.dataSource[section]
if item.isExpanded, let count = item.subItems?.count {
return count + 1
}
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = self.dataSource[indexPath.section]
let cell = tableView.dequeueReusableCell(withIdentifier: "groupCell", for: indexPath)
var imageView: UIImageView?
if indexPath.row > 0, let text = item.subItems?[indexPath.row - 1] {
cell.textLabel?.text = text
} else {
cell.textLabel?.text = item.text
if item.subItems != nil {
imageView = UIImageView(image: item.isExpanded ? self.imgClose : self.imgOpen)
}
}
cell.accessoryView = imageView
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = self.dataSource[indexPath.section]
if indexPath.row == 0 && item.subItems != nil {
self.dataSource[indexPath.section].isExpanded = !item.isExpanded
let indexSet = IndexSet(integer: indexPath.section)
tableView.reloadSections(indexSet, with: .automatic)
} else {
// non-expandable menu item tapped
}
}
}
You should separate the process.
First, create the hamburger menu: for this, I recommend using this 3rd party library: https://github.com/John-Lluch/SWRevealViewController
With the help of this, it is very easy to create a side out menu from the left side of the screen.
The best thing that you will get a ViewController which will responsible for the menu, so you can easily customize it.
Second, as mentioned below, you should use a tableView with expandable cells. The best way to do this is to basically just show the headers of the cells. If the user taps on a header, then show the actual cell. (rowheight > 0). There is a tutorial about this: https://www.youtube.com/watch?v=bSKUYRsMCrM
-> you can create a sliding drawer menu(hamburger menu) using any of the following libraries:
1) REFrostedViewController
2) SWRevealViewController or any other
-> Sub Menu: In the drawer view controller, you have to add a table view and implement expandable/collapsible sections to display a submenu. You can follow any tutorial explaining about expand-collapse table view sections. Some of the tutorial links are below:
https://github.com/jeantimex/ios-swift-collapsible-table-section
https://medium.com/#legonaftik/uitableview-with-collapsible-sections-927d726b985c
1st Follow https://github.com/jonkykong/SideMenu.
And then to make EXPANDABLE Cells:-
You just need to create 2 Cells in UITableView(In Storyboard). First cell for those who are not expandable and Second cell for the expandable.
class SideMenuTableViewController: UITableViewController {
// MARK:- Constants And Vars
var isOurProjectCellExpanded = false
}
class SideMenuTableViewController: UITableViewDataSource, UITableViewDelegate {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "simpleCell", for: indexPath) as! SideMenuBasicTableViewCell
switch indexPath.row {
case 0:
cell.itemName.text = "HOME"
break
case 1:
cell.itemName.text = "About Us"
break
case 2:
if(isOurProjectCellExpanded){
//expandedCell
let cell = tableView.dequeueReusableCell(withIdentifier: "expandedCell", for: indexPath) as! SideMenuBasicTableViewCell
cell.itemName.text = "Our Projects"
return cell
}else{
cell.arrowDownImageView.isHidden = false
cell.itemName.text = "Our Projects"
}
break
case 3:
cell.itemName.text = "Bahria Town phase 1-7"
break
case 4:
cell.itemName.text = "Bahria Town phase 8"
break
default:
break
}
return cell
}
//And in `DidSelectRow` Method
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if(indexPath.row == 2){
if(isOurProjectCellExpanded){
isOurProjectCellExpanded = false
tableView.reloadRows(at: [indexPath], with: .none)
}else{
isOurProjectCellExpanded = true
tableView.reloadRows(at: [indexPath], with: .none)
}
}else if(indexPath.row == 0){
// Handle it yourself
}else if(indexPath.row == 1){
// Handle it yourself
}else if(indexPath.row == 3){
// Handle it yourself
}else if(indexPath.row == 4){
// Handle it yourself
}
}
}
I have used tableview(grouped).
So i need to select one row from the each section of UITableviewSection.
So for that i have tableview and one submit button .So i need to check when i click on the submit button i need to check whether i have selected one row from the each section ,if not then show alert as not selected the section number.How to check?
This is my data.
{
"data":[
{
"question": "Gender",
"options": ["Male","Female"]
},
{
"question": "How old are you",
"options": ["Under 18","Age 18 to 24","Age 25 to 40","Age 41 to 60","Above 60"]
},
{
"question": "I am filling the Questionnaire for?",
"options": ["Myself","Mychild","Partner","Others"]
}
]
}
QuestionModel:-
class QuestionListModel: NSObject {
var selected = false
var dataListArray33:[NH_OptionsModel] = []
var id:Int!
var question:String!
var buttontype:String!
var options:[String]?
var v:String?
var optionsModelArray:[OptionsModel] = []
init(dictionary :JSONDictionary) {
guard let question = dictionary["question"] as? String,
let typebutton = dictionary["button_type"] as? String,
let id = dictionary["id"] as? Int
else {
return
}
if let options = dictionary["options"] as? [String]{
print(options)
print(options)
for values in options{
print(values)
let optionmodel = OptionsModel(values: values)
self.optionsModelArray.append(optionmodel)
}
}
self.buttontype = typebutton
self.question = question
self.id = id
// print(self.dataListArray33)
}
}
optionModel:-
class OptionsModel: NSObject {
var isSelected:Bool? = false
var v:String?
var values:String?
init(values:String) {
self.values = values
print( self.values)
}
ViewModel:-
func numberOfSections(tableView: UITableView) -> Int{
print((datasourceModel.dataListArray?.count)!)
return (datasourceModel.dataListArray?.count)!
}
func titleForHeaderInSection(atsection section: Int) -> NH_QuestionListModel {
return datasourceModel.dataListArray![section]
}
func numberOfRowsIn(section:Int) -> Int {
print( datasourceModel.dataListArray?[section].optionsModelArray.count ?? 0)
return datasourceModel.dataListArray?[section].optionsModelArray.count ?? 0
// return self.questionsModelArray?[section].optionsModelArray.count ?? 0
}
func datafordisplay(atindex indexPath: IndexPath) -> NH_OptionsModel{
print(datasourceModel.dataListArray![indexPath.section].optionsModelArray[indexPath.row])
return datasourceModel.dataListArray![indexPath.section].optionsModelArray[indexPath.row]
}
func question(answer:String) {
print(questions)
questions.append(answer)
print(questions )
}
func questionlist(answer:String) {
print( questionlist )
questionlist.append(answer)
print( questionlist )
}
func answer(answer:String) {
answers.append(answer)
print(answers)
}
and finally viewController:-
func numberOfSections(in tableView: UITableView) -> Int {
return questionViewModel.numberOfSections(tableView: tableView)
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let identifier = "HeaderCell"
var headercell: questionheader! = tableView.dequeueReusableCell(withIdentifier: identifier) as? questionheader
if headercell == nil {
tableView.register(UINib(nibName: "questionheader", bundle: nil), forCellReuseIdentifier: identifier)
headercell = tableView.dequeueReusableCell(withIdentifier: identifier) as? NH_questionheader
}
headercell.setReviewData(reviews:questionViewModel.titleForHeaderInSection(atsection:section))
return headercell
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 150
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return questionViewModel.numberOfRowsIn(section: section)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "Cell"
var cell: QuestionListCell! = tableView.dequeueReusableCell(withIdentifier: identifier) as? QuestionListCell
if cell == nil {
tableView.register(UINib(nibName: "QuestionListCell", bundle: nil), forCellReuseIdentifier: identifier)
cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? NH_QuestionListCell
}
cell.contentView.backgroundColor = UIColor.clear
let questionsModel = questionViewModel.titleForHeaderInSection(atsection:indexPath.section)
print(questionsModel.buttontype)
questionViewModel.button = questionsModel.buttontype
cell.setOptions(Options1: questionViewModel.datafordisplay(atindex: indexPath))
print("Section \(indexPath.section), Row : \(indexPath.row)")
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){
print("Section \(indexPath.section), Row : \(indexPath.row)")
let cell = tableview.cellForRow(at: indexPath) as? NH_QuestionListCell
let model = questionViewModel.datafordisplay(atindex: indexPath)
print(model.isSelected)
cell?.setOptions(OptionsSelected:questionViewModel.datafordisplay(atindex: indexPath))
print(model.isSelected)
questionViewModel.isselected = model.isSelected!
let section = indexPath.section
let index = indexPath.row
print(section)
print(index)
if !questionViewModel.selectedIndexPaths.contains(indexPath) {
questionViewModel.selectedIndexPaths.append(indexPath)
print(questionViewModel.selectedIndexPaths.append(indexPath))
let questionModel = questionViewModel.titleForHeaderInSection(atsection: section)
print(questionModel.question)
questionViewModel.question = questionModel.question
questionViewModel.questionlist(answer: questionViewModel.question!)
let cell = tableview.cellForRow(at: indexPath) as? NH_QuestionListCell
let model = questionViewModel.datafordisplay(atindex: indexPath)
print(model.values)
questionViewModel.answer(answer: model.values!)
let value: Int = questionModel.id
let string = String(describing: value)
//let x: Int? = Int(model.id)
questionViewModel.question_id = string
questionViewModel.question(answer: questionViewModel.question_id!)
print(questionModel.id)
// append the selected index paths
} // if indexPath.section == section {
// questionViewModel.indexPath(indexPaths: index)
// }
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let index = questionViewModel.selectedIndexPaths.index(of: indexPath) {
print(index)
questionViewModel.selectedIndexPaths.remove(at: index)
}
}
According to this i got the output .
But i have button action in viewcontroller.
#IBAction func forward(_ sender: AnyObject) {
}
In this button action i need to check whether from each section did i selected one row or not .if not show alert .How to do
my current didselect method :-
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath){
let cell = tableview.cellForRow(at: indexPath) as? NH_QuestionListCell
let model = questionViewModel.datafordisplay(atindex: indexPath)
print(model.isSelected)
cell?.setOptions(OptionsSelected:questionViewModel.datafordisplay(atindex: indexPath))
print(model.isSelected)
questionViewModel.isselected = model.isSelected!
let section = indexPath.section
let index = indexPath.row
print(section)
print(index)
if !questionViewModel.selectedIndexPaths.contains(indexPath) {
questionViewModel.selectedIndexPaths.append(indexPath)
print(questionViewModel.selectedIndexPaths.append(indexPath))
let questionModel = questionViewModel.titleForHeaderInSection(atsection: section)
print(questionModel.question)
questionViewModel.question = questionModel.question
questionViewModel.questionlist(answer: questionViewModel.question!)
let cell = tableview.cellForRow(at: indexPath) as? NH_QuestionListCell
let model = questionViewModel.datafordisplay(atindex: indexPath)
print(model.values)
questionViewModel.answer(answer: model.values!)
let value: Int = questionModel.id
let string = String(describing: value)
//let x: Int? = Int(model.id)
questionViewModel.question_id = string
questionViewModel.question(answer: questionViewModel.question_id!)
print(questionModel.id)
}
I have 3 array
According to this didselect method:-
ex:-for section 1 :-i selected 1st row so the data append as below.
questionlist:["How r u?"]
answelist:["fine"]
But suppose i think that i need 2nd indexpath ,so i need to remove the previous appended data from arrays and append the current data .As below:
questionlist:["How r u?"]
answelist:["not well"]
And next for section 2 : i selected 1st indexpath.row data .then that data is append.So i need to get as below:-
questionlist:["How r u?","Gender"]
answelist:["not well","Male"]
Here selecting i think that i need the 2nd option then remove the added indexpath.row data from array and show as:-
questionlist:["How r u?","Gender"]
answelist:["not well","Female"]
Such way how to set?
you can update your model based on the selection like
"data":[
{
"question": "Gender",
"options": ["Male","Female"],
"optionSelected": "Male"
}
]
and on Submit , check data for selections
The table view has a property to get selected index paths. You can use all native components for that. What you need is to deselect an item at index path where one is already selected in a certain section. You also just need to then check that the number of selected index paths is the same as number of arrays in your data source.
Check something like this:
var dataSource: [[Any]]!
var tableView: UITableView!
func didSelectRowAt(_ indexPath: IndexPath) {
guard let selectedPaths = tableView.indexPathsForSelectedRows else { return } // We need to have selected paths
guard selectedPaths.contains(indexPath) == false else { return } // The same cell being selected
let previouslySelectedCellIndexPaths: [IndexPath] = selectedPaths.filter { $0.section == indexPath.section && $0 != indexPath } // Getting all selected index paths within this section
previouslySelectedCellIndexPaths.forEach { tableView.deselectRow(at: $0, animated: true) } // Deselect waht was previously selected
}
/// Will return array of selected objects only if all sections have a selected index
///
/// - Returns: A result array
func getSelectionData() -> [Any]? {
guard let selectedPaths = tableView.indexPathsForSelectedRows else { return nil } // We need to have selected paths
guard selectedPaths.count == dataSource.count else { return nil } // This should prevent missing selections assuming all index paths are unique in sections
return selectedPaths.map { dataSource[$0.section][$0.row] } // Map selected index paths back to objects
}
I tried to use kind of minimum code to show all of this. It is all commented so you can see row by row what goes on.
You might want to check is all sections are unique the second method but it is not needed if the first one is always used.
You can store selected indexPath in an array. OnClick of submit just loop through array and check either at least one element is from each section.
FYI : indexPath contains section info also.
Declare an mutable array and allocate in viewDidLoad.
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[anArray addObject:indexPath];
}
on Submit action follow this, you can improvise based on your requirement
-(void)onSubmitAction{
[anArray addObject:indexPath];
NSMutableArray *countOfSection=[[NSMutableArray alloc]init];
for (NSIndexPath*indexPath in anArray ) {
if(![anArray containsObject:indexPath.section])
[countOfSection addObject:indexPath.section];
}
if(countOfSection.count == self.tableview.numberOfSections){
//write your code
}else{
// show alert
}
}
Step 1 : Create Global Variable
var selectedIndexPaths = [IndexPath]()
Step 2: Add UITableView Property
tableView.allowsMultipleSelection = true
Step 3 : Implement the delegate methods
//On Selection
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedIndexPathAtCurrentSection = selectedIndexPaths.filter({ $0.section == indexPath.section})
for indexPath in selectedIndexPathAtCurrentSection {
tableView.deselectRow(at: indexPath, animated: true)
if let indexOf = selectedIndexPaths.index(of: indexPath) {
selectedIndexPaths.remove(at: indexOf)
}
}
selectedIndexPaths.append(indexPath)
}
// On DeSelection
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let index = selectedIndexPaths.index(of: indexPath) {
selectedIndexPaths.remove(at: index)
}
}
Step 4: Getting Selected IndexPaths with sections
#IBAction func forward(sender:Any){
let totalSections = questionViewModel.numberOfSections(tableView: tableView)
for section in 0..<totalSections {
if (selectedIndexPaths.filter({ $0.section == section}).count >= 1) {
continue
} else {
// Show alert
print("Please select item at",(section))
return
}
}
}
I created the Expandable table view cells which is like Below mentioned image.
Library used is JExpandableTableView.
Code for Creating This ExpandableTable View is given below :
Model For Sections:
class SectionInfo: NSObject {
var cells = [CellInfo]()
var CategoryName: String
var CategoryCount: String
var CategoryImage: UIImage
init(_ text: String,SubCount: String, Image: UIImage ) {
self.CategoryName = text
self.CategoryCount = SubCount
self.CategoryImage = Image
}
}
Model For SubCategoryCell:
class CellInfo: NSObject {
var SubcategoryName: String!
var SubcategoryCount: String!
init(_ SubName: String, SubCount: String) {
self.SubcategoryName = SubName
self.SubcategoryCount = SubCount
}
}
View Controller :
class CategoryVC: BaseVC,JExpandableTableViewDataSource,JExpandableTableViewDelegate{
#IBOutlet weak var tblViewCategory: JExpandableTableView!
var tableViewData = [SectionInfo]()
var expandedIndexPath: IndexPath?
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Category"
self.tblViewCategory.dataSource = self
self.tblViewCategory.delegate = self
self.tblViewCategory.keepPreviousCellExpanded = false
self.LoadData()
}
func LoadData() {
var cellInfo = CellInfo("SubCategory 1",SubCount: "10")
let sec1 = SectionInfo("Category 1", SubCount: "5", Image: UIImage(named: "Category3")!)
sec1.cells.append(cellInfo)
let sec2 = SectionInfo("Category 2", SubCount: "8", Image: UIImage(named: "Category3")!)
cellInfo = CellInfo("SubCategory 2",SubCount: "20")
sec2.cells.append(cellInfo)
cellInfo = CellInfo("SubCategory 2.1",SubCount: "30")
sec2.cells.append(cellInfo)
let sec3 = SectionInfo("Category 3", SubCount: "10", Image: UIImage(named: "Category3")!)
cellInfo = CellInfo("SubCategory 3",SubCount: "30")
sec3.cells.append(cellInfo)
tableViewData.append(sec1)
tableViewData.append(sec2)
tableViewData.append(sec3)
let celNib = UINib.init(nibName: "SubCategoryCell", bundle: nil)
tblViewCategory.register(celNib, forCellReuseIdentifier: "SubCategoryCell")
let headerNib = UINib.init(nibName: "HeaderView", bundle: nil)
tblViewCategory.register(headerNib, forHeaderFooterViewReuseIdentifier: "HeaderView")
}
#IBAction func DrawerMenutap(_ sender: Any) {
self.OpenDrawerMenu()
}
func tableView(_ tableView: JExpandableTableView, numberOfRowsInSection section: Int, callback: #escaping (Int) -> Void) {
let sectionInfo = self.tableViewData[section]
callback(sectionInfo.cells.count)
}
func tableView(_ tableView: JExpandableTableView, heightForHeaderInSection section: Int) -> CGFloat {
return 150
}
func tableView(_ tableView: JExpandableTableView, heightForRowAtIndexPath indexPath: IndexPath) -> CGFloat {
return 44
}
func tableView(_ tableView: JExpandableTableView, initialNumberOfRowsInSection section: Int) -> Int {
// let sectionInfo = self.tableViewData[section]
return 0
}
func numberOfSections(in tableView: JExpandableTableView) -> Int {
return tableViewData.count
}
func tableView(_ tableView: JExpandableTableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
let section = tableViewData[indexPath.section]
let row = section.cells[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "SubCategoryCell", for: indexPath) as! SubCategoryCell
cell.contentView.backgroundColor = UIColor.white
cell.lblSubCategoryName.text = row.SubcategoryName
return cell
}
func tableView(_ tableView: JExpandableTableView, viewForHeaderInSection section: Int) -> UIView? {
let section = self.tableViewData[section]
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderView") as! HeaderView
header.contentView.backgroundColor = UIColor.groupTableViewBackground
header.CatName.text = section.CategoryName
header.CatImgView.image = UIImage(named: "Category4")
header.CatCount.text = section.CategoryCount
return header
}
}
//MARK: Table View Cell for Category
class CatCell: UITableViewCell {
#IBOutlet weak var lblName: UILabel!
}
My further requirement is I want to expand the Cells (Subcategory 2, SubCategory 2.1) in order to accommodate SubSubCategory(Childrens of SubCategory) in Case if they Exist. So what should be the approach for achieving this.
UITableView is really designed in a way to show two levels, sections and rows.
But to show more then two levels you can manipulate rows that will increase/expand or decrease/collapse according to your model for Section, SubCategory.
So table structure will look like that
section_header_1
subCategory_1.0
subSubCategory_1.0.1
subCategory_1.1
subSubCategory_1.1.1
subCategory_1.2
subSubCategory_1.2.1
section_header_2
subCategory_2.0
subSubCategory_2.0.1
subCategory_2.1
subSubCategory_2.1.1
subCategory_2.2
subSubCategory_2.2.1
For Header you have to make your own custom header row and put that as the first row of each section. You could set up a cell to LOOK like a header, and setup the tableView:didSelectRowAt to manually expand or collapse the section, subCategory or SubSubCategory it is in. the rows after first row will be your subCategory and subSubCategory.
Then a Model For Section, SubCategory and SubSubCategory to store a booleans corresponding the the "expend" value of each of your sections, subCategories. you can avoid SubSubCategory model if it's only store it's name but it's easy to understand if you do so. for an example a struct for holding Section, SubCategory "expend" booleans.
public struct Section {
var name: String
var expand: Bool
var subCategory:[SubCategory]
public init(name: String, expand: Bool = false ,subCategory: [SubCategory]) {
self.name = name
self.expand = expand
self.subCategory = subCategory
}
}
public struct SubCategory {
var name: String
var expand: Bool
var subSubCategory: SubSubCategory
public init(name: String, expand: Bool = false, subSubCategory: SubSubCategory) {
self.name = name
self.expand = expand
self.subSubCategory = subSubCategory
}
}
public struct SubSubCategory {
var name: String
public init(name: String) {
self.name = name
}
}
Create 3 Custom cell one for Header other for SubCategory and SubSubCategory and display it in the first row of every section Header Cell and after expand or collapse show your SubCategory or SubSubCategory Cell accordingly.
after all together your code should be look that that.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var sampleData: [Section] = [
Section(name: "Category 1", expand: false,
subCategory: [
SubCategory(name: "Category 1.1", expand: false, subSubCategory: SubSubCategory(name: "SubSubCategory 1.1.1")),
SubCategory(name: "Category 1.2", expand: false, subSubCategory: SubSubCategory(name: "SubSubCategory 1.2.1"))
]
),
Section(name: "Category 2", expand: false,
subCategory: [
SubCategory(name: "Category 2.1", expand: false, subSubCategory: SubSubCategory(name: "SubSubCategory 2.1.1")),
SubCategory(name: "Category 2.2", expand: false, subSubCategory: SubSubCategory(name: "SubSubCategory 2.2.1"))
]
)
]
override func viewDidLoad() {
super.viewDidLoad()
}
//
// MARK: - View Controller DataSource and Delegate
//
func numberOfSections(in tableView: UITableView) -> Int {
return sampleData.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var expandCount = 0
if sampleData[section].expand {
// if header is expanded all subCategory will be also expanded
expandCount = sampleData[section].subCategory.count
for subCategory in sampleData[section].subCategory{
//check for how many subSubCategory is expanded
if subCategory.expand{
expandCount += 1
}
}
}
// returning the count of total expanded SubCategories and SubSubCategories
// 1 is for header you can remove if you are using `viewForHeaderInSection`
return 1 + expandCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Header cell
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "header")!
return cell
}else{
var countValue = 0
var indexSubCategory = 0
let sampleDataSection = sampleData[indexPath.section]
// check for how many "subCategory" expanded or collapsed
if sampleDataSection.expand{
for (index, subCategory) in sampleDataSection.subCategory.enumerated(){
countValue += 1
if countValue >= indexPath.row{
indexSubCategory = index
break
}
// check for how many "subSubCategory" expanded or collapsed
if subCategory.expand{
if index == sampleDataSection.subCategory.count-1{
countValue += 2
indexSubCategory = index + 1
}else{
countValue += 1
}
}
}
// if countValue is grater then indexPath.row it will return "subSubCategory" cell
// else/countValue = indexPath.row then return "subCategory" cell
if countValue > indexPath.row{
// Cell subSubCategory
let cell = tableView.dequeueReusableCell(withIdentifier: "subSubCategory")!
cell.textLabel?.text = self.sampleData[indexPath.section].subCategory[indexSubCategory - 1].subSubCategory.name
return cell
}else{
// Cell subCategory
let cell = tableView.dequeueReusableCell(withIdentifier: "subCategory")!
cell.textLabel?.text = self.sampleData[indexPath.section].subCategory[indexSubCategory].name
return cell
}
}
else{
// Cell subCategory
let cell = tableView.dequeueReusableCell(withIdentifier: "subCategory")!
cell.textLabel?.text = self.sampleData[indexPath.section].subCategory[indexPath.row].name
return cell
}
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// then header cell is selected switch between collapse or expand between "subCategory"
if indexPath.row == 0{
let expand = !sampleData[indexPath.section].expand
//Toggle collapse
sampleData[indexPath.section].expand = expand
self.tableView.reloadSections([indexPath.section], with: .none)
}else{
var countValue = 0
var indexSubCategory = 0
let sampleDataSection = sampleData[indexPath.section]
if sampleDataSection.expand{
for (index, subCategory) in sampleDataSection.subCategory.enumerated(){
countValue += 1
if countValue >= indexPath.row{
indexSubCategory = index
break
}
if subCategory.expand{
if index == sampleDataSection.subCategory.count-1{
countValue += 2
indexSubCategory = index + 1
}else{
countValue += 1
}
}
}
// and if "subCategory" cell is selected switch between collapse or expand between "subSubCategory"
if countValue == indexPath.row{
let subSubCategory = sampleData[indexPath.section].subCategory[indexSubCategory]
let expand = !subSubCategory.expand
sampleData[indexPath.section].subCategory[indexSubCategory].expand = expand
UIView.performWithoutAnimation {
self.tableView.reloadSections([indexPath.section], with: .none)
self.tableView.layoutIfNeeded()
}
}
}
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}
}
Download demo project from here
You can achieve this by creating a custom cell for the SubCategory 2.1, and displaying it in the first row of every section.
Than, in the didSelectRow method, if the first row is selected, you update the state of the SubCategory 2.1 to collapsed (or non collapsed),reload the section and in the numberOfRows method you should return the appropriate number of rows according to the state of the SubCategory 2.1.
A UITableView really isn't designed to show more than two levels of a hierarchy, as sections and rows.
If you want to show more than two levels there are all deferent type of custom solutions to this, one easy way is to design different cell for Subcategory and display accordingly like showed in the image bellow.
In the above example it is same cell but with different background colour to indicate subcategory.
I came across this video on youtube :
https://www.youtube.com/watch?v=VFtsSEYDNRU
It might help you to achieve what you want.
But I think that you could achieve this by making a custom UIView and putting it into your cell so that when you tap on the subcategory it expands and shows more detail as you wanted it.
Hope this helps.
I have a JSON is look some thing like this :
"product": [
{
"product_id": 471,
"info": "123456",
},
{
"product_id": 471,
"info": "356697456",
},
{
"product_id": 472,
"info": "1432",
},
{
"product_id": 473,
"info": "4321",
},
]
I want to set my TableView to look something like this image below :
what I want is:
If the first cell in the TableView I want the Product 1 (in Red color) shown.
If the second cell's product_id is same with it previous cell's product_id then Product 1 is no longer shown, it disappears.
Since the 3rd cell's product_id is not same with previous cell (second cell), so the red label Product 2 is shown up.
Same go to Product 3 and the rest of the cell in the TableView
What I already tried:
In order to achieve this,I need to get the indexPath inside cellAtRow delegate,so I compare each cell's product_id with the previous one,and then control the logic inside.
Here is my code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
let thisIndexPath = indexPath.row
if thisIndexPath - 1 > -1 {
let previousProductId = self.productItem[thisIndexPath - 1].productId
let thisProductId = self.productItem[thisIndexPath].productId
if previousProductId == thisProductId {
cell.productLabel.isHidden = true
cell.productHeight.constant = 0
cell.productnameTopContraint.constant = 0
cell.productnameBottomContraints.constant = 0
}else {
cell.productnameLabel.isHidden = false
}
}else{
cell.productnameLabel.isHidden = false
}
cell.item = selfProductItem[indexPath.row]
return cell
}
}
But now the problem is:
-- When the TableView first launch,the UI display like that I shown above,but when I start scrolling,all the cell's Product label(in red color) is gone,although the product_id is not same with the previous cell's product_id.
-- When I scroll back to the first cell,the Product Label(In red color) is gone as well.Which means the UI is only right at first launch of the screen,which is not persistent.
So my question is:
What is the correct way to compare data from current cell to the previous cell?
Is it right to do the comparison inside cellForRowAt delegate method?If not,where should I do this?
I think that to solve your issue, you should think about how you will store your JSON data.
You could begin by creating a struct called 'Product' which will store the product information and then by making it Equatable you can add a function which will allow you to compare the productID's:
/// Structure Of A Product
struct Product: Equatable{
var productID: Int
var productInfo: Int
static func == (lhs: Product, rhs: Product) -> Bool {
return lhs.productID == rhs.productID
}
}
Now to use this your structure you can create an Array variable to store your Products:
//Array To Store Array Of Product
var products = [Product]()
In this example I am just manually inputting the product information but you should handle this in a better way. However, it does illustrate one way you could 'start' to handle this:
override func viewDidLoad() {
super.viewDidLoad()
//1. Create Products
let productOne = Product(productID: 471, productInfo: 123456)
let productTwo = Product(productID: 471, productInfo: 356697456)
let productThree = Product(productID: 472, productInfo: 1432)
let productFour = Product(productID: 473, productInfo: 4321)
//2. Add Them To The Products Array
addUnique(productOne)
addUnique(productTwo)
addUnique(productThree)
addUnique(productFour)
}
/// Checks That A Product Doesn't Exist
///
/// - Parameter product: Product
func addUnique(_ product: Product) {
if !products.contains(product) {
products.append(product)
}
}
In Step 1 we are manually creating the products.
In Step 2 we are calling the addUnique(_ product) function which will only allow unique Products to be stored.
After ensuring that there are no duplicate ProductID's, it should be easy for you to set the format as you like:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath) as! MyCell
cell.productLabel.text = products[indexPath.row].productID
cell.productnameLabel.text = products[indexPath.row].productInfo
}
Of course you will need to fix any colouring of labels etc.
I tried and it is working fine. I make one dummy array for you. Please check bellow
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tblProduct: UITableView!
var arrProduct = NSMutableArray()
var arrForSection = NSMutableArray()
var arrForProductId = NSMutableArray()
override func viewDidLoad()
{
super.viewDidLoad()
let dict = NSMutableDictionary()
dict.setValue("471", forKey: "product_id")
dict.setValue("123456", forKey: "info")
arrProduct.add(dict)
let dict1 = NSMutableDictionary()
dict1.setValue("471", forKey: "product_id")
dict1.setValue("356697456", forKey: "info")
arrProduct.add(dict1)
let dict2 = NSMutableDictionary()
dict2.setValue("472", forKey: "product_id")
dict2.setValue("1432", forKey: "info")
arrProduct.add(dict2)
let dict3 = NSMutableDictionary()
dict3.setValue("472", forKey: "product_id")
dict3.setValue("4321", forKey: "info")
arrProduct.add(dict3)
print(arrProduct)
self.createSection()
}
//MARK:
//MARK: Create section
func createSection()
{
arrForSection.removeAllObjects()
let arrtemp = NSMutableArray()
arrtemp.addObjects(from: (self.arrProduct as NSArray) as! [Any])
for i in 0 ..< arrtemp.count
{
let dict = self.arrProduct[i] as! NSMutableDictionary
let strProductId = (dict["product_id"] as? String)!
if(!arrForProductId .contains(strProductId))
{
arrForProductId.add(strProductId)
}
}
for j in 0 ..< arrForProductId.count
{
let strTempDate:String = arrForProductId .object(at: j) as! String
let arr1 = NSMutableArray()
for i in 0 ..< arrtemp.count
{
let dict = arrtemp .object(at: i) as! NSMutableDictionary
let strProductId = (dict["product_id"] as? String)!
if(strProductId == strTempDate)
{
arr1.add(dict)
}
}
arrForSection.add(arr1)
}
self.tblProduct.reloadData()
}
//MARK:
//MARK: TableView Delegate
func numberOfSections(in tableView: UITableView) -> Int {
return self.arrForSection.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return (((arrForSection .object(at: section)) as! NSMutableArray).count)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell:UITableViewCell = self.tblProduct.dequeueReusableCell(withIdentifier: "cell")!
let dictData = ((arrForSection .object(at: indexPath.section)) as! NSMutableArray).object(at: indexPath.row) as! NSDictionary
cell.textLabel?.text = dictData["info"] as? String
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
{
return arrForProductId[section] as? String
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Result see attach
Hope it helps!
I think in this case you can divide cells of your table view in sections and assign header (name of product) to each section. Please rever to official documentation for more information.
try setting the height constraints in else part too.
else part of this : if previousProductId == thisProductId { and this : if thisIndexPath - 1 > -1 {.