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.
Related
I want to display three different entitys from core data on a table view. I can do this with one entity and tried to carry that logic over to displaying three on the same table view. I used the same entity in the code below to test this. I am getting blank sections in my table view. I must have the cellForRowAt method wrong? Here is my code.
var word: [NSManagedObject] = []
let sections = ["Custom Library", "Mastered Words", "Library"]
var array = [
[NSManagedObject](),
[NSManagedObject](),
[NSManagedObject]()
]
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell2")
array = [
word,
word,
word
]}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array[section].count
}
func numberOfSections(in tableView: UITableView) -> Int {
sections.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.sections[section]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let customWord = array[indexPath.row][indexPath.section]
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell2", for: indexPath)
cell.textLabel?.text = customWord.value(forKeyPath: "title") as? String
return cell
Based on the code you've shown, your array property is an array of empty arrays as word is an empty array itself. If you're using array to determine the number of rows in each section based on the array at that section index, then you're going to get 0 rows for each section.
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
}
}
I have a tableView with section headers and I want to append a certain user input to multiple selected headers. I have created the section headers and am able to change the image on the section header to show that it is selected but I want to create an array of those selected.
Here is my code for the section header:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let userModel = Data.userModels[section]
let cell = tableView.dequeueReusableCell(withIdentifier: "nameCell") as! NameHeaderTableViewCell
cell.setup(model: userModel)
cell.checkMarkButton.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
cell.enable(on: false)
if isUserEditing == true {
cell.enable(on: true)
}
return cell.contentView
}
Here is where I change the section image when a user taps the section:
#objc func handleTap(sender: UIButton) {
sender.isSelected = !sender.isSelected
}
When the user clicks the save button, I want the user input to be appended to those cells where the section header is selected. Here is that code:
#IBAction func save(_ sender: Any) {
//VALIDATION
guard mealItemTextField.text != "", let item = mealItemTextField.text else {
mealItemTextField.placeholder = "please enter an item"
mealItemTextField.layer.borderWidth = 1
mealItemTextField.layer.borderColor = UIColor.red.cgColor
return
}
guard priceTextField.text != "", let price = Double(priceTextField.text!) else {
priceTextField.placeholder = "please enter a price"
priceTextField.layer.borderWidth = 1
priceTextField.layer.borderColor = UIColor.red.cgColor
return
}
tableView.reloadData()
}
Currently I am stuck on how to access all of the indexes of the sections that are selected(i.e. the ones that have a state of selected) Here are some screenshots to help visualize the program:
P.S. Not sure if this is helpful but I populate the data with an array of structs. Here is that code:
func numberOfSections(in tableView: UITableView) -> Int {
return Data.userModels.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if Data.userModels[section].isExpandable {
return Data.userModels[section].itemModels.count
} else {
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
}
cell?.textLabel?.text = Data.itemModels[indexPath.row].itemName
return cell!
}
Any help is appreciated, thanks!
The problem is that your checkmarks are merely a visual feature of the header. When the user taps on a checkmark, you're merely toggling its selected state:
#objc func handleTap(sender: UIButton) {
sender.isSelected = !sender.isSelected
}
That's not going to work. You need to be keeping track of this information at all times in a part of your data model that deals with the section headers. That way, when the Save button is clicked, the information is sitting there in the data model waiting for you. Your handleTap needs to work out what section this is the header of and reflect the info off into the model. The data model is the source of truth, not some view in the interface. (I am surprised that you are not already having trouble with this when you scroll your table view.)
Another problem with your code is this:
let cell = tableView.dequeueReusableCell(withIdentifier: "nameCell") as! NameHeaderTableViewCell
You can't use a UITableViewCell as a reusable section header view. You need to be using a UITableViewHeaderFooterView here.
There is a tableview cell and there is a more button which is located on the tableview not in the cell so I want that when the project run it show only five user data in the tableview cell and other user data is show when the user click on the more button(which is located out side the cell) how do I achieved this?
Thanks for help
You can find the prototype cell image
//Mark:TableViewDelegate
extension MembersViewController:UITableViewDelegate,UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return imgarray.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tblView.dequeueReusableCell(withIdentifier: "membTableCell", for: indexPath) as! membTableCell
cell.profiletabImg.image = imgarray[indexPath.row]
cell.namelbl.text = names[indexPath.row]
cell.positionlbl.text = "Ceo of code with color"
return cell
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 40
}
#IBAction func morebutton(sender:UIButton){
}
}
The tableview expands automatically if you add data to the array and call the tableview.reloadData() function.
From the Apple's Doc -
"Call this method to reload all the data that is used to construct the table, including cells, section headers and footers, index arrays, and so on. For efficiency, the table view redisplays only those rows that are visible. It adjusts offsets if the table shrinks as a result of the reload. The table view’s delegate or data source calls this method when it wants the table view to completely reload its data. It should not be called in the methods that insert or delete rows, especially within an animation block implemented with calls to beginUpdates() and endUpdates()."
#IBAction func moreBtnPressed(_ sender: Any) {
tableviewArray.append("Append your array with remaining items")
tableview.reloadData()
}
Return 5 in numberOfRowsInSection method if more button is not tapped.
When more button is tapped reload the tableview and return imgarray.count in numberOfRowsInSection method.
class TableViewController: UITableViewController {
var dataArr = [String]()
var expanded = false
override func viewDidLoad() {
super.viewDidLoad()
dataArr = (1..<25).map({ String($0) })
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "\(dataArr.count - 5) more", style: .plain, target: self, action: #selector(morebutton(_:)))
}
#objc func morebutton(_ sender: UIBarButtonItem) {
self.navigationItem.rightBarButtonItem = nil
expanded = true
tableView.reloadData()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if !expanded && !dataArr.isEmpty {
return 5
} else {
return dataArr.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") ?? UITableViewCell(style: .default, reuseIdentifier: "reuseIdentifier")
cell.textLabel?.text = dataArr[indexPath.row]
return cell
}
}
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.