RxSwift Observeable doesn't update tableview on data change - ios

I setup the tableview and want to update the UI as soon as the data changes. I simulate a data change with the dispatcher. But the problem is, that the table won't update. Can someone explain how to setup a tableview with RxSwift to update it's cell on data change?
#IBOutlet private var tableView: UITableView!
let europeanChocolates: Variable<[Chocolate]> = Variable([])
let disposeBag = DisposeBag()
//MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
title = "Chocolate!!!"
setupCellConfiguration()
europeanChocolates.value = Chocolate.ofEurope
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
}
}
//MARK: Rx Setup
private func setupCellConfiguration() {
europeanChocolates.asObservable().bindTo(tableView.rx.items(cellIdentifier: ChocolateCell.Identifier, cellType: ChocolateCell.self)) {
row, chocolate, cell in
cell.configureWithChocolate(chocolate: chocolate)
}
.addDisposableTo(disposeBag)
}

You didnt provide implementation of your Chocolate, but I assume it's structure, in that case you are not changing anything, because in
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
}
your chocolate from europeanChocolates is copied to new var choclate, but you never save it. You can easily check it, if you try this
override func viewDidLoad() {
super.viewDidLoad()
title = "Chocolate!!!"
setupCellConfiguration()
europeanChocolates.value = Chocolate.ofEurope
europeanChocolates.asObservable().subscribe(onNext: { choco in
print("choco changed \(choco)")
})
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
}
}
I think it won't print "choco changed". If Im right this should help you:
override func viewDidLoad() {
super.viewDidLoad()
title = "Chocolate!!!"
setupCellConfiguration()
europeanChocolates.value = Chocolate.ofEurope
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
var choclate = self.europeanChocolates.value[0]
choclate.countryName = "Denmark"
self.europeanChocolates.value[0] = choclate
}
}
If it doesn't help, please provide implementation of Chocolate and ChocolateCell

In swift5 the Variable has been deprecated. So preferably you should use the behaviour subjects. To pass value in it use OnNext function. If it bind with the table view then it will automatically refresh the new data.

You have to reload the tableView just after the data change. To reload tableView call this:
this.tableView.reloadData()

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

How can I send data back to the first view controller after detecting that the back button was pressed?

Now Using the Delegate Pattern to transfer the data:
I have implemented this function:
var overviewController = addOverview()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
overviewController.delegate = self
}
func setValues(){
overview.name = buildingName.text!
overview.city = city.text!
overview.contact = contactName.text!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy"
overview.date = dateFormatter.string(from: datePicker.date)
overview.email = email.text!
overview.building = buildingTypes[buildingPicker.selectedRow(inComponent: 0)]
overview.freq = cleaningFreqs[cleaningFreqPicker.selectedRow(inComponent: 0)]
overview.phone = phoneNumber.text!
overview.sft = sqFt.text!
overview.state = State.text!
overview.street = street.text!
}
// MARK: - Navigation
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent:parent)
if parent == nil {
setValues()
print("Hello")
delegate?.setValues(obj: overview)
}
}
And here i the setValues from the protocol I wrote:
func setValues(obj: overviewProps){
overview = obj
}
However after printing one of the properties of the object, the data has not been transferred.
You can use Delegate pattern or use Observer. See these examples for help.
Delegate: https://www.appcoda.com/swift-delegate/
Observer: NSNotificationCenter addObserver in Swift
Edit: using delegate based on your code
Protocol
protocol SetValueDelegate {
func didFinishSetValue(obj: Overview)
}
First view controller
class FirstViewController: UIViewController, SetValueDelegate {
var secondViewController = SecondViewContrller()
override func viewDidLoad() {
super.viewDidLoad()
secondViewController?.delegate = self
}
func didFinishSetValue(obj: Overview) {
// when it comes back to the first screen you can use ->> obj data
}
}
Second view controller
class SecondViewController: UIViewController {
var delegate: SetValueDelegate?
override func viewDidLoad() {
super.viewDidLoad()
…
}
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if parent == nil {
setValues()
…
self.delegate?.didFinishSetValue(obj: overview)
}
}
}
There are many ways to pass/exchange data between two viewControllers. I do not know how your implementation look like. But you can use delegate pattern to achieve your target. Use a delegate and trigger the methods when you want to pass data.
As I said there are other solutions, too. But it is up to you, which one you want to choose.
The proper way to pass data between the current view and the root view with your navigation design is to trigger an unwind segue.
Have a look at the follow article. It is in Swift 3, but it should give you a good start in translating that into Swift 4.2
https://link.medium.com/b4eFIx7LaV

RxSwift - trigger search programmatically in UISearchBar

I am observing UISearchBar.rx.text attributes to perform some search related Action when User types some text.
But at some time, I also would like to trigger this search Action programmatically. For instance at the creation of the view like in this example, where unfortunately the "Searching for [...]" text is not printed.
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet weak var mySearchBar: UISearchBar!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Trigger search when text changes
mySearchBar.rx.text.subscribe(onNext: { (text)
print("Searching for \(text)...")
// do some search Action
}
.disposed(by: disposeBag)
// Programmatically trigger a search
mySearchBar.text = "Some text to search"
}
}
The problem is changing mySearchBar.text does not trigger a new rx.text Event. Is there some way to do so?
For instance, I know thanks to this post that with an UITextField, this is possible using the UITextField.sendActions(for: .ValueChanged) function. Is there some similar way to do so with UISearchBar?
You could use a Variable<String?> as a sink for your search bar updates. That way you could also set its value programmatically, and use the variable instead of the search bar directly to drive your action:
class ViewController: UIViewController {
let searchText = Variable<String?>(nil)
let searchBar = UISearchBar()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
searchBar.rx.text.asDriver()
.drive(searchText)
.disposed(by: disposeBag)
searchText.asObservable().subscribe(onNext: { [weak self] (text) in
if let welf = self, welf.searchBar.text != text {
welf.searchBar.text = text
}
})
.disposed(by: disposeBag)
searchText.value = "variables so cool"
searchText.asObservable().subscribe(onNext: { [weak self] (text) in
self?.doStuff(text)
})
.disposed(by: disposeBag)
}
}
This is a simple usage of Drivers
import UIKit
import RxCocoa
import RxSwift
class SearchViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let dataModels = Driver.just(["David", "Behrad","Tony","Carl","Davidov"])
let searchingTextDriver = searchBar.rx.text.orEmpty.asDriver()
let matchedCases = searchingTextDriver.withLatestFrom(dataModels){ searchingItem,models in
return models.filter({ (item) -> Bool in
return item.contains(searchingItem)
})
}
matchedCases.do(onNext: { (items) in
print(items)
}).drive().disposed(by: disposeBag)
}
}
As you see we created a dataModels Driver that it could be a response of your network or dataBase fetching.
the searching text converted to Driver then mixed with the dataModels to create matchedCases.
Yes same works for UITextBar.
Setting .text programmatically does not trigger the action automatically, I don't know why! but this is the case for UITextField too.
Call sendActions after each time you update .text property of a UIView subclass.
Try this :
// Programmatically trigger a search
mySearchBar.text = "Some text to search"
mySearchBar.sendActions(for: .valueChanged)

How to recovery dataSource after disposed from a catchError

I have one UITableView populated by reactive viewmodel using RxSwift, pagination and refresh are working well. The viewModel.dataSource() is consuming my API and sometime I can receive a empty result parsed as error type.
I want to catch this error and create an empty state, hiding tableview and showing a emptyViewState. I thought I could make it with the catchError.
My problem is after catchError, the dataSource is disposed and I couldn't be able to recovery the empty state and repopulated the tableview, I tried to recreate the dataSource calling self.bindDataSource() but I getting fatal error.
There is a way to avoid dataSource disposed ? How can I reconnect / rebuild the dataSource to recovery from the empty state ?
class MyViewControl: UIViewController {
fileprivate let disposeBag = DisposeBag()
fileprivate let viewModel = ViewModel()
let dataSource = SearchViewModel.SearchDataSource()
#IBOutlet fileprivate weak var tableView: UITableView!
#IBOutlet weak var emptyStateView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// When I disable tableview, can see a hidden view with empty state message and one button
viewModel.isTableViewHidden
.bindTo(tableView.rx.isHidden)
.addDisposableTo(disposeBag)
self.setupTableView()
}
fun setupTableView() {
// ... setup table view
self.bindDataSource()
}
fileprivate func bindDataSource() {
// Bind dataSource from search to UITableView
viewModel.dataSource()
.debug("[DEBUG] Loading Search Tableview ")
.bindTo( tableView.rx.items(dataSource: dataSource) )
.addDisposableTo( disposeBag )
}
#IBAction fileprivate func emptyStateAction(_ sender: UIButton) {
// Do something and try to recreate the bindDataSource
self.bindDataSource()
}
}
class SearchViewModel {
private let disposeBag = DisposeBag()
typealias SearchDataSource = RxTableViewSectionedReloadDataSource<PaginationStatus<WorkerEntity>>
let isTableViewHidden = BehaviorSubject<Bool>(value: false)
// Controls to refresh and paging tableview
let refreshTrigger = BehaviorSubject<Void>(value:())
let nextPageTrigger = PublishSubject<Void>()
// Others things happing herer
func dataSource() -> Observable<[PaginationStatus<WorkerEntity>]> {
return self.refreshTrigger.debug("[DEBUG] Refreshing dataSource")
.flatMapLatest { [unowned self] _ -> Observable<[PaginationStatus<WorkerEntity>]> in
// Access the API and return dataSource
}
.catchError { [unowned self] error -> Observable<[PaginationStatus<WorkerEntity>]> in
// Hidden the tableview
self.isTableViewHidden.onNext(true)
// Do others things
return Observable.of([PaginationStatus.sectionEmpty])
}
}
}
when you bindDataSource() you dont reinitialised your datasource, so you bind it to a error event.
You need to init it, to bind it again. And you might want to remove your binding too
let disposeBagTableView = DisposeBag()
//remove
let dataSource = SearchViewModel.SearchDataSource()
fileprivate func bindDataSource() {
// Bind dataSource from search to UITableView
disposeBagTableView = DisposeBag()
SearchViewModel.SearchDataSource()
.debug("[DEBUG] Loading Search Tableview ")
.bindTo( tableView.rx.items(dataSource: dataSource) )
.addDisposableTo( disposeBagTableView )
}

How to read data set in first VC in other VC?

I am a looking for a way to read the data set in the first view controller in another view controller
In my fist VC i have
class ScannerViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, AVCaptureMetadataOutputObjectsDelegate {
var scannedVisitors = [Visitor]()
.....
....
override func viewDidLoad() {
super.viewDidLoad()
// test data, will be provided server side
var visitor = Visitor(visitorName: "John", visitorCompany: "google", visitorPlace: "San", visitorPhone: "94888484", visitorEmail: "john#google.com")
scannedVisitors += [visitor]
visitor = Visitor(visitorName: "Matt", visitorCompany: "apple", visitorPlace: "Tokyo", visitorPhone: "94888484", visitorEmail: "matt#apple.com")
scannedVisitors += [visitor]
Now First view in Storyboard shows the scannedVisitors
But in an other viewController/view i like to list all scannedVisitors in a table view again, for this i have
class ListVisitorsVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var scannedVisitorsArr = [Visitor]()
override func viewDidLoad() {
super.viewDidLoad()
// first try this, later use NSUserDefaults or Core Data
let firstVC = ScannerViewController()
scannedVisitorsArr = firstVC.scannedVisitors
print("Visitors \(scannedVisitorsArr)")
}
But the scannnedVisitorsArr is empty???
1/ How can i fix the empty scannedVisitorsArr? Should i have a seperate populate scannedVisitors function in my first view controller??
2/ Is there a size restriction when using NSUserDefaults??
[EDIT]
Testing with adding a function to first VC
func readAndPopulateData(){
var visitor = Visitor(visitorName: "John", visitorCompany: "google", visitorPlace: "San", visitorPhone: "94888484", visitorEmail: "john#google.com")
scannedVisitors += [visitor]
visitor = Visitor(visitorName: "Matt", visitorCompany: "apple", visitorPlace: "Tokyo", visitorPhone: "94888484", visitorEmail: "matt#apple.com")
scannedVisitors += [visitor]
}
and
override func viewDidLoad() {
super.viewDidLoad()
self.readAndPopulateData()
Now in my other VC
let firstVC = ScannerViewController()
firstVC.readAndPopulateData()
scannedVisitorsArr = firstVC.scannedVisitors
But this just returns
Visitors [folder_name.Visitor, folder_name.Visitor]
and not the data set?
[EDIT]
Reading up on data container singleton as it looks like it's usable for this. But i read different pos/neg stories about using singletons? Any tips on the subject data container singleton are welcome.
You can use prepareForSegue Method
example
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if (segue.identifier == "ToListVisitorsVC") {
//Checking identifier is crucial as there might be multiple
// segues attached to same view
var detailVC = segue!.destinationViewController as ListVisitorsVC;
detailVC.visitors = self.scannedVisitors
}
}

Resources