Volodymyr
Volodymyr

Reputation: 1442

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

image

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!

Upvotes: 1

Views: 774

Answers (1)

flanker
flanker

Reputation: 4200

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
   }
}


Upvotes: 1

Related Questions