summing values on UITableView Cells swift - ios

I have a TableView in a view controller and a UILabel in the UIViewController too. Everything gets displayed well but I am trying to achieve something and I cannot simply get the Logic behind it.
The tableview cell has a UILabel that has Int values on the cells, thiese numbers varies now how do I get the sum of the numbers on the Label in the tableView cell and display on the Label in the view controller.
I have tried creating a variable in the UIView controller and adding this value to t but I am unable to do that because I cannot add this number together
any help as to how to do this. Thanks
var total: Double?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! BrokageCheckoutCell
cell.configure()
total = Double("\(cell.subPrice.text!)")
cell.selectionStyle = .none
return cell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: cellId2, for: indexPath) as! OtherCheckoutCell
cell.configure()
cell.selectionStyle = .none
return cell
default: break
}
return UITableViewCell()
}

You ask:
how do I get the sum of the numbers on the Label in the tableView cell and display on the Label in the view controller
In short, you don’t. The label in the table view cell (which is part of the “view”) is not our source of data. It’s only used to show the individual strings.
For example, let’s say your app has 100 items to be summed, but the app’s table view shows, say, only 12 rows at a time. As a memory and performance improvement, iOS will reuse cells that scroll out of view in order to show the contents for new rows that scroll into view. But this means that you can’t say “sum all of the contents of the labels in those 100 cells” because there aren’t 100 cells; there are only 12.
Your app should instead refer to its “model”, that structure (e.g., an array) that cellForRowAt used to determine what to show in a given table view cell. So, if you want to add up a grand total, you should add up the values in that array, not referring to the cells at all.
And if your cell has controls that allow data to be modified, then the cell should initiate the update of the model. Then, the process of calculating the grand total (summing the values in the array) still works.
So, let’s consider an example:
You should have a model that contains the numbers that you want to sum. Your example is a little unclear, so I’ll create an example where each table row contained a name, a unit price, and a quantity:
struct Item {
let name: String
let unitPrice: Decimal
var quantity: Decimal
}
In this example, the total for any given row will be the quantity times the unit price. And the grand total for all the rows will be the sum of all of those products.
Then your view controller might have an array of those for its model:
var items: [Item] = ...
Then when you want to update your grand total label, you’d just have a method that calculated the quantity times the price for the total for each row and then sum up those values to calculate the grand total. It will then update the grand total text field accordingly:
private extension ViewController {
func updateTotal() {
let total = items
.map { $0.quantity * $0.unitPrice }
.reduce(0, +)
totalLabel.text = totalFormatter.string(for: total)
}
}
Note, I am not retrieving these numbers from the cells (because those might be reused), but rather I’m retrieving all the data I need from the model.
The key here, though, is if your cell, for example, allowed the user to change one of those values, you need a mechanism to update the table view controller’s model. We’ll use a protocol for that:
protocol ItemCellDelegate: class {
func cell(_ cell: ItemCell, didUpdateQuantity quantity: Decimal)
}
class ItemCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var quantityTextField: UITextField!
#IBOutlet weak var unitPriceLabel: UILabel!
static let quantityFormatter: NumberFormatter = ...
static let priceFormatter: NumberFormatter = ...
weak var delegate: ItemCellDelegate?
}
Obviously, when you configure the cell, the view controller will specify the delegate for updates to the text field (or whatever):
// MARK: Public methods
extension ItemCell {
func configure(name: String, quantity: Decimal, unitPrice: Decimal, delegate: ItemCellDelegate) {
self.delegate = delegate
nameLabel.text = name
quantityTextField.text = ItemCell.quantityFormatter.string(for: quantity)
unitPriceLabel.text = ItemCell.priceFormatter.string(for: unitPrice)
}
}
And when the ItemCell gets updates, it just calls the delegate:
// MARK: Private methods
private extension ItemCell {
#IBAction func didUpdateQuantity(_ sender: UITextField) {
var quantity: Decimal?
if let text = sender.text, let value = ItemCell.quantityFormatter.number(from: text) {
quantity = value.decimalValue
}
delegate?.cell(self, didUpdateQuantity: quantity ?? 0)
}
}
Then, when the view controller configured the cell, it would supply itself as the delegate:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
let item = items[indexPath.row]
cell.configure(name: item.name, quantity: item.quantity, unitPrice: item.unitPrice, delegate: self)
return cell
}
}
And, of course, the view controller would conform to that protocol to accept updates to the text field and update the model:
extension ViewController: ItemCellDelegate {
func cell(_ cell: ItemCell, didUpdateQuantity quantity: Decimal) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
items[indexPath.row].quantity = quantity
updateTotal()
}
}
And, as you can see, if can, after updating the model, it can automatically update the total, too.
The key here is that we never use the cells as a place where we hold data. The cells are just for (a) displaying data and (b) informing the view controller if there are any updates it needs to be aware of.
We strive for a very clear separation of responsibilities, where “views” are for showing and interacting with controls presently on screen and the “model” contains all of our data.

The usual way to do this is to have a delegate of the cell, and make your ViewController this delegate. Whenever the value of the cell changes, send this value through the delegate so that the view controller can add up all these values.
That's the quick way.
The better way to do this is to have a datasource for your cells that keeps track of the values. This is better, because as the cells scroll on and off the screen, the datasource can keep track of the values and restore them when that index path becomes visible again.
Since the data source knows what the values are, it's trivial to sum them.

Related

Swap and reload data in tableView for different Realm object

Problem I want to allow users to hit 'swap' in a table cell and then find a different Realm object to populate the 2 text labels (for exercise name and number of reps) in the cell with the values from the new object.
Research There's quite a bit (admittedly old) on 'moving rows' (e.g. here How to swap two custom cells with one another in tableview?) and also here (UITableView swap cells) and then there's obviously a lot on reloading data in itself but I can't find anything on this use case.
What have I tried my code below works fine for retrieving a new object. i.e. there's some data in the cell, then when you hit the 'swapButton' it goes grabs another one ready to put in the tableView. I know how to reload data generally but not within one particular cell in situ (the cell that the particular swap button belongs to... each cell has a 'swap button').
I'm guessing I need to somehow find the indexRow of the 'swapButton' and then access the cell properties of that particular cell but not sure where to start (I've played around with quite a few different variants but I'm just guessing so it's not working!)
class WorkoutCell : UITableViewCell {
#IBOutlet weak var exerciseName: UILabel!
#IBOutlet weak var repsNumber: UILabel!
#IBAction func swapButtonPressed(_ sender: Any) {
swapExercise()
}
func swapExercise() {
let realmExercisePool = realm.objects(ExerciseGeneratorObject.self)
func generateExercise() -> WorkoutExercise {
let index = Int(arc4random_uniform(UInt32(realmExercisePool.count)))
return realmExercisePool[index].generateExercise()
}
}
//do something here like cell.workoutName
//= swapExercise[indexRow].generateExercise().name???
}
Hold your objects somewhere in a VC that shows UITableView. Then add the VC as the target to swap button. Implement swapping objects on button press and reload data of table view after.
The whole idea is to move logic to view controller, not in separate cell.
There are 2 ways.
1. Adding VS as button action target.
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ... // get cell and configure it
cell.swapBtn.addTarget(self, action: #selector(swapTapped(_:)), for: .touchUpInside)
return cell
}
func swapTapped(_ button: UIButton) {
let buttonPosition = button.convertPoint(CGPointZero, toView: self.tableView)
let indexPath = self.tableView.indexPathForRowAtPoint(buttonPosition)!
// find object at that index path
// swap it with another
self.tableView.reloadData()
}
Make VC to be delegate of cell. More code. Here you create protocol in cell and add delegate variable. Then when you create cell you assign to VC as delegate for cell:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ... // get cell and configure it
cell.delegate = self
return cell
}
func swapTappedForCell(_ cell: SwapCell) {
// the same logic for swapping
}
Solution from OP I adapted the code here How to access the content of a custom cell in swift using button tag?
Using delegates and protocols is the most sustainable way to achieve this I think.
I hope this helps others with the same problem!

Swift - How to get the value of a custom cell label when pressing a button in a different custom cell?

Hit a bit of a brick wall with this problem.
I have a TableView which of dynamic prototype. The TableView has 2 sections.
Section 1 loads a custom TableViewCell from a xib file. The cell contains a stepper and a label:
class quantityTableViewCell: UITableViewCell {
#IBOutlet weak var quantityLabel: UILabel!
#IBAction func quantityStepper(_ sender: UIStepper) {
quantityLabel.text = String(Int(sender.value))
}
}
Section 2 loads another custom TableViewCell which contains just a button:
class addToBasketTableViewCell: UITableViewCell {
#IBOutlet weak var submitButton: UIButton!
}
Now in my TableView class where both cells are being loaded in their own sections, I want to capture the current value of 'quantityLabel' inside the first section when I click the button in the second section and print the result to console.
For example, if I step the value to 5, when I hit the 'submitButton' it prints '5'.
I'm a bit unsure how to go about this, any guidance would be great. Below is a copy of the cells being loaded:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item: ItemPreBasketModel = cellItems[indexPath.row] as! ItemPreBasketModel
if indexPath.section == 0 {
let quantityCell = Bundle.main.loadNibNamed("quantityTableViewCell", owner: self, options: nil)?.first as! quantityTableViewCell
return quantityCell
} else if indexPath.section == 1 {
let addToBasketCell = Bundle.main.loadNibNamed("addToBasketTableViewCell", owner: self, options: nil)?.first as! addToBasketTableViewCell
return addToBasketCell
}
}
It should be something like this:
let path = IndexPath(item: 0, section: 0)
let cell = table.cellForRow(at: path) as? quantityTableViewCell
print(cell?.quantityLabel.text)
replace "table" with your table object.
You should never rely on a value from a cell, because cells might appear and disappear when the user scrolls the table view.
Instead, you should store the value of the stepper in a model, and when the user taps on the button, read the value from the model (and not from any cell).
So:
When quantityStepper is called, update the label and inform some delegate (e.g. the hosting view controller) that the value changed. Be aware:
You should not update the model directly from within the quantityTableViewCell
Instead, you should send a message (= your own Protocol) to some delegate (which implements this Protocol) to inform it that the value changed to some value
The delegate (maybe your view controller) will store this value somewhere
When addToBasketTableViewCell is called, you should also inform the delegate about this. The delegate (your view controller) then will then work with the value that he got in 3. and do whatever has to be done.
With this approch, the cells are decoupled from each other. You don't have any problems with cell reusage, and you can initialize the cells properly because the value is always stored in the model, and the cells only display it. Updates are always reflected to the model.

Content of a cell in static tableview isn't shown SWIFT 3

Here is my implementation of tableView(_:cellForRowAt:):
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let index = indexPath.section
let weekDay = WeekDays.day(at: index)
if self.availability.numberOfTimeslots(for: weekDay) == 0 {
let cell = NotSelectedCell(style: .default, reuseIdentifier: nil)
return cell
}
return UITableViewCell()
}
Here is my code for my custom table view cell:
class NotSelectedCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.backgroundColor = .red
self.textLabel?.numberOfLines = 0
self.textLabel?.textAlignment = .center;
self.textLabel?.text = "Not Available"
}
}
I've also tried initializing custom cell cell = NotSelectedCell() the result is the same. The content isn't shown. dataSource or viewDelegate aren't the problem as I'm working with UITableViewController.
Here's an image
The problem is awakeFromNIB "prepares the receiver for service after it has been loaded from an Interface Builder archive, or nib file." But you're instantiating this programmatically, so that method isn't called. You could theoretically move the code to init(style:reuseIdentifier:), make sure to call super in your implementation, and do any additional customization after that point.
But, you generally wouldn't programmatically instantiate cells when using static cells. (It's the point of static cells, that IB takes care of everything for you.) You generally don't implement UITableViewDataSource at all when using static cells.
I would advise using dynamic table and have two cell prototypes, one with reuse identifier of "NotAvailable" and one with "Available" (or whatever identifiers you want). Then programmatically instantiate the cell with the appropriate identifier. (By the way, this also has the virtue that your cell with "NotAvailable" can be designed entirely in IB, and no code is needed, for that cell at least.) This way, the storyboard takes care of instantiating the appropriate cell.
So, here I have two cell prototypes in my dynamic table, one for "not available" and one for "available":
Then the code would look at the model to figure out which to instantiate:
// for the complicated cell where I want to show details of some window of availability, add IBOutlets for that cell's labels
class AvailableCell: UITableViewCell {
#IBOutlet weak var startLabel: UILabel!
#IBOutlet weak var stopLabel: UILabel!
#IBOutlet weak var doctorLabel: UILabel!
}
// some super simple model to represent some window of availability with a particular doctor in that office
struct Availability {
let start: String
let stop: String
let doctor: String
}
class ViewController: UITableViewController {
let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
let available = ...
override func numberOfSections(in tableView: UITableView) -> Int {
return days.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return available[days[section]]?.count ?? 1
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return days[section]
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// see if there are any available windows for the given day, if not, return "not available" cell
guard let availabilities = available[days[indexPath.section]] else {
return tableView.dequeueReusableCell(withIdentifier: "NotAvailable", for: indexPath)
}
// otherwise, proceed with the more complicated "Available" cell where I have to populate various labels and the like
let cell = tableView.dequeueReusableCell(withIdentifier: "Available", for: indexPath) as! AvailableCell
let availability = availabilities[indexPath.row]
cell.startLabel.text = availability.start
cell.stopLabel.text = availability.stop
cell.doctorLabel.text = availability.doctor
return cell
}
}
And that would yield:
Now, clearly, I just whipped up a super primitive model, and didn't do any UI design in the "available" cell prototype other than inserting three labels. But it illustrates the idea: If your dynamic table has multiple unique cell designs, just implement cell prototypes for each with unique identifiers and instantiate the appropriate one. And this way, you enjoy full cell reuse, minimize how much visual design you have to do programmatically, etc.
You are not supposed to use the cellForRow:atIndexPath method when using static cells. The cells are static, so the loading flow is different. What i'd suggest is to connect the cells individually from the interface builder to your view controller.
STILL, if you want to do it this way you have to get your cells by calling "super" since that's the class who is actually generating your static cells.
UITableView with static cells without cellForRowAtIndexPath. How to set clear background?
EDIT:
I just noticed that this is wrong:
if self.availability.numberOfTimeslots(for: weekDay) == 0 {
let cell = NotSelectedCell(style: .default, reuseIdentifier: nil)
return cell
}
You have to use the "dequeueReusable" method or something. Then again, these are STATIC Cells, so you should just be linking the cells directly from the interface builder.

Trying to calculate the the price of product in Swift3 Tableview controller

I am trying to calculate the total of product price all the products. So, I am able to get the total but it is generating array of ouput. But I only need a final total.
This is my git repo:[(github.com/sulkytejas/shopping-cart)]
code:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cartCell", for: indexPath)
let order = orders?[indexPath.row]
let prices = [order?.product?.price]
print("in table view")
for price in prices {
total += Double(price!)
}
print (total)
cell.textLabel?.text = order?.product?.name
return cell
}
Output :
in table view
1810636.0
in table view
1810676.0
in table view
1810766.0
in table view
1810806.0
in table view
1810821.0
in table view
1810911.0
in table view
1810951.0
You need to change readOrdersFromArchive function in orders.swift file. If you don't have order history, this function always returns nil value. That is why you can not append any order in the orders array in your addToCartPressed function.
class func readOrdersFromArchive() -> [Order]?
{
return NSKeyedUnarchiver.unarchiveObject(withFile: archiveFilePath()) as? [Order] ?? [Order]()
}
I was able to see the total value after adding the code below.
var total = 0.0
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
orders = Orders.readOrdersFromArchive()
if let orders = orders {
for order in orders {
total += (order.product?.price!)!
}
}
print(total)
}
I just moved the for loop from cellForRowAtIndexPath to viewWillAppear. It is not a proper place to calculate total value because it gets called every time tableview configure its cell.
let prices = [order?.product?.price]
What's going on here? This looks like an order has only one product? And you wrap the price of that one product in an array resulting in an array with a single price?
I have no idea what your interface is here, but it seems like you want something more like:
let prices = order.products.map() { $0.price }
This would get all the products an array, then make a new array with the price of each product.
Your array of output has come from your code being run for each cell you have in your table view, which from your output seems to be 7.
If you only want to output the final total then you need to call
print (total)
outside of the tableView() function. An ideal place would be in theviewDidLoad() function.
The total should be computed outside the tableview delegates, because as mentioned by #Paulw11, it will be called for every row every time you scroll or view the tableview. Best option would be to create a method for computing the total in the model itself, or in an extension to the model from where you are populating the table.
What is the datatype of orders here?
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cartCell", for: indexPath)
let order = orders?[indexPath.row]
let prices = [order?.product?.price]
let localTotal:Double = 0.0
print("in table view")
for price in prices {
localTotal += Double(price!)
}
print (localTotal)
total = localTotal
print (total)
cell.textLabel?.text = order?.product?.name
return cell
}

Init custom UITableViewCell from nib without dequeueReusableCellWithIdentifier

SWIFT
I need to make an array of cells. I have few custom cell classes (inheritated from UITableViewCell) with nib files.
How to init cell without registering nib in tableview and doing dequeueReusableCellWithIdentifier?
I did it like this, but don't think, that it will work:
var labelCell = CustomCellClass.initialize()
I'm inferring from the discussion in comments elsewhere that the reason you want to not allow cells to be dequeued and reused is that you're having trouble keeping track of user input captured in the cells.
The bottom line is that you really should allow the cells to be dequeued and reused and just handle that appropriately. If you're having problems with cells being reused, this can be resolved by separating the “model” (i.e. your data) from the “view” (i.e., the UIKit controls). This is the spirit of the model-view-controller pattern, but is true in any of those patterns that have separation of concerns (e.g., MVVP, MVP, etc.).
The key is that as values change in the cell, your cell should immediately tell the view controller so that the view controller can update the model immediately. Then, when the view controller needs to later do something with the value associated with a particular row, it doesn't retrieve it from the cell, but rather from its own model.
So, I might define a protocol for the cell to inform the table view that its text field changed:
protocol CustomCellDelegate: class {
func cell(_ cell: CustomCell, didUpdateTextField textField: UITextField)
}
And I'd then define a cell class that called that delegate:
class CustomCell: UITableViewCell {
weak var delegate: CustomCellDelegate?
#IBOutlet weak var customTextField: UITextField! // hook up outlet to this property in IB
#IBAction func didChangeValue(_ sender: UITextField) { // hook up "editing changed" action for the text field to this method in IB
delegate?.cell(self, didUpdateTextField: sender)
}
}
Now, the view controller will:
register the reuse identifier with the NIB in question;
in cellForRowAt, populate the text field and specify itself as the delegate for that cell; and
handle the didUpdateTextField method to update model if user changes anything.
Thus, something like:
class ViewController: UITableViewController {
var values = ["One", "Two", "Three"] // some initial values
private let cellIdentifier = "CustomCell"
override func viewDidLoad() {
super.viewDidLoad()
// if you’re using NIBs, you register them.
// obviously if using prototype cells in your storyboard, this isn’t necessary.
tableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: cellIdentifier) // or use cell prototype with storyboard identifer specified
}
}
// MARK: - UITableViewDataSource
extension ViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return values.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! CustomCell
// populate cell and specify delegate
cell.delegate = self
cell.customTextField.text = values[indexPath.row]
return cell
}
}
// MARK: - CustomCellDelegate
extension ViewController: CustomCellDelegate {
func cell(_ cell: CustomCell, didUpdateTextField textField: UITextField) {
// when the cell tells us that its text field's value changed, update our own model
if let indexPath = tableView.indexPath(for: cell), let string = textField.text {
values[indexPath.row] = string
}
}
}
Many people might be inclined to simplify this further, by hooking the IBAction for the text field directly to a view controller method. That works, and eliminates the need for this protocol, but the problem is that you need to figure out with which row this particular UIKit control is associated. The common trick is to navigate up the view hierarchy to identify the appropriate cell (e.g. often the text field will be in a content view within the cell, so you grab textField.superview.superview as! UITableViewCell), but that feels a little fragile to me.
But regardless of this little detail, hopefully this illustrates the broader pattern. Rather than trying to have cells keep track of user input, you should have the cell (the “view”) update the controller of any data changes immediately, and the view controller then updates the model immediately, and you no longer need to worry about the cell reuse optimizations that iOS employs.
For Swift 2 renditions, see previous revision of this answer.
The very idea of a static table is that it is fully defined in IB. One solution is to copy-paste those cells from their isolated NIBs to the one containing the table.
A better solution is to make the table dynamic, and have the dynamic code return a static number of sections and rows. Write the rest of the logic as if it's dynamic, (e.g. register all of the nibs, and initialize an array of cell identifiers how you want them organized in the table, use that array to dequeue).
Another way to solve it:
1) UITextField Delegate in View Controller. Store values in array
var textFieldValues = ["", "", "", "", "", "", "", "", "", ""] // with count of text fields in table. Init in top of ViewController.
//MARK:TextField Delegate
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if string == " "
{
return false
}
let indexPathForCellWhereTextFieldIs = self.tableView.indexPathForCell(textField.superview?.superview as! UITableViewCell)
textFieldValues[(indexPathForCellWhereTextFieldIs?.section)!] = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string) as NSString as String
return true
}
2) At indexPathForRow
cell.registrationTextField.text = textFieldValues[indexPath.section]
cell.registrationTextField.delegate = self
3) Get data from array
func tapOnRegisterButton(sender:UIButton)
{
debugPrint(textFieldValues)
}

Resources