How can I make this subscribe loop not be nested? I can't seem to figure out how you would go about doing this because I push the view controller in the main subscribe loop, and not just set a value.
button.rx.tap.subscribe(onNext: { _ in
let viewController = MyViewController()
self.navigationController.pushViewController(viewController)
viewController.myPublishRelay.asObservable().subscribe(onNext: { value in
// do something with value
})
})
You desire two different side effects, so it makes sense to have two subscription. To prevent from nesting, you could do something in the lines of this:
let viewControllerToPresent = button.rx.tap.map { _ in
return MyViewController()
}.share()
viewControllerToPresent.subscribe(onNext: { [unowned self] viewController in
self.view.pushViewController(viewController)
}.disposed(by: self.disposeBag)
viewControllerToPresent.flatMapLatest { viewController in
viewController.myPublishRelay.asObservable()
}.subscribe(onNext: { value in
// do something with value
}.disposed(by: self.disposeBag)
The call to share is important here, otherwise the mapping after rx.tap would occur twice, making us subscribe to the publish relay of a view controller that is not the one we presented.
You can use .sample() or .combineLatest(), depending on how does your publishRelay update.
For example, Observable.combineLatest(myPublishRelay, button.rx.tap) { $0 }.subscribe(onNext: { value ...
See http://rxmarbles.com for reference on operators.
Whenever I see a nested subscribe I think of flatMap. Something like this should work:
button.rx.tap
.flatMap { _ in
let viewController = MyViewController()
self.navigationController.pushViewController(viewController)
return viewController.myPublishRelay.asObservable()
}
.subscribe(onNext: { value in
// do something with value
})
Related
This is my first question to the StackOverflow community so excuse me if I'm doing something wrong.
1. What I'm trying to achieve
Basically, I want to make a custom reactive wrapper around Eureka's SelectableSection class in order to observe the value of the selected row when it is changed. I'm thinking to get this data from the onSelectSelectableRow closure which is called every time a row is selected.
2. What I've tried to do for that
Actually, I've got this working but it's not a generic use of the custom wrapper, here is the example that works but only when I specify the row and its value type, for example ListCheckRow<Int>.
extension SelectableSection: ReactiveCompatible {}
extension Reactive where Base : SelectableSection<ListCheckRow<Int>> {
var selectedValue: Observable<Base.SelectableRow.Cell.Value?> {
return Observable.create { observer in
self.base.onSelectSelectableRow = {cell, row in
observer.onNext(row.value)
}
return Disposables.create {
observer.onCompleted()
}
}
}
}
This works fine and as I expected but when it comes to something more generic like the next code example, I get an error saying that: "Cannot assign to property: 'base' is a 'let' constant"
extension SelectableSection: ReactiveCompatible {}
extension Reactive where Base : SelectableSectionType {
var selectedValue: Observable<Base.SelectableRow.Cell.Value?> {
return Observable.create { observer in
self.base.onSelectSelectableRow = {cell, row in // Error: Cannot assign to property: 'base' is a 'let' constant
observer.onNext(row.value)
}
return Disposables.create {
observer.onCompleted()
}
}
}
}
Any help will be much appreciated, thanks. 🙏
The fundamental problem here is that SelectableSectionType is a protocol that isn't restricted to class types and Reactive assumes that Base is a class (or otherwise is not going to be modified by the observable creation.)
I think the most generic you can make this is something like:
extension Reactive {
func selectedValue<Row, T>() -> Observable<T?> where Base: SelectableSection<Row>, Row: SelectableRowType, T == Row.Cell.Value {
Observable.create { [base] observer in
base.onSelectSelectableRow = { cell, row in
observer.onNext(row.value) // this is problematic. See below.
}
return Disposables.create {
observer.onCompleted() // this is wrong. See below.
}
}
}
}
The biggest problem with the above though is that if you subscribe to the resulting Observable more than once or create more than one Observable using this computed property, all but the last subscription will silently fail. The simple way to fix this is to always remember to share any result but that's rather error prone.
The way to fix this would be to associate a Subject with each SelectableSection, but you can't modify the class, so what are we to do?
Here's a solution:
extension Reactive {
func selectedValue<Row, T>() -> Observable<T?> where Base: SelectableSection<Row>, Row: SelectableRowType, T == Row.Cell.Value {
Observable.create { [base] observer in
if let block = selectableSections.first(where: { $0.section === base }) {
let subject = block.subject as! PublishSubject<T?>
return Disposables.create(
block.disposable.retain(),
subject.subscribe(observer)
)
}
else {
let subject = PublishSubject<T?>()
let block = SelectableSectionBlock(
section: base,
subject: subject,
disposable: RefCountDisposable(disposable: Disposables.create {
selectableSections.removeAll(where: { $0.section === base })
})
)
base.onSelectSelectableRow = { cell, row in
subject.onNext(row.value)
}
selectableSections.append(block)
return Disposables.create(
block.disposable,
subject.subscribe(observer)
)
}
}
}
}
private struct SelectableSectionBlock {
let section: Section
let subject: Any
let disposable: RefCountDisposable
}
private var selectableSections = [SelectableSectionBlock]()
The selectableSections array stores a Subject and RefCountDisposable for each SelectableSection.
Whenever an Observable is created, or subscribed to...
if it's the first time working with this section, it will create a Subject and RefCountDisposable assign the onSelectSelectableRow to send a next event to the subject and store the subject in the array.
otherwise it will find the subject and disposable associated with this Section and retain the disposable.
Once it has the subject and disposable from above, it will subscribe the new observer to the subject and return a new Disposable that will remove that subscription and decrement the ref-count when the time comes.
Yes this is quite a bit more complex than the simple assignment case, but it's the right thing to do.
As for calling onCompleted() inside the disposable closure. By the time the closure is called, the observer has already emitted an onCompleted/onError event, or the observer has stopped listening to the observable. So this event will never be seen.
I have this editor view model that I use in different other view models. The parent view models have a selectable user, once a user is selected, I'm gonna need a new instance of the editor with the new user.
This is a simplified version of the editor and a parent.
class EditorViewModel {
let user: String
let item = PublishSubject<String>()
init(user: String) {
self.user = user
}
}
class ParentViewModel {
var editor: Observable<EditorViewModel>!
let user = BehaviorSubject<String?>(value: nil)
init() {
editor = user.compactMap { $0 }.map { EditorViewModel(user: $0) }
}
}
Once the editor saves an item, I expect to get the saved item by flatMaping the editor to its item. Like this:
let parent = ParentViewModel()
parent.editor.flatMapLatest { $0.item }.debug("item").subscribe(onNext: { item in
print("This doesn't print")
})
parent.editor.subscribe(onNext: { editor in
print("This one prints")
editor.item.onNext("something")
})
parent.user.onNext("1")
The flatMap line does subscribe but it never gets an item.
This is the output for running the code above in the playground:
2021-10-28 13:47:41.528: item -> subscribed
This one prints
Also, if you think this is too crazy a setup, I concur and am open to suggestions.
By default, Observables are cold. This means that each subscription works independently and in this case each subscription is getting a different EditorViewModel. (The .map { EditorViewModel(user: $0) } Observable will call its closure for each subscription.)
Adding a .share() or .share(replay: 1) after the .map { EditorViewModel(user: $0) } Observable will make it hot which means that all subscriptions will share the same effect.
As to your sub-question. I don't think I would setup such a system unless something outside of this code forced me to. Instead, I would pass an Observable into the EditorViewModel instead of a raw User. That way you don't need to rebuild editor view models every time the user changes.
I'm pretty new in RX, so i'm kind confused how to do it correctly.
My view model:
let feedItems: BehaviorSubject<[FeedItem]> = BehaviorSubject(value: [FeedItem]())
let isLoadingMore: PublishSubject<Bool> = PublishSubject()
let loadPageTrigger: PublishSubject<Void> = PublishSubject()
let isRefreshing: PublishSubject<Bool> = PublishSubject()
func fetchFeed(showLoading: Bool = false, loadMore: Bool = false) {
//api call and set self.feedItems.onNext(response) after completion
}
In my VC i bind feedItems to collectionView, check when collectionView in bottom and bind it to isLoadingMore, and also bind UIRefresherControl to isRefreshing.
In my ViewModel i want to get value of isRefreshing and isLoadingMore in loadPageTrigger and call fetchFeed with some parameters:
ViewModel:
override init() {
super.init()
loadPageTrigger.subscribe(onNext: {
// need to get values of isRefreshing and isLoadingMore to call fetchFeed with params
self.fetchFeed()
}).disposed(by: disposeBag)
}
Subjects provide a convenient way to poke around Rx, however they are not recommended for day to day use.
-- Intro to Rx
If you’re loading up your view model full of Subjects then you haven't quite gotten the grasp of Rx yet. That's okay though, everybody has to start somewhere.
The most direct solution based on what you already have is something like this:
loadPageTrigger
.withLatestFrom(Observable.combineLatest(isRefreshing, isLoadingMore))
.subscribe(onNext: { [unowned self] isRefreshing, isLoadingMore in
// here you have isRefreshing and isLoadingMore as Bools
self.fetchFeed()
})
.disposed(by: disposeBag)
Note the unowned self. It is very important to make sure you aren't capturing self in closures that are being retained by the disposeBag which is held in self.
The optimum solution would involve only Observables and no Subjects at all.
The IObservable<T> 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
I came across this problem when testing my View:
In my ViewModel I call to an asynchronous operation and when the response arrives, I use a PublishSubject to produce a change in my View. In my View, I call DispatchQueue.main.async in order to hide or show a button.
ViewModel
let refreshButtons = PublishSubject<Bool>(true)
refreshButtons.onNext(true)
View
model.refreshButtons.asObservable()
.subscribe(onNext: {
[unowned self] success in
self.updateButtons(success)
})
.addDisposableTo(disposable)
private func updateButtons(_ show:Bool) {
DispatchQueue.main.async{
button.isHidden = !show
}
}
Now I don't know how to unit test that refreshButtons.onNext(true) will hide or show my button.
The solutions I can think of are:
Overriding the method and having an async expectation, but for that I need to make the method public, what I don't want, or
Dispatching the main queue in my ViewModel and not in the view, what it sounds odd to me, but might me ok.
How can I solve this?
Thank you in advance.
You could use an async expectation based on a predicate in your unit test to wait an see if the button is not hidden anymore.
func testButtonIsHidden() {
// Setup your objects
let view = ...
let viewModel = ...
// Define an NSPredicate to test your expectation
let predicate = NSPredicate(block: { input, _ in
guard let _view = input as? MyView else { return false }
return _view.button.isHidden == true
})
// Create an expectation that will periodically evaluate the predicate
// to decided whether it's fulfilled or not
_ = expectation(for: predicate, evaluatedWith: view, handler: .none)
// Call the method that should generate the behaviour you are expecting.
viewModel.methodThatShouldResultInButtonBeingHidden()
// Wait for the
waitForExpectationsWithTimeout(1) { error in
if let error = error {
XCTFail("waitForExpectationsWithTimeout errored: \(error)")
}
}
}
Something worth noting is that the value you pass to the NSPredicate should be a class. That is because classes are passed by reference, so value inside the predicate block will be the same as the one touched by your view model. If you were to pass a struct or enum though, which are passed by copy, the predicate block would receive a copy of the value as it is at the time of running the setup code, and it will always fail.
If instead you prefer to use UI tests as suggested by #Randall Wang in his answer, then this post might be useful for you: "How to test UI changes in Xcode 7". Full disclosure, I wrote that post.
First of all, You don't need test private method
If you want to test if the button is hidden or not,try UI testing
here is the WWDC of UI testing.
https://developer.apple.com/videos/play/wwdc2015/406/
I'm trying to use Action / CocoaAction library.
The primary usage for now is to show an UIAlertController and when an UIAlertAction button is tapped it has to call a function defined in my viewModel (changeAddress that returns an Observable).
My understanding of this would be:
let ac = CocoaAction(workFactory: {[unowned self] _ in
self.viewModel!.requestChangeAddress()
.subscribeNext({ [unowned self] data in
if let response = data?.result
{
self.showResultOperation(response)
}
})
.addDisposableTo(self.disposeBag)
return .empty()
})
let OKAction = UIAlertAction.Action("OK", style: .Default)
OKAction.rx_action = ac
But unfortunately it doesn't work. The workFactory closure is correctly called but the subscription doesn't take effect. I know something is wrong when I return .empty but I cannot understand how to solve.
How can I correct this? What I'm doing wrong?
great question! Thanks for posting the code.
So your code is getting the observable from requestChangeAddress, but then it's subscribing to it and adding it to a dispose bag. The Action class is actually going to take care of that for you, you only need to return the disposable.
The problem is that you want to do something with the values sent on the action, and returning the observable won't let you do that. So you need to include a call to doOnNext in the observer chain. Here's what it might look like:
let ac = CocoaAction(workFactory: {[unowned self] _ in
return self.viewModel!
.requestChangeAddress()
.doOnNext({ [unowned self] data in
if let response = data?.result
{
self.showResultOperation(response)
}
})
.map { _ in Void() }
})
Using doOn functions in RxSwift is a great way to inject side-effects into your observables when necessary, like it is here.
EDIT: I added the map at the end to fix a compiler error, because the return type from the factory method is Observable<Void>.