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)
}
}
Related
I'm new in the RxSwift development and I've an issue while presentation a view controller.
My MainViewController is just a table view and I would like to present detail when I tap on a item of the list.
My DetailViewController is modally presented and needs a ViewModel as input parameter.
I would like to avoid to dismiss the DetailViewController, I think that the responsability of dismiss belongs to the one who presented the view controller, i.e the dismiss should happen in the MainViewController.
Here is my current code
DetailsViewController
class DetailsViewController: UIViewController {
#IBOutlet weak private var doneButton: Button!
#IBOutlet weak private var label: Label!
let viewModel: DetailsViewModel
private let bag = DisposeBag()
var onComplete: Driver<Void> {
doneButton.rx.tap.take(1).asDriver(onErrorJustReturn: ())
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
bind()
}
private func bind() {
let ouput = viewModel.bind()
ouput.id.drive(idLabel.rx.text)
.disposed(by: bag)
}
}
DetailsViewModel
class DetailsViewModel {
struct Output {
let id: Driver<String>
}
let item: Observable<Item>
init(with vehicle: Observable<Item>) {
self.item = item
}
func bind() -> Output {
let id = item
.map { $0.id }
.asDriver(onErrorJustReturn: "Unknown")
return Output(id: id)
}
}
MainViewController
class MainViewController: UIViewController {
#IBOutlet weak private var tableView: TableView!
private var bag = DisposeBag()
private let viewModel: MainViewModel
private var detailsViewController: DetailsViewController?
override func viewDidLoad(_ animated: Bool) {
super.viewDidLoad(animated)
bind()
}
private func bind() {
let input = MainViewModel.Input(
selectedItem: tableView.rx.modelSelected(Item.self).asObservable()
)
let output = viewModel.bind(input: input)
showItem(output.selectedItem)
}
private func showItem(_ item: Observable<Item>) {
let viewModel = DetailsViewModel(with: vehicle)
detailsViewController = DetailsController(with: viewModel)
item.flatMapFirst { [weak self] item -> Observable<Void> in
guard let self = self,
let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
self.present(detailsViewController, animated: true)
return detailsViewController.onComplete.asObservable()
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController? = nil
})
.disposed(by: bag)
}
}
MainViewModel
class MainViewModel {
struct Input {
let selectedItem: Observable<Item>
}
struct Output {
let selectedItem: Observable<Item>
}
func bind(input: Input) -> Output {
let selectedItem = input.selectedItem
.throttle(.milliseconds(500),
latest: false,
scheduler: MainScheduler.instance)
.asObservable()
return Output(selectedItem: selectedItem)
}
}
My issue is on showItem of MainViewController.
I still to think that having the DetailsViewController input as an Observable isn't working but from what I understand from Rx, we should use Observable as much as possible.
Having Item instead of Observable<Item> as input could let me use this kind of code:
item.flatMapFirst { item -> Observable<Void> in
guard let self = self else {
return Observable<Void>.never()
}
let viewModel = DetailsViewModel(with: item)
self.detailsViewController = DetailsViewController(with: viewModel)
guard let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
present(detailsViewController, animated: true)
return detailsViewController
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController = nil
})
.disposed(by: bag)
What is the right way to do this?
Thanks
You should not "use Observable as much as possible." If an object is only going to ever have to deal with a single item, then just pass the item. For example if a label is only ever going to display "Hello World" then just assign the string to the label's text property. Don't bother wrapping it in a just and binding it to the label's rx.text.
Your second option is much closer to what you should have. It's a fine idea.
You might find my CLE library interesting. It takes care of the issue you are trying to handle here.
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
I am starting on combine with swift, but having some hard time (had experience working in swiftui before).
So the question is how to perform certain operation:
lets say i have vc1. and I go to vc2 from there
2.then i start asynchronos network closure and come back to vc1 (by popping out vc2).
Now say i want to ge just a string from vc2's asycnrhoss clousre to vc1 when i am back to vc1.
how can i achieve this?
I want to use publisher of lets say <String, Never>
how can I subscribe in my vc1 and publish or send it from vc2 ?
I am using this approach but its not working, it never comes to code under sink.....
public class Parent {
public static let shared = Parent()
public var publisher = PassthroughSibject<String,Never>()
}
class vc1: ViewController {
func viewdidLoad() {
let subscription = Parent.shared.oublisehr.sink { (result) in
print(result)
}
}
func navigatetoVC1() {
///// some code to navigate to vc1
}
func button() {
self.navigatetoVC1
}
}
class vc2: ViewController {
func viewDidload() {
///
}
func performsomeOperation() {
someasyncoperation(completion: { result in
switch result {
case .success:
//send some data to vc1
Parent.shared.publisher.send("testdata")
case .failure:
//send some data to vc1
})
self.dismisVC2() //some method to pop out vc2
}
}
Your code is almost right, except you are using your Anycancellable inside viewdidload, so its scope is getting exhausted. So use it outside in the view controller as an optional AnyCancellable type.
Below code should work.
class vc1: ViewController {
var subscription = AnyCancellable?
func viewdidLoad() {
self.subscription = Parent.shared.oublisehr.sink { (result) in
print(result)
}
}
func navigatetoVC1() {
///// some code to navigate to vc1
}
func button() {
self.navigatetoVC1
}
}
I'm very new to RxSwift and RxCocoa and I've recently made heavy use of Variable because of how convenient it is to just push mutations into the Variable through its value. Now that it is deprecated I'm trying to understand how best to use BehaviorRelay instead. There's an Rx-y way of doing what I want to do, but I'm having a hard time landing on it.
What I want is to put an instance of struct-based model behind a ViewModel and observe changes to it and bind UI elements in such a way that I can mutate that model through the BehaviorRelay.
The model is simple:
struct Pizza {
var name: String
var price: Float
}
So is the View Model:
final class PizzaViewModel {
let pizzaRelay = BehaviorRelay<Pizza>(value: Pizza(name: "Sausage", price: 5.00))
init(pizza: Pizza) {
pizzaRelay.accept(pizza)
// I feel like I'm missing something Rx-like here...
}
}
Then somewhere you would maybe bind a UITextField to the BehaviorRelay like so:
viewModel
.pizzaRelay
.asObservable()
.map { $0.name }
.bind(to: nameTextField.rx.text)
.disposed(by: disposeBag)
The question becomes: if you need to push values from the text field back into the BehaviorRelay how should that work?
nameTextField
.rx
.controlEvent([.editingChanged])
.asObservable()
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
if let text = self.nameTextField.text {
self.viewModel.pizzaRelay.value.name = text // does not compile because value is a let
}
}).disposed(by: disposeBag)
I'm probably not using the correct types here or I'm not thinking in the correct Rx-fashion in-terms of streams of inputs/outputs, but I'm curious how others might approach this problem?
Other things I've considered:
Just reconstructing a new Pizza in the .subscribe using current value in the BehaviorRelay, mutating the name and then .accept-ing that back into the relay. That doesn't feel exactly right, though.
Creating individual BehaviorRelay's for each property I want to mutate on my Pizza, then .accept-ing values for each property and then using combineLatest on all those relays and returning a Observable<Pizza>. But that feels clunky also.
How should this work in an ideal world? Am I thinking about this incorrectly? Help! My head hurts.
In an ideal world, you wouldn't use Relays or even Subjects for such code. Instead of starting with a struct, you should start with a flow. How should data move through your system?
As an example, here is a view controller with view model that can convert Fahrenheit to Celsius and back:
struct TempInOut {
let fahrenheit: Observable<String>
let celsius: Observable<String>
}
func tempViewModel(input: TempInOut) -> TempInOut {
let celsius = input.fahrenheit
.compactMap { Double($0) }
.map { ($0 - 32) * 5.0/9.0 }
.map { "\($0)" }
let fahrenheit = input.celsius
.compactMap { Double($0) }
.map { $0 * 5.0/9.0 + 32 }
.map { "\($0)" }
return TempInOut(fahrenheit: fahrenheit, celsius: celsius)
}
The main thing to understand is how the data flows from input.fahrenheit to output.celsius, and how it flows from input.celsius to output.fahrenheit.
It's a different way of thinking about your program... I recently heard about the notion of "temporal design" and I think that's a good term of art for it.
Here is the view controller that would use the above view model.
class ViewController: UIViewController {
#IBOutlet weak var fahrenheitField: UITextField!
#IBOutlet weak var celsiusField: UITextField!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let input = TempInOut(
fahrenheit: fahrenheitField.rx.text.orEmpty.asObservable(),
celsius: celsiusField.rx.text.orEmpty.asObservable()
)
let output = tempViewModel(input: input)
disposeBag.insert(
output.fahrenheit.bind(to: fahrenheitField.rx.text),
output.celsius.bind(to: celsiusField.rx.text)
)
}
}
I am trying to use MVVM. I am going to VC2 from VC1. I am updating the viewModel.fromVC = 1, but the value is not updating in the VC2.
Here is what I mean:
There is a viewModel, in it there is a var fromVC = Int(). Now, in vc1, I am calling the viewModel as
let viewModel = viewModel().
Now, on the tap of button, I am updating the viewModel.fromVC = 8. And, moving to the next screen. In the next screen, when I print fromVC then I get the value as 0 instead of 8.
This is how the VC2 looks like
class VC2 {
let viewModel = viewModel()
func abc() {
print(viewModel.fromVC)
}
}
Now, I am calling abc() in viewDidLoad and the fromVC is printed as 0 instead of 8. Any help?
For the MVVM pattern you need to understand that it's a layer split in 2 different parts: Inputs & Outputs.
Int terms of inputs, your viewModel needs to catch every event from the viewController, and for the Outputs, this is the way were the viewModel will send data (correctly formatted) to the viewController.
So basically, if we have a viewController like this:
final class HomeViewController: UIViewController {
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
titleLabel.text = "toto"
}
}
We need to extract the responsibilities to a viewModel, since the viewController is handling the touchUp event, and owning the data to bring to th label.
By Extracting this, you will keep the responsibility correctly decided and after all, you'll be able to test your viewModel correctly 🙌
So how to do it? Easy, let's take a look to our futur viewModel:
final class HomeViewModel {
// MARK: - Private properties
private let title: String
// MARK: - Initializer
init(title: String) {
self.title = title
}
// MARK: - Outputs
var titleText: ((String) -> Void)?
// MARK: - Inputs
func viewDidLoad() {
titleText?("")
}
func buttonDidPress() {
titleText?(title)
}
}
So now, by doing this, you are keeping safe the different responsibilities, let's see how to bind our viewModel to our previous viewController :
final class HomeViewController: UIViewController {
// MARK: - public var
var viewModel: HomeViewModel!
// MARK: - Outlets
#IBOutlet private weak var titleLabel: UILabel!
// MARK: - View life cycle
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
// MARK: - Private func
private func bind(to viewModel: HomeViewModel) {
viewModel.titleText = { [weak self] title in
self?.titleLabel.text = title
}
}
// MARK: - Actions
#IBAction func buttonTouchUp(_ sender: Any) {
viewModel.buttonDidPress()
}
}
So one thing is missing, you'll asking me "but how to initialise our viewModel inside the viewController?"
Basically you should once again extract responsibilities, you could have a Screens layer which would have the responsibility to create the view like this:
final class Screens {
// MARK: - Properties
private let storyboard = UIStoryboard(name: StoryboardName, bundle: Bundle(for: Screens.self))
// MARK: - Home View Controller
func createHomeViewController(with title: String) -> HomeViewController {
let viewModel = HomeViewModel(title: title)
let viewController = storyboard.instantiateViewController(withIdentifier: "Home") as! HomeViewController
viewController.viewModel = viewModel
return viewController
}
}
And finally do something like this:
let screens = Screens()
let homeViewController = screens.createHomeViewController(with: "Toto")
But the main subject was to bring the possibility to test it correctly, so how to do it? very easy!
import XCTest
#testable import mvvmApp
final class HomeViewModelTests: XCTestCase {
func testGivenAHomeViewModel_WhenViewDidLoad_titleLabelTextIsEmpty() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
viewModel.titleText = { title in
XCTAssertEqual(title, "")
expectation.fulfill()
}
viewModel.viewDidLoad()
waitForExpectations(timeout: 1.0, handler: nil)
}
func testGivenAHomeViewModel_WhenButtonDidPress_titleLabelTextIsCorrectlyReturned() {
let viewModel = HomeViewModel(title: "toto")
let expectation = self.expectation("Returned title")
var counter = 0
viewModel.titleText = { title in
if counter == 1 {
XCTAssertEqual(title, "toto")
expectation.fulfill()
}
counter += 1
}
viewModel.viewDidLoad()
viewModel.buttonDidPress()
waitForExpectations(timeout: 1.0, handler: nil)
}
}
And that's it 💪