Swift: how to hide/show rows in section on `didSelectRowAt indexPath` - swift

Here is a sample I want to achieve. In general I have a dataSource like that an array of [Category]:
public struct Category {
let name: String
let image: UIImage
let id: Int
let subCategories: [SubCategory]
var onCategorySelected: (Int) -> Void
}
public struct SubCategory {
let name: String
let id: Int
var onSubCategorySelected: (Int) -> Void
}
I've started with approach that Category is a section and SubCategory is a row. So in numberOfSections I return category array count and in numberOfRowsInSection I return 1 if category is not selected and subcategory.count + 1 if selected. On cellForRowAt I setup proper cell from a custom class.
And I stuck on tableview delegate method: didSelectRowAt indexPath. How can I collapse or expand rows in specific section. I am trying at the moment:
public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(indexPath)
if indexPath.section == selectedCategory {
var indexToRemove: [IndexPath] = []
(1...props.categories[selectedCategory].subCategories.count).forEach { idx in
indexToRemove.append(IndexPath(row: idx, section: selectedCategory))
}
tableView.beginUpdates()
tableView.deleteRows(at: indexToRemove, with: .top)
tableView.endUpdates()
}
}
I got a crush because I don't update a data source but it seems that I don't actually need to remove these elements from a data source but only hide if first row in section is not selected and show immediately if selected. Any help or even general approach for displaying such structure is welcomed! Thanks!

I think you'll struggle in this way as category headers are don't have built in handlers for tap events (although you could build them into custom header views) but also when the numberOfRows for a section = 0 the section header isn't shown.
A better approach would be to put all entries in a single tableView section with two types of custom cell: one cell design for the categories and one for the sub-categories. Add a Bool variable to Category called something like expanded to track when a section has been expanded. Create an array that hold an entry for each visible cell using an enum with associated values to show whether its a Category or subCategory. Then in didSelectRowAt for a Category cell you can check the expanded property and then either insert or delete the sub-category cells as required.
The outline of the solution will look something like the below (all typed from memory, so may well have some syntax issues, but it should be enough to get you going)
public struct Category {
let name: String
let image: UIImage
let id: Int
let subCategories: [SubCategory]
var expanded = false
var onCategorySelected: (Int) -> Void
}
class CategoryCell: UITableViewCell {
static let cellID = "CategoryCell"
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
//set up imageViews (main & up/down) and textLabel
}
func configure(with category: Category {
//populate cell fields from category
}
}
class CategoryCell: UITableViewCell {
static let cellID = "SubcategoryCell"
// same things as above
}
Then in the view controller
enum RowType {
case category (Category)
case subcategory (SubCategory)
func toggled() -> RowType {
switch self {
case .subcategory: return self
case .category (let category):
category.expanded.toggle()
return .category(category)
}
}
}
//create an array of rows currently being displayed. Start with just Categories
var visibleRows: [RowType] = ArrayOfCategories.map{RowType.Category($0)}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CategoryCell.self, forCellReuseIdentifier: CategoryCell.cellID )
tableView.register(SubCategoryCell.self, forCellReuseIdentifier: SubCategoryCell.cellID)
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return visibleRows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch visibleRows[indexPath.row]
case .Category (let category):
let cell = tableView.dequeueReusableCell(withIdentifier: CategoryCell.CellID, for: indexPath) as! CategoryCell
cell.configure(with: category)
return cell
case .subCategory (let subCategory):
let cell = tableView.dequeueReusableCell(withIdentifier: SubCategoryCell.CellID, for: indexPath) as! SubCategoryCell
cell.configure(with: subCategory)
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch visibleRows[indexPath.row] {
case .category(let category):
if category.expanded {
tableView.deleteRows(... //delete the subCategory rows
visibleRows.remove( ... //delete the same rows from visibleRows
} else {
visibleRows.insert( ... //insert the .subcategory rows corresponding to the category struct's [subCategory] array
tableView.insertRows(... //insert the appropriate subCategory rows
}
visibleRows[indexPath.row] = visibleRows[indexPath.row].expanded.toggled()
tableView.reloadRows(at:[indexPath.row], with: .fade)
case .subCategory (let subCategory):
//do anything you want for a click on a subCat row
}
}

Related

How to manage section in UITableView

I'm trying to create an application with sections in my TableView.
But actually, I don't know how to manage sections.
I create sections and it works fine but when I try to add a new row in my section I got a problem.
Example:
I Create a new Item in my first section.
The section name is "Aucun" and the rows label is going to set to "Test 1"
It works!
So, now I want to add something else
The section name is "Produits Laitiers" and the row label is "Test2"
FAIL :( The section is create but the row is not the good one
There is my code for the moment
func numberOfSections(in tableView: UITableView) -> Int {
return arraySection.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return arraySection[section]
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return numberOfRowsInSection[section]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell")
cell?.textLabel?.text = "\(String(describing: products[indexPath.row]["Name"]!))"
cell?.textLabel?.adjustsFontSizeToFitWidth = true
cell?.textLabel?.font = UIFont(name: "Arial", size: 14)
return cell!
}
numberOfRowsInSection is an array of int where I store the number of products which have to be in this section.
You are getting "Test 1" each time, because you have sections, but you are always trying to get a value by using an index of a row without checking an index of a section:
\(String(describing: products[indexPath.row]["Name"]!))
If all your values for cells are storing in a single array, then you should get a value from an array by using the section number:
\(String(describing: products[indexPath.section]["Name"]!))
But this will work only if each section will have only one row. Otherwise you will have to get the number of section first to detect the section where current row is allocated and then get the number of row.
If you have an array of sections with array of rows for each section, that will look like this:
let arraySection = [ [section #0 values], [section #1 values], ...
]
Then you can get the value for each row by using this:
let value = arraySection[indexPath.section][indexPath.row]
========EDIT===========
But there is a much better way - use objects or structs
struct Row {
var value: String = ""
}
struct Section {
var name: String = ""
var values: [Row] = []
}
So, using this structs, your code will be changed:
//your array of sections will contain objects
let arraySection: [Section] = []
func numberOfSections(in tableView: UITableView) -> Int {
return arraySection.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
//get the name of the section
return arraySection[section].name
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//check the count of rows for each section
return arraySection[section].values.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell")
// now we can get the value for current row by checking sections and then rows
cell?.textLabel?.text = arraySection[indexPath.section].values[indexPath.row].value
cell?.textLabel?.adjustsFontSizeToFitWidth = true
cell?.textLabel?.font = UIFont(name: "Arial", size: 14)
return cell!
}
I think the problem is with the products array in the UITableViewDataSource method cellForRowAt. You are accessing the same array element for the first object in each section with
products[indexPath.row]["Name"]!
Probably you need to adjust your model to handle the indexPath.section.
The first thing I going to creates a Section model to keep track of the sections, like the following:
/// Defines a section in data source
struct Section {
// MARK: - Properties
/// The title of the section
let title: String
/// The items in the section
var items: [String]
}
Then I going to use a UITableViewController with an Add right button to insert new section/rows to the UITableView, to add new sections/row I going to use a UIAlertController for the sake of brevity with two UITextField's inside where you need to put the name of the section and the name of the row. At the end should look like the following image:
In case the section already exist is going to add the new row to the section, otherwise, is going to create the new section and row dynamically.
DynamicTableViewController
class DynamicTableViewController: UITableViewController {
// MARK: - Properties
/// The data source for the table view
var dataSource = [Section(title: "Section 1", items: ["Row 1"])]
// MARK: - UIViewController
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return dataSource.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource[section].items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = dataSource[indexPath.section].items[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return dataSource[section].title
}
#IBAction func didTapAddButton(_ sender: Any) {
presentAlerController()
}
}
extension DynamicTableViewController {
func add(_ sectionName: String?, _ row: String?) {
guard let name = sectionName, let rowName = row,
!name.isEmpty, !rowName.isEmpty else {
return
}
if let index = dataSource.index(where: { $0.title == name }) {
dataSource[index].items.append(rowName)
} else {
dataSource.append(Section(title: name, items: [rowName]))
}
tableView.reloadData()
}
func presentAlerController() {
let alertController = UIAlertController(title: "Add", message: "Add new Section/Row", preferredStyle: .alert)
let addAction = UIAlertAction(title: "Add", style: .default) { [weak self] _ in
let sectionTextField = alertController.textFields![0] as UITextField
let rowTextField = alertController.textFields![1] as UITextField
self?.add(sectionTextField.text, rowTextField.text)
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in }
alertController.addTextField { textField in
textField.placeholder = "Add a new section name"
}
alertController.addTextField { textField in
textField.placeholder = "Add a new row"
}
alertController.addAction(addAction)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
}
In the above example, I'm not checking for the existence of duplicate row, you can do it is very easy. Also, I'm using the reloadData(), but you can use too if you want :
// insert the new section with row of the row in the existent section
tableView.beginUpdates()
// insert new section in case of any
insertSections(_ sections: IndexSet, with animation: UITableViewRowAnimation)
// insert new row in section using:
// insertRows(at: [IndexPath], with: UITableViewRowAnimation)
tableView.endUpdates()
Also you need to create a new UIBarButtonItem and connect it to the #IBAction I create it to be able to present the UIAlertController and add new section/row to the UITableView.
I hope this helps you.

Swift: How to access the cells within a UITableView

I currently have UITableView that has one UITableViewCell within it. Within my code I created a button over this cell. When that button is pressed, the cells within it expand.
How would I go about making something expand when one of those cells are selected? Would I need to put a UITableView within the cell?
This is what I would do if I needed to bang something out in a hurry. Let each vegetable occupy its own table section, then expand or contract the section when the user toggles:
// We need some kind of data model.
class Vegetable {
init(name: String, facts: [String]) {
self.name = name
self.facts = facts
}
let name: String
let facts: [String]
var isShown: Bool = false // This flag gets toggled, which is why I've defined it as a class.
}
// And a table view controller configured for dynamic (not static!) cells. Create this in a storyboard, and change the UITableViewControllers class to VeggieTable.
class VeggieTable: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
veggieData = [Vegetable(name: "Tomato", facts: ["Number: 15", "Red Colored"]),
Vegetable(name: "Lettuce", facts: ["Good with peanut butter", "Green", "Crunchy snack"])] // etc.
}
var veggieData: [Vegetable]!
override func numberOfSections(in tableView: UITableView) -> Int {
return veggieData.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let veg = veggieData[section]
return veg.isShown ? veg.facts.count + 1 : 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let veg = veggieData[indexPath.section]
switch indexPath.row {
case 0:
cell.textLabel?.text = veg.name
default:
cell.textLabel?.text = veg.facts[indexPath.row - 1]
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let veg = veggieData[indexPath.section]
// Toggle the is shown flag:
veg.isShown = !veg.isShown
tableView.deselectRow(at: indexPath, animated: true)
// TODO: replace reloadData() with sophisticated animation.
// tableView.beginUpdates()
// tableView.insertRows() / deleteRows() etc etc.
// tableView.endUpdates()
tableView.reloadData()
}
}
Eventually you'd add table animations to make the rows appearing look smooth etc.
Hope that helps.

What's someones preferred method for adding a comments section to a TableViewCell?

I am currently using a tableview to display a individual "Posts." In each tableview cell is a unique Post. I would like to add a collection of "comments" into each cell. The only way I can think about collecting comments is to add another TableView inside the Post's cell. What is some preferred methods to accomplishing this? It seems pretty complicated to use a TableView inside another TableViews Cell.
I don't think it's a good idea.
Create N sections in your table view, one section for every post.
For every section
the cell at row 0 will be populated with the post data
every cell in the following rows will be populated with the comments data.
Example
struct PostModel {
let title: String
let comments: [String]
}
class Posts: UITableViewController {
private var posts: [PostModel] = ... // TODO
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return posts.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return posts[section].comments.count + 1
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let post = posts[indexPath.section]
let cell = tableView.dequeueReusableCellWithIdentifier("StandardCell") ?? UITableViewCell(style: .Default, reuseIdentifier: "StandardCell")
if indexPath.row == 0 {
cell.textLabel?.text = post.title
return cell
} else {
let comment = post.comments[indexPath.row-1]
cell.textLabel?.text = comment
return cell
}
}
}

swift 2 how to remove empty rows in a tableview in a viewcontroller

Is there a way to reset tableview height so that no empty rows are showed. For an example, a tableview displays 3 rows but there is only one row having real data. I'd like the tableview shrinks it size so there is only one row display
I guess you have a View Controller like this
class Controller: UITableViewController {
private var data: [String?] = ["One", nil, "Three", nil, nil]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCellID")!
cell.textLabel?.text = data[indexPath.row]
return cell
}
}
Since your model (data in my example) contains nil values you are getting a table view like this
One
_
Three
_
_
Removing the empty rows
Now you want instead a table view like this right?
One
Three
Filtering your model
Since the UI (the table view) is just a representation of your model you need to change your model.
It's pretty easy
class Controller: UITableViewController {
private var data: [String?] = ["One", nil, "Three", nil, nil]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCellID")!
cell.textLabel?.text = data[indexPath.row]
return cell
}
override func viewDidLoad() {
data = data.filter { $0 != nil }
super.viewDidLoad()
}
}
As you can see, inside viewDidLoad() I removed the bill values from the data array. Now you'll get only cells for real values.
I think you need remove the empty data before execute func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int.
//EDITED
I give you this possible solution, maybe there are more ways to do it, you can try with this.
class MyViewController: UITableViewDataSource, UITableViewDelegate {
private var realHeight = 0
private var data : [String] = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCellID")!
cell.textLabel?.text = data[indexPath.row]
self.realHeight += self.tableView.rowHeight //Increase real height value.
return cell
}
func loadData() { //This function reload the data
self.realHeight = 0
self.tableView.reloadData()
//Update tableViewHeight here, use self.realHeight. You can use constraints or update directly the frame.
}
}
The solution is have a counter that represents the sum of all visibles rows height. e.g If you have n cells then your height will be self.tableView.rowHeight * n, then you ever will have an effective height.
I recommend you to create a constraint for the table height and when the cells change, you only need change the constant of the constraint, is more easy than change the frame.

How to populate data from class model to tableview

My data is coming in modal class named Menu by creating its object menus. Now How can i send the menu names to tableview to show in particular cell
var menus = [Menu]()
for (_, content) in json {
let menu = Menu(id: Int(content["id"].stringValue),
name: content["name"].string,
image: content["image"].string,
coupon: content["coupon"].int,
icon: content["icon"].string,
order: Int(content["order"].stringValue),
aname: content["name"].string,
options: Int(content["options"].stringValue),
subcategory:content["subcategory"].arrayObject)
menus.append(menu)
}
for menu in menus {
print(menu.name)
print(menu.id)
print(menu.subcategory)
}
print(menus.count)
Here All the data is saved in Menu class by the help of menus object.I have added the codes to show data to tableview. here i have created custom tableview and trying to populate it
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return menus.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Menucell", forIndexPath: indexPath)as! MENUTableViewCell
cell.Ordermenu.text = (" \(menus[indexPath.row])")///here its not fetching the value
return cell
}
Its not working . how the implementation should be ?
It shows the projectName.classname
updated after accepting answer
Try below line of code. Hope it will help you...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Menucell", forIndexPath: indexPath)as! MENUTableViewCell
let menu : Menu = menus[indexPath.row]
cell.Ordermenu!.text = menu. name
return cell
}
Do you register the table cells? Try something like this...
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
var cell: MENUTableViewCell! = tableView.dequeueReusableCellWithIdentifier("MENUTableViewCellIDENTIFIER") as? MENUTableViewCell
if cell == nil
{
let nib: UINib = UINib(nibName:"MENUTableViewCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier:"MENUTableViewCellIDENTIFIER")
cell = tableView.dequeueReusableCellWithIdentifier("MENUTableViewCellIDENTIFIER") as! CompactIncidentCellView
//cell.selectionStyle = UITableViewCellSelectionStyle.None
}
// work with cell here...
cell.Ordermenu.text = (" \(menus[indexPath.row])")
return cell
}