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.
Related
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
As I understand, it is best to only test public methods of a class.
Let's have a look at this example. I have a view model for the view controller.
protocol MyViewModelProtocol {
var items: [SomeItem] { get }
var onInsertItemsAtIndexPaths: (([IndexPath]) -> Void)? { get set }
func viewLoaded()
}
class MyViewModel: MyViewModelProtocol {
func viewLoaded() {
let items = createDetailsCellModels()
updateCellModels(with: items)
requestDetails()
}
}
I want to test class viewLoaded(). This class calls two other methods - updateItems() and requestDetails()
One of the methods sets up the items and the other one call API to retrieve data and update those items. Items array us updated two times and onInsertItemsAtIndexPaths are called two times - when setting up those items and when updating with new data.
I can test whether after calling viewLoaded() expected items are set up and that onInsertItemsAtIndexPaths is called.
However, the test method will become rather complex.
What is your view, should I test those two methods separately or just write this one huge test?
By testing only viewLoaded(), my idea is that the implementation can change and I only care that results are what I expect.
I think the same thing, only public functions should be tested, since public ones use private ones, and your view on MVVM is correct. You can improve it by adding a DataSource and a Mapper that allows you to improve testing.
However, yes, the test seems huge to me, the tests should test simple units and ensure that small parts of the code work well, with the example you show is difficult, you need to divide by layers (clean code).
In the example you load the data into the viewModel and make it difficult to mockup the data. But if you have a Domain layer you can pass the UseCase mock to the viewModel and control the result. If you run a test on your example, the result will also depend on what the endpoint returns. (404, 200, empty array, data with error ...). So it is important, for testing purposes, to have a good separation by layers. (Presentation, Domain and Data) to be able to test each one separately.
I give you an example of how I would test a view mode, sure there are better and cooler examples, but it's an approach.
Here you can see a viewModel
protocol BeersListViewModel: BeersListViewModelInput, BeersListViewModelOutput {}
protocol BeersListViewModelInput {
func viewDidLoad()
func updateView()
func image(url: String?, index: Int) -> Cancellable?
}
protocol BeersListViewModelOutput {
var items: Box<BeersListModel?> { get }
var loadingStatus: Box<LoadingStatus?> { get }
var error: Box<Error?> { get }
}
final class DefaultBeersListViewModel {
private let beersListUseCase: BeersListUseCase
private var beersLoadTask: Cancellable? { willSet { beersLoadTask?.cancel() }}
var items: Box<BeersListModel?> = Box(nil)
var loadingStatus: Box<LoadingStatus?> = Box(.stop)
var error: Box<Error?> = Box(nil)
#discardableResult
init(beersListUseCase: BeersListUseCase) {
self.beersListUseCase = beersListUseCase
}
func viewDidLoad() {
updateView()
}
}
// MARK: Update View
extension DefaultBeersListViewModel: BeersListViewModel {
func updateView() {
self.loadingStatus.value = .start
beersLoadTask = beersListUseCase.execute(completion: { (result) in
switch result {
case .success(let beers):
let beers = beers.map { DefaultBeerModel(beer: $0) }
self.items.value = DefaultBeersListModel(beers: beers)
case .failure(let error):
self.error.value = error
}
self.loadingStatus.value = .stop
})
}
}
// MARK: - Images
extension DefaultBeersListViewModel {
func image(url: String?, index: Int) -> Cancellable? {
guard let url = url else { return nil }
return beersListUseCase.image(with: url, completion: { (result) in
switch result {
case .success(let imageData):
self.items.value?.items?[index].image.value = imageData
case .failure(let error ):
print("image error: \(error)")
}
})
}
}
Here you can see the viewModel test using mocks for the data and view.
class BeerListViewModelTest: XCTestCase {
private enum ErrorMock: Error {
case error
}
class BeersListUseCaseMock: BeersListUseCase {
var error: Error?
var expt: XCTestExpectation?
func execute(completion: #escaping (Result<[BeerEntity], Error>) -> Void) -> Cancellable? {
let beersMock = BeersMock.makeBeerListEntityMock()
if let error = error {
completion(.failure(error))
} else {
completion(.success(beersMock))
}
expt?.fulfill()
return nil
}
func image(with imageUrl: String, completion: #escaping (Result<Data, Error>) -> Void) -> Cancellable? {
return nil
}
}
func testWhenAPIReturnAllData() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "All OK")
beersListUseCaseMock.error = nil
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.items.bind { (_) in}
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNotNil(viewModel.items.value)
XCTAssertNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
func testWhenDataReturnsError() {
let beersListUseCaseMock = BeersListUseCaseMock()
beersListUseCaseMock.expt = self.expectation(description: "Error")
beersListUseCaseMock.error = ErrorMock.error
let viewModel = DefaultBeersListViewModel(beersListUseCase: beersListUseCaseMock)
viewModel.updateView()
waitForExpectations(timeout: 10, handler: nil)
XCTAssertNil(viewModel.items.value)
XCTAssertNotNil(viewModel.error.value)
XCTAssert(viewModel.loadingStatus.value == .stop)
}
}
in this way you can test the view, the business logic and the data separately, in addition to being a code that is very reusable.
Hope this helps you, I have it posted on github in case you need it.
https://github.com/cardona/MVVM
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've got to two String publishers and one computed property which returns AnyPublisher. Logic is quite simple but I would like to know if there is any way to propagate initial value. I think it should be somehow possible since publishers have initial values.
In VC I'm assigning new values to Publishers from ViewModel (from textField).
firstTextField.addTarget(self, action: #selector(firstTextFieldDidChange(_:)), for: .editingChanged)
secondTextField.addTarget(self, action: #selector(secondTextFieldDidChange(_:)), for: .editingChanged)
#objc private func firstTextFieldDidChange(_ textField: UITextField) {
viewModel.firstPublisher = textField.text ?? ""
}
#objc private func secondTextFieldDidChange(_ textField: UITextField) {
viewModel.secondPublisher = textField.text ?? ""
}
And then I'm assigning Publisher (combineLatest) to my button:
_ = viewModel.validatedText
.receive(on: RunLoop.main)
.assign(to: \.isEnabled, on: button)
In VM I've got two Publishers:
#Published var firstPublisher: String = ""
#Published var secondPublisher: String = ""
and CombineLatest:
var validatedText: AnyPublisher<Bool, Never> {
return Publishers.CombineLatest($firstPublisher, $secondPublisher) {
return !($0.isEmpty || $1.isEmpty)
}.eraseToAnyPublisher()
}
validatedText only starts publishing new values when I start typing in both text fields. I tried assigning some new values in init of VM for example (to first and second Publisher) but it also didn't work. Is there any way to do it or I will have to set initial state of button (disable it) without using combine?
Unfortunately, it seems like this just may be the behavior of #Published, but you can work around this in your generated Publisher by prepending an initial value:
var validatedText: AnyPublisher<Bool, Never> {
let validate: (String, String) -> Bool = {
!($0.isEmpty || $1.isEmpty)
}
return Publishers.CombineLatest($firstPublisher, $secondPublisher, transform: validate)
.prepend(validate(firstPublisher, secondPublisher))
.eraseToAnyPublisher()
}
Conversely, it is fairly trivial to write your own property delegate to get the behavior you want if you'd rather take that approach:
import Combine
#propertyDelegate
struct InitialPublished<Value> : Publisher {
typealias Output = Value
typealias Failure = Never
private let subject: CurrentValueSubject<Output, Failure>
var value: Value {
set { subject.value = newValue }
get { subject.value }
}
init(initialValue: Value) {
subject = CurrentValueSubject(initialValue)
}
func receive<S>(subscriber: S) where S: Subscriber, Value == S.Input, Failure == S.Failure {
subject.receive(subscriber: subscriber)
}
}
I'm using RxSwift with MVVM and I'm found myself a bit confused. Here's why:
My Code Right Now
ViewModel
internal protocol DetailViewModelInput {
func viewDidLoad(with name: String)
}
internal protocol DetailViewModelOutput {
var gnomeObject: Observable<Gnome?> { get }
}
struct DetailViewModel: DetailViewModelType, DetailViewModelInput, DetailViewModelOutput {
let disposeBag = DisposeBag()
let gnomeObject: Observable<Gnome?>
init() {
gnomeObject = viewDidLoadProperty
.asObservable()
.filter { !$0.isEmpty }
.map { guard let gnome = Gnome
.fetch(uniqueValue: $0, forKey: "name")! as? Gnome else { return nil }
return gnome
}
}
let viewDidLoadProperty = Variable<String>("")
func viewDidLoad(with name: String) {
viewDidLoadProperty.value = name
}
}
ViewController
I make the binding as follows:
func bindViewModel() {
viewModel.outputs.gnomeObject
.subscribe { observable in self.populate(with: observable.element != nil ? observable.element! : nil) }
.addDisposableTo(viewModel.disposeBag)
}
And this is "fine". It works perfectly (at least as expected).
But, I while reading the following book: https://victorqi.gitbooks.io/rxswift/content/tips.html
In the tips section it says:
Always strive to model your systems or their parts as pure functions. Those pure functions can be tested easily and can be used to modify operator behaviors.
And after reading it I'm changed my ViewModel as follows:
ViewModel (Edited)
internal protocol DetailViewModelInput {
func viewDidLoad(with name: String)
}
internal protocol DetailViewModelOutput {
func gnomeObject() -> Observable<Gnome?>
}
protocol DetailViewModelType {
var disposeBag: DisposeBag { get }
var inputs: DetailViewModelInput { get }
var outputs: DetailViewModelOutput { get }
}
struct DetailViewModel: DetailViewModelType, DetailViewModelInput {
let disposeBag = DisposeBag()
let viewDidLoadProperty = Variable<String>("")
func viewDidLoad(with name: String) {
viewDidLoadProperty.value = name
}
}
// MARK: DetailViewModelOutput
extension DetailViewModel: DetailViewModelOutput {
func gnomeObject() -> Observable<Gnome?> {
return viewDidLoadProperty
.asObservable()
.filter { !$0.isEmpty }
.map { guard let gnome = Gnome
.fetch(uniqueValue: $0, forKey: "name")! as? Gnome else { return nil }
return gnome
}
}
}
The difference in the ViewModels is the GnomeObject declaration, in one it is a var and in the "edited" is a func.
My concern is, that every time gnomeObject() gets called from the ViewController, it will create a new instance of the observable.
What should be the best practice in this case?
Hmm, in the first version, gnomeObject is a let, not a var. Once it is set, it is never changed to a different object.
In the second version gnomeObject() returns a different object every time it's called. So this actually breaks the "pure function" paradigm. (Note: if the Observable was a struct instead of a class then this wouldn't be the case because structs don't have identity.)
Your first example follows the pure function concept while your second version breaks it.
If you're looking to eliminate the need to instantiate gnomeObject in the initializer, you could modify the first example to use a lazy var like so:
lazy var gnomeObject: Observable<Gnome?> = self.viewDidLoadProperty
.asObservable()
.filter { !$0.isEmpty }
.map { guard let gnome = Gnome
.fetch(uniqueValue: $0, forKey: "name")! as? Gnome else { return nil }
return gnome
}
When they say you should use pure functions they mean that functions (when possible) should have the same output for the same set of inputs, meaning, if a function is called twice with the same set of inputs it should return the same thing twice.
That means you don't have any hidden mutable state that the caller of the functions is not aware of (a property in the class that owns the method, for example). Everything should be as explicit as possible.
So, it's something you should be aware of when it comes to functions. But it's completely ok to use properties, as you were doing in the first code, they don't apply to this.