Rxswift Update Tableview cell value - ios

I am able to bind an Observable sequence of data to a table.
Now lets say i have a button on each cell which on click changes the
label of that cell to new value. How to do this ?
I have done so far as follows
I have created an #IBAction for that button in the cell pointing class
then i am doing
label.text = "new text"
but when i scroll down then scroll up, the label show previous value not the new value
previously when i use array and set each value to a cell i used to update that array item and called tableview.reloadData.
how can i do this in RxSwift??
I have done so far
tableview.dataSource = nil (then)
myData.bindTo ... (bind again)
This does not seem to me the right way. so what is the appropriate way to deal with this??

I am able to achive this with RxSwiftCommunity Action
https://github.com/RxSwiftCommunity/Action
ViewController
variable.asObservable().bindTo(mytable.rx.items(cellIdentifier: "cell")) { (row, person, cell) in
if let cellToUse = cell as? TableViewCell {
cellToUse.person = Variable(person)
cellToUse.button1.rx.action = cellToUse.action
}
}.addDisposableTo(disposeBag)
and in cell
class TableViewCell: UITableViewCell {
#IBOutlet weak var label1: UILabel!
#IBOutlet weak var label2: UILabel!
#IBOutlet weak var button1: UIButton!
let disposeBag = DisposeBag()
let action = CocoaAction { input in
return .just(input)
}
var person : Variable<Person>!{
didSet {
self.updateUI()
}
}
private func updateUI(){
person.asObservable()
.subscribe(onNext: {
person in
let name : Observable<String> = Observable.of(person.name)
let age : Observable<String> = Observable.of("\(person.age)")
_ = name.bindTo(self.label1.rx.text)
_ = age.bindTo(self.label2.rx.text)
}).addDisposableTo(disposeBag)
action.elements
.asObservable()
.subscribe(onNext: {
_ in
self.label2.text = "asd"
}).addDisposableTo(disposeBag)
}
}
like before not sure if this is the right way but it worked for me
thanks to (https://rxswift.slack.com/messages/#fpillet) for showing me the way

Related

How to select the current item to delete it from an array in Swift using Realm

So, I have tried looking ALL over the internet and StackOverflow to find an answer, but I'm not sure what to even look for, so the answer may already have been shared. So I'm sorry in advance, as I am a noob. However, I still need help. (please!) I've got an app I'm working on with a tableview full of parts, with a details part page that gives details of the part (Part name, part number, description, etc.)
I have a delete button at the end of the page, and when you click it, it asks you if you want to delete, are you sure? If the user says yes, then the part deletes, but the delete
only deleted the LAST item from the tableview, the most recently added. Which I know, is because I've called the following function:
func deletePart() {
if let partToDelete = getPartsArray().last {
try! realm.write {
realm.delete(partToDelete)
}
}
with 'getPartsArray().last'
I'm trying to see how I can get the CURRENT selected part in the tableview to be deleted. Right now, I could have the second part from the top selected, and if I click THAT part's delete button, it will always delete the last part from the tableview.
Here's the code for the getPartsArray function:
func getPartsArray() -> [PartInfo] {
return getAllParts().map { $0 }
}
I (noobishly) have already tried: with 'getPartsArray().current' and apparently that's not a thing lol.
I was also thinking, since I'm using REALM / Mongo DB, I could find the part by it's ID? and then delete it? but I'm not sure how to find the current select part's id either.
Any help would be much appreciated. Thank you!
EDIT: here is my TableView Code:
//
// ViewAllPartsViewController.swift
// PartKart
//
// Created by Kiarra Julien on 10/20/21.
//
import Foundation
import UIKit
class ViewAllPartsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, CurrencyFormatter {
private var brain = PartKartBrain()
private var parts = [PartInfo]()
#IBOutlet var tableView: UITableView!
#IBAction func returnHome() {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let nib = UINib(nibName: "DemoTableViewCell", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: "DemoTableViewCell")
tableView.delegate = self
tableView.dataSource = self
parts = brain.getPartsArray()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
parts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DemoTableViewCell", for: indexPath) as! DemoTableViewCell
cell.partNameLabel.text = parts[indexPath.row].partName
// Convert string value to double
cell.partCostLabel.text = formatCurrency(value: parts[indexPath.row].partCost)
// String(format: "$%.2f", parts[indexPath.row].partCost)
cell.purchaseDateLabel.text = parts[indexPath.row].purchaseDate
// cell.textLabel?.text = parts[indexPath.row].partName
// cell.textLabel?.numberOfLines = 0countTotalParts()
// cell.textLabel?.font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
performSegue(withIdentifier: "showPartDetails", sender: parts[indexPath.row])
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
if let viewcontroller = segue.destination as? PartDetailsViewController {
viewcontroller.part = sender as? PartInfo
}
}
}
and here's where I call delete part:
class PartDetailsViewController: UIViewController, CurrencyFormatter {
//Store Information Labels
#IBOutlet weak var storeNameLabel: UILabel!
#IBOutlet weak var storeNumLabel: UILabel!
#IBOutlet weak var storeAddrLabel: UILabel!
//Part Information Labels
#IBOutlet weak var partNameLabel: UILabel!
#IBOutlet weak var partNumLabel: UILabel!
#IBOutlet weak var partDescLabel: UILabel!
#IBOutlet weak var partCostLabel: UILabel!
#IBOutlet weak var partQtyLabel: UILabel!
#IBOutlet weak var purchaseDateLabel: UILabel!
#IBOutlet weak var hasWarrantySwitch: UISwitch!
#IBOutlet weak var warrantyLengthLabel: UILabel!
//Mechanic Information Labels
#IBOutlet weak var mechanicNameLabel: UILabel!
#IBOutlet weak var mechanicNumLabel: UILabel!
#IBOutlet weak var mechanicAddrLabel: UILabel!
#IBOutlet weak var laborCostLabel: UILabel!
#IBOutlet weak var serviceDateLabel: UILabel!
var part: PartInfo?
let brain = PartKartBrain()
#IBAction func deletePartBtn(_ sender: UIButton) {
// Declare Alert message
let dialogMessage = UIAlertController(title: "Confirm", message: "Are you sure you want to delete this part?", preferredStyle: .alert)
// Create OK button with action handler
let ok = UIAlertAction(title: "OK", style: .default, handler: { (action) -> Void in
print("Ok button tapped")
// I CALL DELETE PART RIGHT HEREEE!
self.brain.deletePart()
// delay and then dismiss the page
let delayInSeconds = 0.5
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { [unowned self] in
dismiss(animated: true, completion: nil)
}
})
// Create Cancel button with action handlder
let cancel = UIAlertAction(title: "Cancel", style: .cancel) { (action) -> Void in
print("Cancel button tapped")
}
//Add OK and Cancel button to dialog message
dialogMessage.addAction(ok)
dialogMessage.addAction(cancel)
// Present dialog message to user
self.present(dialogMessage, animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
title = part?.partName
//Set the Store Info Labels Equal to actual data
storeNameLabel.text = part?.partName
storeNumLabel.text = part?.storeNumber
storeAddrLabel.text = part?.storeAddress // < ---- The address is cut off the screen!
//Set the Part Info Labels Equal to actual data
partNameLabel.text = part?.partName
partNumLabel.text = part?.partNumber
partDescLabel.text = part?.partDescription
if let partCost = part?.partCost {
partCostLabel.text = formatCurrency(value: partCost)
}
if let partQty = part?.partQuantity {
partQtyLabel.text = String(partQty)
}
purchaseDateLabel.text = part?.purchaseDate
//If there's no warranty, display 'N/A' instead
if part?.hasWarranty == true {
hasWarrantySwitch.isOn = true
warrantyLengthLabel.text = part?.warrantyLength
} else {
hasWarrantySwitch.isOn = false
warrantyLengthLabel.text = "N/A"
}
//Set the Mechanic Info Labels Equal to actual data
mechanicNameLabel.text = part?.mechanicName
mechanicNumLabel.text = part?.mechanicNumber
mechanicAddrLabel.text = part?.mechanicAddress
//laborCostLabel.text = part?.laborCost
if let laborCost = part?.laborCost {
laborCostLabel.text = formatCurrency(value: laborCost)
}
serviceDateLabel.text = part?.serviceDate
}
}
Let me state the question back to you
"How to I delete a selected row from my tableview"?
If that's the question let be provide a boiled down answer
First, don't do this when working with Realm
return getAllParts().map { $0 }
Realm objects are lazily loaded into a Results object. As soon as you run them against a high level function like map, reduce etc (storing them in an Array), they ALL get loaded into memory and if you have a large dataset, that will overwhelm the device. Additionally if you have sorting etc, your objects will get out of sync with the underlying Realm data.
Your best bet is to leverage Realm Results as your tableView dataSource - it behaves much like an array.
So here's a viewController class that has a parts Results object as a tableView datasource
class InStockVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
var partsNotificationToken: NotificationToken!
var partsResults: Results<PartsClass>! //the tableView dataSource
Assume the user taps or selects row #2 and then taps or clicks 'Delete'
//pseudocode
let selectedRow = tableView.selectedRow
let part = partResults[selectedRow]
try! realm.write {
realm.delete(part)
}
The above code determines which row in the table was selected, gets the part object from Results and then tells Realm to delete it.
As soon as that object is deleted from Realm, the partsResults object reflects that change and the object is automagically removed! The only thing you need to do is to reflect that change in the UI.
There are many ways of handling that with animations and so forth but to keep it simple, lets just reload the tableView so the deleted row is no longer displayed.
Note in the above code, there's also a var partsNotificationToken: NotificationToken!, that token is an observer of the results object - when something in the underlying data changes, the results object changes as well and the notification token fires an observer action to handle that change. Here's an example observer
self.partsNotificationToken = self.partsResults.observe { changes in
switch changes {
case .initial:
self.tableView.reloadData() //update the tableview after data is initially loaded
case .update(_, _, _, _):
self.tableView.reloadData() //update the tableView after changes

Swift - How to calculate a sum of buttons in table view cell

I have two types of table cells, one that has 6 buttons, and each button has a number value, and then a second cell that has a button to calculate the total sum of the selected numbers, and a label to display this total sum in.
My issue is I cannot get the label to calculate the total. Here is what I have so far
Number Cell:
protocol ToggleNumberCellDelegate: AnyObject {
/// This method detects the selected value of the cell.
func toggleNumberCell(_ toggleNumberCell: ToggleNumberCell, selectedValue: Int)
}
class ToggleNumberCell: UITableViewCell {
static let reuseIdentifier = String(describing: ToggleNumberCell.self)
#IBOutlet private weak var titleLabel: UILabel!
#IBOutlet private weak var zeroButton: UIButton!
#IBOutlet private weak var oneButton: UIButton!
#IBOutlet private weak var twoButton: UIButton!
#IBOutlet private weak var threeButton: UIButton!
#IBOutlet private weak var fourButton: UIButton!
#IBOutlet private weak var fiveButton: UIButton!
#IBOutlet private weak var sixButton: UIButton!
private weak var delegate: ToggleNumberCellDelegate?
private var value: Int?
//...
#IBAction func buttonTapped(_ sender: UIButton) {
switch sender {
case zeroButton:
self.zeroButton.backgroundColor = UIColor.NBABlue
self.zeroButton.tintColor = UIColor.white
self.value = 0
self.delegate?.toggleNumberCell(self, selectedValue: self.value!)
print("The value you tapped is \(value)")
case oneButton:
self.oneButton.backgroundColor = UIColor.NBABlue
self.oneButton.tintColor = UIColor.white
self.value = 1
self.delegate?.toggleNumberCell(self, selectedValue: self.value!)
print("The value you tapped is \(value)")
case twoButton:
self.twoButton.backgroundColor = UIColor.NBABlue
self.twoButton.tintColor = UIColor.white
self.value = 2
self.delegate?.toggleNumberCell(self, selectedValue: self.value!)
print("The value you tapped is \(value)")
The above code just sets up the buttons, and gives each a value by using its delegate.
Label Cell
class CalculateCell: UITableViewCell, ToggleNumberCellDelegate {
var increment = 0
static let reuseIdentifier = String(describing: CalculateCell.self)
#IBOutlet private weak var calculateButton: UIButton!
#IBOutlet private weak var totalLabel: UILabel!
func configure(answer: AnswerModel) {
self.backgroundColor = UIColor.secondarySystemGroupedBackground
self.totalLabel.text = answer.text
self.totalLabel.font = .preferredFont(forTextStyle: .headline)
calculateButton.layer.cornerRadius = 10.0
}
override func prepareForReuse() {
super.prepareForReuse()
self.totalLabel.text = nil
}
#IBAction func calculateTapped(_ sender: UIButton) {
// You need to get the selected value in here somehow.
}
func toggleNumberCell(_ toggleNumberCell: ToggleNumberCell, selectedValue: Int) {
increment = selectedValue
}
}
Here I called the delegate to get the selected value, but I think I did this wrong, can someone tell me how I can calculate to total value?
Here is a screenshot of what I want to achieve.
You have a basic problem with your approach. You should not store data in table view cells. They are views, and should display data, not store data.
You should set up a model object to store the state of your UI. (it could be as simple as an array of integers, one of the value of each ToggleNumberCell in your table view.)
You should have a controller object serve as the delegate for the ToggleNumberCell. When its toggleNumberCell method gets called, it would update the count for that cell's entry in your data model, and then tell the total cell to update itself. Your table view data source would query the model and use the value for each entry in the model to calculate the totals, then install that value in the total cell.
I would recommend you make the UIViewController (than manages the UITableView) the ToggleNumberCellDelegate. Then that view controller can keep track of the total of all of the buttons. The view controller then can provide the total to
the CalculateCell. This separates the logic between the cells. Ideally the controller manages the logic, while the cells are simply views displaying the data.
Also, I would change the toggleNumberCell function to the following:
func toggleNumberCell(_ toggleNumberCell: ToggleNumberCell, selectedValue: Int) {
// Using += will result in adding to the existing value instead of overwriting it
increment += selectedValue
}

RxDataSources cell reload animations not working properly

I am having some issues with the RxDataSources cell reload animations for RxSwift. I have a simple table setup like so:
import UIKit
import RxDataSources
import RxCocoa
import RxSwift
import Fakery
class ViewController1: UIViewController {
#IBOutlet weak var tableView: UITableView!
let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}
private func setupTableView() {
tableView.register(UINib(nibName: "TestTableViewCell", bundle: nil), forCellReuseIdentifier: "cell")
let dataSource = RxTableViewSectionedAnimatedDataSource<SectionOfTestData>(
animationConfiguration: AnimationConfiguration(insertAnimation: .none, reloadAnimation: .none, deleteAnimation: .none),
configureCell: { dataSource, tableView, indexPath, element in
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TestTableViewCell
cell.testData = element
return cell
})
someData
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
}
let someData = BehaviorRelay<[SectionOfTestData]>(value: [SectionOfTestData(items: [
TestData(color: .red, name: "Henry"),
TestData(color: .blue, name: "Josh")
])])
#IBAction func didTapUpdateButton(_ sender: Any) {
let colors: [UIColor] = [.blue, .purple, .orange, .red, .brown]
let items = someData.value.first!.items
// Add random data when button is tapped
someData.accept([SectionOfTestData(items: items + [TestData(color: colors.randomElement()!, name: Faker().name.firstName())])])
}
}
The models:
struct TestData {
let color: UIColor
let name: String
}
extension TestData: IdentifiableType, Equatable {
typealias Identity = Int
var identity: Identity {
return Int.random(in: 0..<20000)
}
}
struct SectionOfTestData {
var items: [Item]
var identity: Int {
return 0
}
}
extension SectionOfTestData: AnimatableSectionModelType {
typealias Identity = Int
typealias Item = TestData
// Implement default init
init(original: SectionOfTestData, items: [Item]) {
self = original
self.items = items
}
}
class TestTableViewCell: UITableViewCell {
#IBOutlet weak var colorView: UIView!
#IBOutlet weak var nameLabel: UILabel!
var testData: TestData! {
didSet {
colorView.backgroundColor = testData.color
nameLabel.text = testData.name
}
}
}
When the button is tapped the BehaviorRelay is updated and the table seems to refresh however the "animations" are always the same. In the supplied code I have actually set all animation types to .none yet it is still performing an animation. If I try to change the animation type to another type such as .bottom again the animation is the same. What am I doing wrong here?
Is this a reload animation or insert animation? I have no idea if the table reloads or inserts when the data is updated, I can't find any information in the documents. Any pointers on this would be greatly appreciated!
Your problem is:
var identity: Identity {
return Int.random(in: 0..<20000)
}
RxDataSources uses the identity value in order to compute the changeset.
You have implemented it in a way that essentially returns a new value each time (unless you get a collision) so from the framework point of view, you are always removing all the items and adding new items. You can check this by implementing
decideViewTransition: { _, _, changeset in
print(changeset)
return .animated
}
To complete the accepted answer :
make a uuid and pass it to identity:
let id = UUID().uuidString
var identity: String {
return id
}
then the animations work perfectly.

How to use RxSwift to implement whether a view is hidden by a button's click state (isSelected)

I want to have the view hidden when the button is selected and the view shown when button is deselected, how do I do it using RxSwift?
When I created a custom control, a checkbox out of UIButton, I actually struggled observing that isSelected property. But here's an easy way:
Subclass the UIButton (this is optional, but in this way, you get shorter lines in your controller).
In your custom button, subscribe to its own .rx.tap.
Have a BehaviorRelay called isSelectedBinder in your button.
Finally, you can now bind that isSelectedBinder of your instantiated button to the .rx.isHidden of your whatever view.
Controller
class ViewController: UIViewController {
#IBOutlet weak var button: MyButton!
#IBOutlet weak var someView: UIView!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.button.isSelectedBinder
.bind(to: self.someView.rx.isHidden)
.disposed(by: disposeBag)
}
}
Button
class MyButton: UIButton {
var isSelectedBinder = BehaviorRelay<Bool>(value: true)
let disposeBag = DisposeBag()
override func awakeFromNib() {
super.awakeFromNib()
weak var weakSelf = self
self.rx.tap.subscribe { _ in
print("TAP")
guard let strongSelf = weakSelf else { return }
strongSelf.isSelectedBinder.accept(!strongSelf.isSelectedBinder.value)
}.disposed(by: self.disposeBag)
}
}

#IB Action function inside of viewdidload in Swift

Is it possible to have an #IB Action function inside of viewDidLoad() ?
The action is a simple one - a Stepper that increases other label.text values accordingly. However, the values that the stepper needs to work with depend on the return content of a url - which are only known after the viewDidLoad() of course.
So I think I can't have the IBaction way up on top before the viewDidLoad(), and the error I get if I try to do my IB action inside of the viewDidLoad() is:
"Only instance methods can be declared ‘IBAction' ”
EDIT
Let me clarify myself, sorry for the confusion. I know I need an outlet to get the UIStepper values from. I have that:
#IBOutlet weak var stepper: UIStepper!
I then have an action also connected to same UIStepper that will increase/decrease value of a label's text (new_total) accordingly:
#IBOutlet weak var new_total: UILabel!
#IBAction func step_up_pass(sender: AnyObject) {
new_total.text = "\(Int(stepper.value))"
}
However, I want to start out with a value (todays_price) I'm getting back from a json request and use that as a starting point, to multiply it using the stepper and put the multiplied value into the label's text.
I have a struct in a separate file that defines my object so:
struct PassengerFromOtherBus {
var fname: String?
var lname: String?
var todays_price: Int?
init(json: NSDictionary) {
self.fname = json["fname"] as? String
self.lname = json["lname"] as? String
self.todays_price = json["todays_price"] as? Int
}
}
So later on in the view controller, inside of the viewDidLoad(), after connecting to the URL and then parsing it using NSJSONSerialization and a bunch of other code here (that I don't need to confuse you with) I finally have my value todays_price. So my question is, how do I get my action to use that value when it's only known inside of my viewDidLoad()? Xcode will not even let me connect the IBAction to anywhere inside the viewDidLoad function!
This is not done with an Action but with an Outlet. Connect the Stepper from IB as an Outlet to your ViewController. Then just set the values of the Stepper in ViewDidLoad.
I would never go directly from a UIStepper.value to UILabel.text.
Use an intermediary variable to store the value.
Do the same for the return from the JSON. By setting a didSet function on those variables you can update the UI when any of the values is updated.
class FirstViewController: UIViewController {
var todays_price: Int = 0 {
didSet { // didSet to trigger UI update
myLabel.text = "\(stepperValue * todays_price)"
}
}
var stepperValue : Int = 1 {
didSet { // didSet to trigger UI update
myLabel.text = "\(stepperValue * todays_price)"
}
}
#IBOutlet weak var myStepper: UIStepper!
#IBOutlet weak var myLabel: UILabel!
override func viewDidLoad() {
//
let returnValueFromJson = 10
todays_price = returnValueFromJson
}
#IBAction func stepperUpdate(sender: AnyObject) {
stepperValue = Int(myStepper.value)
}
}
Just add a variable to the top of your view controller to hold the value from your json request. Then in viewDidLoad you update that variable, and then you can use it to set your label and inside the IBAction (that doesn't have to be inside viewDidLoad).
So you would do something like this:
class WhateverViewController: UIViewController {
var todays_price: Int!
override func viewDidLoad() {
super.viewDidLoad()
todays_price = // The value you got from json goes here
new_total.text = "\(todays_price)"
}
#IBAction func step_up_pass(sender: AnyObject) {
new_total.text = "\(Int(stepper.value) * todays_price)"
}
}

Resources