How to add checkbox to table cell - ios

I am working on a project where I program a shopping app. I have 2 Table Views in a big UIViewController.
As you can see from picture, my 1st table called products and my 2nd table piesa. My 1st table works as a list of the items I have on stock (plafon, plafon cu 2 parti culisante, etc), when i tap one of the items from the table, it will remove it from thereand add it to my 2nd tabel. It works like a shopping list. I implemented this part. I am stuck on the 2nd part.
What do I want now is: on the items I have in table 2, for example the 1st cell plafon to have 4 checkboxes on him. It will work something like that What do you want to do with your item? Replaced, paint it, repair or or repaint it. To have 4 options.
My question is, how do I add checkboxes to each table cell from 2nd table. Is there any way to make checkboxes depend on each other (for example, can't have 2 checkboxes active at same time: repaint and paint, only one or 2 that differ like repair and repaint).
I am a starter newb in iOS, thanks in advance.

You should not have 2 tables in a controller, in fact is not a good practice.
What you need are 2 sections and nothing else. With that, you could use only one table.
The rest is logic problem and nothing else.
You must add a new key in the object, which tells you if you are selected or not.
In your custom cell (I hope and assume that you must be using one). Create a button to which you going to change the state based on the new key that you added (remember this key must be a boolean, for example "isSelected" = true).
The object that you need should be something like:
data =
(
{
sectionName: "Piese disponible"
rows: (
{
name: "Plafon",
isSelected: false //if need this param in this section
},
{
name: "Plafon cu 2 parti culisante",
isSelected: false //if need this param in this section
},
{
name: "etc etc etc",
isSelected: false //if need this param in this section
}
)
},
{
sectionName: "Piese"
rows: (
{
name: "Plafon",
isSelected: false //for the checkbox btn
}
)
}
)
In your didSelectRowAtIndexPath Delegate method you need change the isSelected key.
let cell = tableView.cellForRowAtIndexPath(indexPath) as! yourCustomTableViewCell
let eachRow = yourArray.objectAtIndex(indexPath.row)
let isSelected = eachRow["isSelected"] as! Bool
if isSelected == false {
eachRow.setValue(true, forKey: "isSelected")
}
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: indexPath.row, inSection: indexPath.section)], withRowAnimation: .Fade)
And i you need unselect the last selected cell, create a loop and search the object with the isSelected key in true and set to false and use reloadRowsAtIndexPaths in that cell
Something like:
for eachInfo in yourArray {
let isSelectedRow = eachRow["isSelected"] as! Bool
if isSelectedRow == true {
eachInfo.setValue(false, forKey: "isSelected")
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: i, inSection: theSection)], withRowAnimation: .Fade)
break
}
i += 1
}
However, considering that the cells are reused and must rendering your button with the new state must put your logic in cellForRowAtIndexPath delegate method.
Something Like:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "yourcustomCellId"
var cell: yourCustomTableViewCell! = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? yourCustomTableViewCell
if cell == nil {
tableView.registerNib(UINib(nibName: "yournibCellname", bundle: nil), forCellReuseIdentifier: cellIdentifier)
cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as? yourCustomTableViewCell
}
cell.selectionStyle = UITableViewCellSelectionStyle.None
let eachRow = yourArray.objectAtIndex(indexPath.row)
let isSelected = eachRow["isSelected"] as! Bool
var imageNamebtn = "unselectedImage"
if isSelected == true {
imageName = "selectedImage"
}
cell.yourButon.backgroundImage = UIImage(named: imageName)
return cell
}
For Build The count of sections you need use numberOfSectionsInTableView from UITableViewDataSource and for build the section design need viewForHeaderInSection.
I hope I've helped

You are going to need to make a custom cell. You can change whatever you want to the UI for the custom cell and then hook up the exclusive logic inside your code. Here is a tutorial on how to use custom cells. Skip down to the part that starts with Designing Your Own Prototype Cells. Hope this helps!

Related

Check if at least one cell is selected in each section of a UITableView

I want to create a simple method that will determine whether or not at least one cell in each section is selected.
Something like this:
func checkSections () ->Bool {
let array : [IndexPath] = self.selectedIndexPaths.copy() as! [IndexPath];
for indexPath: IndexPath in array {
let SectionString : String? = self.sectionHeaderTitleArray[indexPath.section] // this will return the names of the selected sections but no way to use that
// check all the sections and see if they contain a selected cell
}
}
I know it's incomplete, but I have no idea how to start creating it and I'm hoping to find help to start the func at least. Please understand that I haven't touched anything programming related in 5+ years and I was never a professional
You can get the selected indexPaths as follows:
let array: [IndexPath] = tableView.indexPathsForSelectedRows
And then something like:
for indexPath: IndexPath in array {
if let section = indexPath.section {
// yes, we have a selected cell in this section
}
}
This should do the trick
/// Checks if the number of selected sections equals
/// the number of sections in the table view
func checkSections() -> Bool {
let selectedSections =
Set(tableView.indexPathsForSelectedRows?.map { $0.section } ?? [])
return selectedSections.count == tableView.numberOfSections
}
You also need to set tableView.allowsMultipleSelection = true to allow multiple selections in multiple sections of your table view.

Showing and hiding a view only on a specific cell of a table view

I have a table view with custom cells. They are quite tall, so only one cell is completely visible on the screen and maybe, depending on the position of that cell, the top 25% of the second one. These cells represent dummy items, which have names. Inside of each cell there is a button. When tapped for the first time, it shows a small UIView inside the cell and adds the item to an array, and being tapped for the second time, hides it and removes the item. The part of adding and removing items works fine, however, there is a problem related to showing and hiding views because of the fact that cells are reused in a UITableView
When I add the view, for example, on the first cell, on the third or fourth cell (after the cell is reused) I can still see that view.
To prevent this I've tried to loop the array of items and check their names against each cell's name label's text. I know that this method is not very efficient (what if there are thousands of them?), but I've tried it anyway.
Here is the simple code for it (checkedItems is the array of items, for which the view should be visible):
if let cell = cell as? ItemTableViewCell {
if cell.itemNameLabel.text != nil {
for item in checkedItems {
if cell.itemNameLabel.text == item.name {
cell.checkedView.isHidden = false
} else {
cell.checkedView.isHidden = true
}
}
}
This code works fine at a first glance, but after digging a bit deeper some issues show up. When I tap on the first cell to show the view, and then I tap on the second one to show the view on it, too, it works fine. However, when I tap, for example, on the first one and the third one, the view on the first cell disappears, but the item is still in the array. I suspect, that the reason is still the fact of cells being reused because, again, cells are quite big in their height so the first cell is not visible when the third one is. I've tried to use the code above inside tableView(_:,cellForRow:) and tableView(_:,willDisplay:,forRowAt:) methods but the result is the same.
So, here is the problem: I need to find an EFFICIENT way to check cells and show the view ONLY inside of those which items are in the checkedItems array.
EDITED
Here is how the cell looks with and without the view (the purple circle is the button, and the view is the orange one)
And here is the code for the button:
protocol ItemTableViewCellDelegate: class {
func cellCheckButtonDidTapped(cell: ExampleTableViewCell)
}
Inside the cell:
#IBAction func checkButtonTapped(_ sender: UIButton) {
delegate?.cellCheckButtonDidTapped(cell: self)
}
Inside the view controller (NOTE: the code here just shows and hides the view. The purpose of the code is to show how the button interacts with the table view):
extension ItemCellsTableViewController: ItemTableViewCellDelegate {
func cellCheckButtonDidTapped(cell: ItemTableViewCell) {
UIView.transition(with: cell.checkedView, duration: 0.1, options: .transitionCrossDissolve, animations: {
cell.checkedView.isHidden = !cell.checkedView.isHidden
}, completion: nil)
}
EDITED 2
Here is the full code of tableView(_ cellForRowAt:) method (I've deleted the looping part from the question to make it clear what was the method initially doing). The item property on the cell just sets the name of the item (itemNameLabel's text).
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier:
ItemTableViewCell.identifier, for: indexPath) as? ItemTableViewCell{
cell.item = items[indexPath.row]
cell.delegate = self
cell.selectionStyle = .none
return cell
}
return UITableViewCell()
}
I've tried the solution, suggested here, but this doesn't work for me.
If you have faced with such a problem and know how to solve it, I would appreciate your help and suggestions very much.
Try this.
Define Globally : var arrIndexPaths = NSMutableArray()
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 30
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = self.tblVW.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.textLabel?.text = String.init(format: "Row %d", indexPath.row)
cell.btn.tag = indexPath.row
cell.btn.addTarget(self, action: #selector(btnTapped), for: .touchUpInside)
if arrIndexPaths.contains(indexPath) {
cell.backgroundColor = UIColor.red.withAlphaComponent(0.2)
}
else {
cell.backgroundColor = UIColor.white
}
return cell;
}
#IBAction func btnTapped(_ sender: UIButton) {
let selectedIndexPath = NSIndexPath.init(row: sender.tag, section: 0)
// IF YOU WANT TO SHOW SINGLE SELECTED VIEW AT A TIME THAN TRY THIS
arrIndexPaths.removeAllObjects()
arrIndexPaths.add(selectedIndexPath)
self.tblVW.reloadData()
}
I would keep the state of your individual cells as part of the modeldata that lies behind every cell.
I assume that you have an array of model objects that you use when populating you tableview in tableView(_:,cellForRow:). That model is populated from some backend service that gives you some JSON, which you then map to model objects once the view is loaded the first time.
If you add a property to your model objects indicating whether the cell has been pressed or not, you can use that when you populate your cell.
You should probably create a "wrapper object" containing your original JSON data and then a variable containing the state, lets call it isHidden. You can either use a Bool value or you can use an enum if you're up for it. Here is an example using just a Bool
struct MyWrappedModel {
var yourJSONDataHere: YourModelType
var isHidden = true
init(yourJSONModel: YourModelType) {
self.yourJSONDataHere = yourJSONModel
}
}
In any case, when your cell is tapped (in didSelectRow) you would:
find the right MyWrappedModel object in your array of wrapped modeldata objects based on the indexpath
toggle the isHidden value on that
reload your affected row in the table view with reloadRows(at:with:)
In tableView(_:,cellForRow:) you can now check if isHidden and do some rendering based on that:
...//fetch the modelObject for the current IndexPath
cell.checkedView.isHidden = modelObject.isHidden
Futhermore, know that the method prepareForReuse exists on a UITableViewCell. This method is called when ever a cell is just about to be recycled. That means that you can use that as a last resort to "initialize" your table view cells before they are rendered. So in your case you could hide the checkedView as a default.
If you do this, you no longer have to use an array to keep track of which cells have been tapped. The modeldata it self knows what state it holds and is completely independent of cell positions and recycling.
Hope this helps.

UISegmentControl with UITableView - UITableViewCell layout remains same on changing the segment

In the scenario mentioned in the question title, on changing the segment, ideally the UITableView should reload and hence the UITableViewCell should also reload. The issue is, all the content gets updated like label texts. But if I have expanded a subview of cell in one segment, it remains still expanded after segment is changed.
Segment index change function :
#IBAction func segmentOnChange(sender: UISegmentControl)
{
// Few lines
// self.tableMenu.reloadData()
}
Screenshot 1 :
Screenshot 2 :
So ideally, in screenshot 2, the cart view should have been collapsed.
Update :
Show/Hide view :
func showHideCartView(sender: UIButton)
{
let cell = self.tableMenu.cellForRow(at: IndexPath(row: 0, section: Int(sender.accessibilityHint!)!)) as! RestaurantMenuItemCell
if sender.tag == 1
{
// Show cart view
cell.buttonArrow.tag = 2
cell.viewAddToCart.isHidden = false
cell.constraint_Height_viewAddToCart.constant = 50
cell.buttonArrow.setImage(UIImage(named: "arrowUp.png"), for: .normal)
}
else
{
// Show cart view
cell.buttonArrow.tag = 1
cell.viewAddToCart.isHidden = true
cell.constraint_Height_viewAddToCart.constant = 0
cell.buttonArrow.setImage(UIImage(named: "arrowDown.png"), for: .normal)
}
self.tableMenu.reloadData()
}
cellForRowAtIndexPath :
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantMenuItemCell", for: indexPath) as! RestaurantMenuItemCell
cell.selectionStyle = .none
let menuItem = self.menuItems[indexPath.section]
cell.imageViewMenuItem.image = UIImage(named: "recommend0#2x.png")
cell.labelName.text = menuItem.name
cell.labelDescription.text = menuItem.description
cell.labelPrice.text = String(format: "$%i", menuItem.price!)
cell.buttonArrow.accessibilityHint = String(format: "%i", indexPath.section)
cell.buttonArrow.addTarget(self, action: #selector(self.showHideCartView(sender:)), for: .touchUpInside)
return cell
}
Since you rely on the sender button's tag to determine whether the cell should be shown in expanded or collapsed state, you need to make sure that when the segment changes, the tags for all the cells' buttonArrow also change to 1.
Unless that happens, the cells will be reused and since the buttonArrow's tag is set to 2, it will be shown as expanded.
You are reusing cells, see first line of your cellForRowAt implementation:
let cell = tableView.dequeueReusableCell(withIdentifier: "RestaurantMenuItemCell", for: indexPath) as! RestaurantMenuItemCell
That means that a tableView does not create a new cell, nor it redraws it unless necessary for the given indexPath. However, in the code you expand a cell by setting some height constraints and isHidden flags on subviews of the cell. Now once you reload table at a new segment, the tableView will use the already rendered cells to make the reload as efficient as possible. However, that means, that although you will change the data in the cell, unless you explicitly collapse it, it will stay expanded (because previously you have set the constraints to expanded height).
You need to reset the expanded state when you change the segment. Now let's elaborate, because I believe your current solution has a hidden bug in it:
First of all, since as I said you are dequeueing the cells, in case there are a lot of items in the table and you are scrolling through them, there is a high chance that the expanded cell will get reused even somewhere later in the table view. So it will seem like there is some random item expanded too.
Therefore I suggest you to provide some model that would remember which cells are supposed to be expanded and then use the information in this model to update state of each cell before you return them in your cellForRowAt. The model can be for example a simple array of integer values that would represent indexes of cells that are expanded - so for example if there is an index 3 in the array, that would mean that cell at row 3 should be expanded. Then when you dequeue a cell in cellForRowAt for indexPath with row = 3 you should set constraints on that cell before returning it. This way you can remove the bug I mentioned. Moreover, then when you change segments, you can just remove all the indexes from the array to signify that no cell should be expanded anymore (do it before calling tableView.reloadData()).
What happened here, When you click any row then It will be expanded based on make true first condition in showHideCartView method and after that when you are changing segment index that keep height of previous row as it is because you are reusing cell So you must set normal hight of each row when you change segment index.
For do that take object of indexPath like
var selectedIndexPath: NSIndexPath?
Then set selectedIndexPath in showHideCartView method
func showHideCartView(sender: UIButton) {
selectedIndexPath = NSIndexPath(row: 0, section: Int(sender.accessibilityHint!)!)) // you can change row and section as per your requirement.
//// Your other code
}
Then apply below logic whenever you are changing segment index.
#IBAction func segmentOnChange(sender: UISegmentControl)
{
if indexPath = selectedIndexPath { // check selectedIndexPath is not nil
let cell = self.tableMenu.cellForRow(at: indexPath) as! RestaurantMenuItemCell
cell.buttonArrow.tag = 1
cell.viewAddToCart.isHidden = true
cell.constraint_Height_viewAddToCart.constant = 0
cell.buttonArrow.setImage(UIImage(named: "arrowDown.png"), for: .normal)
}
selectedIndexPath = nil // assign nil to selectedIndexPath
// Few lines
self.tableMenu.reloadData() // Reload your tableview
}

Text is replacing while scrolling tableview

I need to increment decrement label value of a particular row. I am using following method, it is working but when I scroll the table view then values are replacing with other's row.
Like I am showing two row on a screen according to design so while scrolling, first row's label value is replacing with forth row label and so on.
#IBAction func plusBtnAction(_ sender: UIButton) {
var cell:TableViewCell = self.tebleView.dequeueReusableCell(withIdentifier: "Cell")! as UITableViewCell as! TableViewCell
let indexPath = IndexPath(row: sender.tag, section: 0)
cell = (self.tebleView.cellForRow(at: indexPath) as? TableViewCell)!
if cell.tag == sender.tag {
print(sender.tag)
print(cell.countLbl.tag)
cell.countLbl.text = "\(Int(cell.countLbl.text!)! + 1)"
}
}
Remember, you are reusing the cells - dequeueReusableCell each time you need to display a new cell, so what's happening here is the first row disappears and when it is redisplayed, you are using the cell that was used for row 4.
The solution to this is to keep a track of your counter in an array outside of the tableView (table, not teble :-) ) Something like this...
var tableDataCounters : [Int] = []
// set up your data ...
cell.countLbl.text = "\(tableDataCounters[indexPath.row] + 1)"
By using dequeueReusableCell(withIdentifier: ) method of UITableView, you are initially creating cells and later on reusing those created cells. You are facing this problem due to this. Since you have only one row whose text you need to change, it's better to keep the text for that row stored externally. For the plusBtnAction, it's better to do the following:
var cellLabelTextArray: [String] = [] // populate this with data for each cell
#IBAction func plusBtnAction(_ sender: UIButton) {
var cell:TableViewCell = self.tebleView.dequeueReusableCell(withIdentifier: "Cell")! as UITableViewCell as! TableViewCell
let indexPath = IndexPath(row: sender.tag, section: 0)
var cellLabelText = cellLabelTextArray[indexPath.row]
cellLabelText = "\(Int(cellLabelText) + 1)"
cellLabelTextArray[indexPath.row] = cellLabelText
if let visiblePaths: [IndexPath] = self.tableView.indexPathsForVisibleRows {
if visiblePaths.contains(indexPath) {
self.tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
}
}
}
In the above code, we are checking if the indexPath is visible. If it is, then reload that particular indexPath. Otherwise, we don't need to.

Custom UITableviewCells displayed correctly but only one in the section is the correct class

I apologize for the poorly worded question but I simply cannot think of why/how to describe this bug in a short manner.
Basically I have a table view controller displaying two sections, each with a respective custom UITableViewCell subclass. They are displayed perfectly, with the correct model data.
The problem arises when I hit the "done" button, my program traverses through all the cells, gathering the data from each cell and updating the model. The first section goes off without a hitch... but the second section is where trouble hides. The first cell of this section can be casted into the proper subclass, yet any other additional cells don't pass this conditional test and their reuse identifier is nil. I tried checking the cells when they're dequeued but they are being created/reused properly. My code is below and any suggestions will aid my sanity.
Here is the part of my helper function that traverses the cells:
for i in 0..<tableView.numberOfSections {
for j in 0...tableView.numberOfRowsInSection(i) {
let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: j, inSection: i))
// This section seems to work fine
if(i == 0){
if let gradeCell: PercentTableViewCell = cell as? PercentTableViewCell {
print("retrieveCellInfo: gradeCell - \(gradeCell.getPercent())")
newPercentages.append(gradeCell.getPercent())
}
} else if (i == 1){
print("Else if i == 1 : j == \(j) : \(cell?.reuseIdentifier!)") // Prints "category" for the first cell and "nil" for any other
// This is the cast conditional that only lets one cell through
if let catCell = cell as? EditTableViewCell{
newCategories.append(Category(name: catCell.category!, weight: catCell.weight!, earned: catCell.earned!, total: catCell.total!))
}
}
}
}
Here is my delegate function which works perfectly as far as I can tell. As I said, the UI is displayed correctly:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
if(indexPath.section == 0){
/* Grade Parameter Cells */
let cell: PercentTableViewCell = tableView.dequeueReusableCellWithIdentifier("gradeParam", forIndexPath: indexPath) as! PercentTableViewCell
var gradePercentages = course?.getGradePercentages()
// Make sure percentages array is sorted correctly
gradePercentages?.sortInPlace() {
return $0 > $1
}
// Update cell's member variables
cell.initialize(letters[indexPath.row], percent: gradePercentages![indexPath.row])
return cell
} else {
/* Category Cells */
let catcell: EditTableViewCell = tableView.dequeueReusableCellWithIdentifier("category", forIndexPath: indexPath) as! EditTableViewCell
let category = categories![indexPath.row]
catcell.setLabels(category.name, earned: category.earned, total: category.total, weight: category.weight)
return catcell
}
}
The problem arises when I hit the "done" button, my program traverses
through all the cells, gathering the data from each cell and updating
the model
This is your problem. Your cells should reflect your data model, they cannot be relied upon to hold data as once the cell is offscreen it may be re-used for a row that is onscreen.
If data is updated by the user interacting with the cells then you should update your data model as they do that. You can use a temporary store if you don't want to commit changes immediately.

Resources