RXSwift collectionView doesn't update when calling .onNext(newArray) - swift

I got a problem. I got a collectionview which is binded to a winPinataActions PublishSubject<[Object]>(). Initially, when loading collectionview everything is fine, it displays as it has to the objects, however when the pull to refresh action changes the publishSubject data the UI is not updated, it still gets the old content of the PublishSubject.
Here is how I bind the collectionView :
class WinPinatasViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
configureCollectionView()
}
func configureCollectionView() {
/..../
viewModel.winPinataActions
.observeOn(MainScheduler.instance)
.bind(to: collectionView.rx.items(cellIdentifier: "winPinatasCell", cellType: WinPinatasCell.self)) {(row, item, cell) in
cell.configureCell(with: item)
}.disposed(by: bag)
viewModel.getPinataActions()
}
#objc func handleRefreshControl() {
viewModel.getPinataActions()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.collectionView.refreshControl?.endRefreshing()
}
}
}
This is my viewModel class:
class WinPinatasViewModel {
let winPinataActions = PublishSubject<[WinPinatasAction]>()
func getPinataActions() {
guard let ssoId = UserDefaultsStore.ssoId() else {
return
}
NetworkEngine.shared.gamificationNetwork.getUserWinPinataActions(subject: winPinataActions, ssoID: ssoId)
}
}
And my NetworkEngine getuserPinataActions method:
func getUserWinPinataActions(subject winPinatasActions: PublishSubject<[WinPinatasAction]>, ssoID: String) {
//...//
let actions = try decoder.decode([WinPinatasAction].self, from: jsonData)
winPinatasActions.onNext(actions)
winPinatasActions.onCompleted()
//...//
}
When the pull to refresh action is done, the handleRefreshControl() method is called. Also While debugging I could see that after pullToRefresh action the new data is received inside my NetworkEngine method and both .onNext()and onCompleted() are called. But when I scroll through the collectionView the data the cell items are from the old array, not the one new one. Could you help me please? What am I doing wrong?

The problem here is that you are sending a completed event to the Subject but then expecting it to be able to send other events after that. The Observable contract specifies that once an Observable (or Subject in this case) sends a completed event, it will never send any more events under any circumstances.
Instead of passing a Subject into getUserWinPinataActions you should be returning an Observable from the function.
This is closer to what you should have:
class WinPinatasViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let bag = DisposeBag()
let viewModel = WinPinatasViewModel()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.refreshControl!.rx.controlEvent(.valueChanged)
.startWith(())
.flatMapLatest { [viewModel] in
viewModel.getPinataActions()
}
.observeOn(MainScheduler.instance)
.bind(to: collectionView.rx.items(cellIdentifier: "winPinatasCell", cellType: WinPinatasCell.self)) {(row, item, cell) in
cell.configureCell(with: item)
}
.disposed(by: bag)
}
}
class WinPinatasViewModel {
func getPinataActions() -> Observable<[WinPinatasAction]> {
guard let ssoId = UserDefaultsStore.ssoId() else {
return .empty()
}
return GamificationNetwork.shared.getUserWinPinataActions(ssoID: ssoId)
}
}
class GamificationNetwork {
static let shared = GamificationNetwork()
func getUserWinPinataActions(ssoID: String) -> Observable<[WinPinatasAction]> {
Observable.create { observer in
let jsonData = Data() // get jsonData somehow
let actions = try! decoder.decode([WinPinatasAction].self, from: jsonData)
observer.onNext(actions)
observer.onCompleted()
return Disposables.create { /* cancelation code, if any */ }
}
}
}
Remember:
Subjects provide a convenient way to poke around Rx, however they are not recommended for day to day use... In production code you may find that you rarely use the IObserver interface and subject types... The IObservable interface is the dominant type that you will be exposed to for representing a sequence of data in motion, and therefore will comprise the core concern for most of your work with Rx...
-- Intro to Rx
If you find yourself reaching for a Subject to solve a problem, you are probably doing something wrong.
Also, this article might help: Integrating RxSwift Into Your Brain and Code Base

Related

RxSwift, RxCocoa - no called when writing TextField after validation

I am new to RxSwift and RxCocoa
I need to any advice for learning
After result of Checking Id Validation, expect no word in label
But it is updating label and no entering in break point at bind function
What’s problem my code…?
var disposeBag: DisposeBag = DisposeBag()
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
let input: Signal<String> = userIDTextField.rx.text.orEmpty
.asSignal(onErrorSignalWith: .empty())
let output: Driver<String> = viewModel.bind(input)
disposeBag.insert(
output.drive(userIDLabel.rx.text)
)
}
struct ViewModel {
func checkUserIDFromDB(id: String) -> Signal<Bool> {
return .just(false).asSignal()
}
func bind(_ input: Signal<String>) -> Driver<String> {
let validState = input
.map { _ in self.checkUserIDFromDB(id:)}
.withLatestFrom(input)
return validState.asDriver(onErrorDriveWith: .empty())
}
}
This line: .map { _ in self.checkUserIDFromDB(id:)} produces a Signal<(String) -> Signal<Bool>> which is likely not what you wanted.
I'm going to assume that the goal here is to pass the entered string to the network request and wait for it to emit. If it emits true then emit the string to the label, otherwise do nothing...
Further, let's simplify things by using the Observable type instead of Signals and Drivers:
final class ViewController: UIViewController {
let userIDTextField = UITextField()
let userIDLabel = UILabel()
let disposeBag = DisposeBag() // this should be a `let` not a `var`
let viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
let input = userIDTextField.rx.text.orEmpty
let output = viewModel.bind(input.asObservable())
disposeBag.insert(
output.bind(to: userIDLabel.rx.text)
)
}
}
struct ViewModel {
func checkUserIDFromDB(id: String) -> Observable<Bool> { .just(false) }
func bind(_ input: Observable<String>) -> Observable<String> {
input.flatMapLatest { id in // Note this should be `flatMapLatest` not `map`
Observable.zip( // zip up the text with its response
Observable.just(id),
self.checkUserIDFromDB(id: id) // you weren't actually making the network call. This makes it.
.catchAndReturn(false) // if the call fails, emit `false`.
)
}
.compactMap { $0.1 ? $0.0 : nil } // if the response is true, emit the text, else nothing
}
}
The biggest concern I have with this code is what happens if the user continues to type. This will fire after every character the user enters which could be a lot of network requests, the flatMapLatest will cancel ongoing requests that are no longer needed, but still... Consider putting a debounce in the stream to reduce the number of requests.
Learn more about the various versions of flatMap from this article.
Edit
In response to your comment. In my opinion, a ViewModel should not be dependent on RxCocoa, only RxSwift. However, if you feel you must use Driver, then something like this would be appropriate:
func bind(_ input: ControlProperty<String>) -> Driver<String> {
input.asDriver()
.flatMapLatest { id in
Driver.zip(
Driver.just(id),
self.checkUserIDFromDB(id: id)
.asDriver(onErrorJustReturn: false)
)
}
.compactMap { $0.1 ? $0.0 : nil }
}
Using Signal doesn't make much sense in this context.

VIPER architecture using Swift to store data in presenter

So I'm setting up a simple VIPER architecture in Swift.
The Interactor gets some data from an API, and passes the data to the presenter that then passes formatted data to the view.
The presenter will process the data, and just count the number of objects that are downloaded. To do so I have stored a var in the presenter. The question is should I store data in the presenter?
Interactor:
class Interactor {
weak var presenter: Presenter?
func getData() {
ClosureDataManager.shared.fetchBreaches(withURLString: baseUrl + breachesExtensionURL, completion: { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
print(error)
case .success(let breaches):
self.presenter?.dataDidFetch(breaches: breaches)
self.presenter?.dataNumberDidFetch(number: breaches.count)
}
})
}
}
Presenter:
class Presenter {
var wireframe: Wireframe?
var view: ViewController?
var interactor: Interactor?
var dataDownloaded = 0
func viewDidLoad() {
print ("presenter vdl")
}
func loadData() {
interactor?.getData()
}
func dataDidFetch(breaches: [BreachModel]) {
view?.dataReady()
}
func showDetail(with text: String, from view: UIViewController) {
wireframe?.pushToDetail(with: text, from: view)
}
func dataNumberDidFetch(number: Int) {
dataDownloaded += number
view?.showData(number: String(dataDownloaded) )
}
}
View (ViewController)
protocol dataViewProtocol {
func showData(number: String)
}
class ViewController: UIViewController, dataViewProtocol {
#IBOutlet weak var showDetailButton: UIButton!
#IBOutlet weak var dataLabel: UILabel!
// weak here means it won't work
var presenter: Presenter?
#IBAction func buttonPressAction(_ sender: UIButton) {
presenter?.loadData()
}
#IBAction func buttonShowDetailAction(_ sender: UIButton) {
presenter?.showDetail(with: "AAA", from: self)
}
func dataReady() {
showDetailButton.isEnabled = true
}
func showData(number: String) {
dataLabel.text = number
}
override func viewDidLoad() {
super.viewDidLoad()
Wireframe.createViewModule(view: self)
presenter?.viewDidLoad()
}
}
Router (Wireframe)
class Wireframe {
static func createViewModule (view: ViewController) {
let presenterInst = Presenter()
view.presenter = presenterInst
view.presenter?.wireframe = Wireframe()
view.presenter?.view = view
view.presenter?.interactor = Interactor()
view.presenter?.interactor?.presenter = presenterInst
}
}
So should the presenter be used to store the number of objects downloaded?
What have you tried I've implemented the var, as shown above. This is a minimum example of the problem.
What resources have you used I've looked on StackOverflow, and Googled the issue. I can't find an answer, but know I could store the data in the view but I think this is incorrect. I could store the number of data in the Interactor, but this also doesn't seem right. It all seems...to violate separation of concerns...
I won't do your homework / use a different architecture / You should use protocols / Why is there a single protocol in your implementation This isn't homework, it is for my own self - study. There may be other architectures that can be used to do this (and coding to protocols is good practice) but this is about storing a variable in the presenter. I want to know if I should store the variable in the presenter, using VIPER and using Swift. Comments about trivia around the question are seldom helpful if they are about variable names, or the like.
What is the question? I want to know if I can store the number of downloaded data items in the presenter.

RxSwift Observe changes on model and Make request

I'm trying to learn RxSwift concept and got stuck somewhere unfortunately. There is two different screen connected to my TabBarController. On my SettingsViewController, I'm getting two string values and creating a model, On TransactionListViewController, I need to observe changes on and make a new request to fill list.
On parent tab bar controller, I have a Variable and when didLoadCall I'm subscribing this model with wallet.asObservable().subscribe
On SettingViewController when user presses the login button I'm trying to change UserModel with this code:
if let tabBar = parent?.parent as? TransactionTabBarController{
Observable.just(wallet).bind(to: tabBar.wallet)
}
I realized that onNext function for wallet.asObservable().subscribe is calling.
There is also another wallet model on my TransactionListViewController,
on viewDidLoad function I'm running this code:
wallet.asObservable().subscribe(onNext: { (wallet) in
APIClient.getTransaction(address: wallet.walletAddress)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { (model) in
self.changeModels(items: model.result)
.bind(to: self.transactionTableView.rx.items(dataSource: self.dataSource))
.disposed(by: self.disposeBag)
})
.disposed(by: self.disposeBag)}, onError: nil, onCompleted: nil, onDisposed: nil)
.disposed(by: disposeBag)
I tried to set wallet on TabBar's onNext function and I got crush couple of times on TransactionListViewController.
Can anyone help me with that?
Sadly, your code sample is inscrutable. However, it seems as though you are asking how to transmit data between two view controllers that are connected through a tab bar view controller. Below is one way you could go about doing it...
In order to use this code, you only need to assign a function to TabBarController.logic which takes a TabBarController.Inputs as an input parameter and returns a TabBarController.Outputs. You could make this assignment in the AppDelegate.
The key thing to note in this code is that every ViewController subclass has a struct Inputs, a struct Outputs and a var logic in it.
The Inputs has all the UI elements that a user can input to (e.g., Buttons and TextFields,) and the Outputs has all the UI elements that the user can see (e.g., Label text, isHidden flags.)
The logic var is a closure that contains all the logic for that view controller. Note that it can be assigned to. That means that you can develop and test the logic independently of the view controller and you can provide a view controller with a different logic object if necessary depending on context.
For somewhat more complex example code that uses a Coordinator instead of embedding code in the container view controller, see this repo: https://github.com/danielt1263/RxEarthquake
class TabBarController: UITabBarController {
struct Inputs {
let login: Observable<Void>
}
struct Outputs {
let transactions: Observable<[Transaction]>
}
var logic: (Inputs) -> Outputs = { _ in fatalError("Forgot to set logic.") }
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let settings = children[0] as! SettingsViewController
let transactionList = children[1] as! TransactionListViewController
let login = PublishSubject<Void>()
let outputs = logic(Inputs(login: login.asObservable()))
let bag = self.bag
settings.logic = { inputs in
inputs.login
.bind(to: login)
.disposed(by: bag)
return SettingsViewController.Outputs()
}
transactionList.logic = { inputs in
return TransactionListViewController.Outputs(transactions: outputs.transactions)
}
}
}
class SettingsViewController: UIViewController {
struct Inputs {
let login: Observable<Void>
}
struct Outputs {
}
var logic: (Inputs) -> Outputs = { _ in fatalError("Forgot to set logic.") }
private let bag = DisposeBag()
#IBOutlet weak var login: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
_ = logic(Inputs(login: login.rx.tap.asObservable()))
}
}
class TransactionListViewController: UIViewController {
struct Inputs {
}
struct Outputs {
let transactions: Observable<[Transaction]>
}
var logic: (Inputs) -> Outputs = { _ in fatalError("Forgot to set logic.") }
private let bag = DisposeBag()
#IBOutlet weak var transactionTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
let output = logic(Inputs())
let dataSource = MyDataSource()
output.transactions
.bind(to: transactionTableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
}
}

RXSwift + Moya + Error Handling + Refresh Button

I am trying to set up a tableview that refreshes user data after a button is pressed. RXSwift is used for the entire chain of events. Moya is used for routing.
I am trying to use the standard error handling given by Moya, which is:
provider.rx.request(.userProfile("ashfurrow")).subscribe { event in
switch event {
case let .success(response):
image = UIImage(data: response.data)
case let .error(error):
print(error)
}
}
The only way I have been able to get this to work, is to use an inner subscribe method. Please see code below. Can anyone think of a way that does not require an inner subscribe? It seems a bit clumsy as is.
class ViewController: UIViewController {
#IBOutlet weak var refreshBtn: UIButton!
#IBOutlet weak var tableView: UITableView!
let provider = MoyaProvider<MyAPI>()
let disposeBag = DisposeBag()
var latestUsers = Variable<[User]>([])
override func viewDidLoad() {
super.viewDidLoad()
setupObservableBtnRefreshWithDataFetch()
bindDataToTableView()
}
func setupObservableBtnRefreshWithDataFetch() {
let refreshStream = refreshBtn.rx.tap.startWith(())
let responseStream = refreshStream.flatMapLatest { _ -> SharedSequence<DriverSharingStrategy, [User]> in
let request = self.provider.rx.request(.showUsers)
// Inner Subscribe here, to be able to use the standard Moya subscribe methods for error handling
request.subscribe { event in
switch event {
case .success(let user):
print("Success")
case .error(let error):
print("Error occurred: \(error.localizedDescription)")
}
}
return request
.filterSuccessfulStatusCodes()
.map([User].self)
.asDriver(onErrorJustReturn: [])
}
let nilOnRefreshTapStream: Observable<[User]> = refreshBtn.rx.tap.map { _ in return [] }
let tableDisplayStream = Observable.of(responseStream, nilOnRefreshTapStream)
.merge()
.startWith([])
tableDisplayStream
.subscribe { event in
switch event {
case .next(let users):
print("Users are:")
print(users)
self.latestUsers.value = users
break
case .completed:
break
case .error(let error):
print("Error occurred: \(error.localizedDescription)")
break
}
}
.disposed(by: self.disposeBag)
}
func bindDataToTableView() {
latestUsers.asObservable()
.bind(to: tableView.rx.items(cellIdentifier: "cell", cellType: UITableViewCell.self)) { (_, model: User, cell: UITableViewCell) in
cell.textLabel?.text = model.login
}
.disposed(by: disposeBag)
}
}
class User: Decodable {
var name: String?
var mobile: Int?
var userRequestedTime: String?
var login: String?
init(name: String, mobile: Int, login: String = "") {
self.name = name
self.mobile = mobile
self.login = login
}
}
I have investigated Moya and learned it is a wrapper for network operations.
It's not entirely clear to what purpose the inner subscribe serves - based on my understanding, it triggers an identical but separate network request, which should not affect the other request subscription.
It also seems like the refreshButton tap emits two elements in tableDisplayStream (from responseStream (from refreshStream) and nilOnRefreshTapStream).
Note that Variable is deprecated. Personally, I also prefer .debug().subscribe() to manually printing events in the subscription closure.
Based on this, I would write the code as follows instead. I have not tested it. Hope it helps!
class ViewController: UIViewController {
// ...
private let provider = MoyaProvider<MyAPI>()
private let disposeBag = DisposeBag()
/// Variable<T> is deprecated; use BehaviorRelay instead
private let users = BehaviorRelay<[User]>(value: [])
private func setupObservableBtnRefreshWithDataFetch() {
refreshBtn.rx.tap
.startWith(()) // trigger initial load
.flatMapLatest { _ in
self.provider.rx.request(.showUsers)
.debug("moya request")
.filterSuccessfulStatusCodes()
.map([User].self)
.asDriver(onErrorJustReturn: []) // don't let the error escape
}
.drive(users)
.disposed(by: disposeBag)
}
private func bindDataToTableView() {
users
.asDriver()
.debug("driving table view ")
.drive(tableView.rx.items /* ... */)
.disposed(by: disposeBag)
}
}

make dynamic UIbutton appear and dissapear based on number of items in table view

I'm currently trying to make a dynamic UIbutton appear and disappear based on number of items loaded into a table view, fetched from a backend url. I want to button to appear if there's 12 or more items loaded into the table view and not appear if there's less than 12. Any ideas on the best way to handle this?
import UIKit
import RxSwift
import RxCocoa
public class AllProvidersPickerViewController: InputableTableViewController, ViewModelHolder {
#IBOutlet private(set) weak var searchBar: UISearchBar!
#IBOutlet weak var dontSeeProviderButton: UIButton!
var viewModel: AllProvidersPickerViewModel! = nil
private let bag = DisposeBag()
override public func viewDidLoad() {
super.viewDidLoad()
setupRx()
}
private func setupRx() {
viewModel.shownProviders
.bind(to: tableView.rx.items(cellIdentifier: "ProviderCell")) { _, mvpd, cell in
cell.textLabel?.text = mvpd.displayName
}
.addDisposableTo(bag)
tableView
.rx
.modelSelected(MVPD.self)
.bind(to: viewModel.selectedProvider)
.addDisposableTo(bag)
searchBar
.rx.text
.orEmpty
.bind(to: viewModel.searchQuery)
.addDisposableTo(bag)
dontSeeProviderButton
.rx.tap
.bind(to: viewModel.tappedDontSeeProvider)
.addDisposableTo(bag)
}
}
private extension MVPD {
var displayName: String {
return self.names.first ?? ""
}
}
XFreire's answers are fine, or you could do:
viewModel.shownProviders
.map { $0.count < 12 }
.bind(to: dontSeeProviderButton.rx.isHidden)
.disposed(by: bag)
Make sure shownProviders can handle being subscribed to without having to re-send any network requests or whatever. You might need shareReplayLatestWhileConnected() for that.
I have been asked to explain this code... I will do so by breaking it down...
let shownProviders = viewModel.shownProviders
At this point, I know that shownProviders is an array. I don't know much about the type that the array contains because that info wasn't in the question, but I don't need to know
let shownProviders = viewModel.shownProviders
let shouldHideButton = shownProviders.map { $0.count < 12 }
In the above line, I know that $0 is an array and I know that the button should hide if there are fewer than 12 items in the array. $0.count < 12 returns a Bool. map will transform the shownProviders Observable into whatever the block returns, so I know that shouldHideButton is an Observable<Bool>.
let shownProviders = viewModel.shownProviders
let shouldHideButton = shownProviders.map { $0.count < 12 }
let disposable = shouldHideButton.bind(to: dontSeeProviderButton.rx.isHidden)
The above line of code binds the result of shouldHideButton to the isHidden property of the button. It returns a disposable.
let shownProviders = viewModel.shownProviders
let shouldHideButton = shownProviders.map { $0.count < 12 }
let disposable = shouldHideButton.bind(to: dontSeeProviderButton.rx.isHidden)
disposable.disposed(by: bag)
This last line ensures that the binding will be broken when the view controller goes out of scope.
Simplest way:
viewModel.shownProviders
.subscribe(onNext: { [weak self] items in
if items.count < 12 {
self?.viewAllProvidersButton.isHidden = true
}
else {
self?.viewAllProvidersButton.isHidden = false
}
})
.addDisposableTo(bag)
Other way could be to create a property buttonVisibilityObserver of type AnyObserver and bind it to viewModel.shownProviders. Something like this (not tested):
var buttonVisibilityObserver: AnyObserver<[ItemsType]> {
return UIBindingObserver(UIElement: viewAllProvidersButton) { button, items in
button.isHidden = items.count < 12 ? true : false
}.asObserver()
}
And then in your setupRx():
viewModel.shownProviders
.bind(to: buttonVisibilityObserver)
.addDisposableTo(bag)