RxSwift set observable value in another observable next , not working - ios

I am new to ios and rxswift. Trying create mvvm architecture for new app.
If I set observable isSuccess value before calling appStartNetwork.fetchApp() I can observe value. But when I set isSuccess value in fetchApp() on next, Observer in viewcontroller cant be triggered
Whats wrong?
ViewModel
class SplashViewModel {
var isSuccess = PublishSubject<Bool>()
var isLoading = PublishSubject<Bool>()
private let bag = DisposeBag()
func fetchAppStart() {
self.isLoading.onNext(true)
let appStartNetwork=NetworkProvider.shared.makeAppStartNetwork()
appStartNetwork.fetchApp().subscribe(onNext: { [weak self] apiResult in
switch apiResult{
case let .success(response):
//some codes
self?.isLoading.onNext(false)
self?.isSuccess.onNext(true)
break
case let .failure(errorContent):
break
}
},onError:{ err in
self.isLoading.onNext(false)
self.isSuccess.onNext(false)
}).disposed(by: bag)
} }
View Controller
func getAppStart(){
let splashVm=SplashViewModel()
let disposeBag = DisposeBag()
splashVm.isSuccess.subscribe(onNext: { (ok) in
if(ok){
print("splash success")
self.navigateMain()
}else{
self.showAlert("splash fail")
}
},onError:{ err in
self.showAlert(err.localizedDescription)
}).disposed(by: disposeBag)
splashVm.fetchAppStart()
}

There are two problems here, both created by the same programming error, a wrong management of the dispose bag lifecycles.
By creating your dispose bag within the scope of getAppStart, you are bounding its lifecycle to the function's life time. Meaning the dispose bag will dispose of its attached subscriptions when the function finishes.
Moving the creating of both disposeBag and splashVm to the view controller's scope (outside the function), should fix your issue.
let splashVm=SplashViewModel()
let disposeBag = DisposeBag()
func getAppStart(){
splashVm.isSuccess.subscribe(onNext: { (ok) in
if(ok){
print("splash success")
self.navigateMain()
}else{
self.showAlert("splash fail")
}
},onError:{ err in
self.showAlert(err.localizedDescription)
}).disposed(by: disposeBag)
splashVm.fetchAppStart()
}

Related

Why doesn't the subscription of the relay work even though there is a value passed to it?

I have set up a relay like this:
class CustomAlertViewController: UIViewController {
private let disposeBag = DisposeBag()
var alertTextRelay = BehaviorRelay<String>(value: "")
alertTextField.rx.value.orEmpty.changed.subscribe(onNext: { [weak self] value in
print("THIS IS RIGHT", value)
self?.alertTextRelay.accept(value)
print("THIS IS ALSO RIGHT", self?.alertTextRelay.value)
}).disposed(by: disposeBag)
...
I get the text writen in the textField and accept it to the relay and check so that the value is really there. But the subscription never trigger:
class RecievingClass: UIViewController {
let disposeBag = DisposeBag()
let instance = CustomAlertViewController()
instance.alertTextRelay.subscribe(onNext: { value in
self.myText = value
}, onError: { error in
print(error)
}, onCompleted: {
print("completed")
}).disposed(by: disposeBag)
...
Nothing in the subscription is triggered. Why? If it helps, CustomAlertViewController is used as a custom alert (a view on an overlay). Also, there are a lot of static functions in CustomAlertViewController. Not sure if that's relevant. Let me know if I can provide anything else.
Try to change this alertTextField.rx.value.orEmpty.changed.subscribe on this alertTextField.rx.text.orEmpty.asObservable(). And add [weak self] for RecievingClass in other way you get memory leak.

Unexpected nil after adding programmatic view instead of storyboard

I have an application written in the MVVM-C pattern, using RxSwift
After adding a new view programmatically, the application crashes with a
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an
Optional value
error. I am at a complete loss, the implementation is almost exactly the same, minus the fact one view controller is a storyboard and one is not.
This is my new ViewController
import UIKit
import RxSwift
import RxCocoa
final class FeedViewController: TableViewController, ViewModelAttaching {
var viewModel: Attachable<FeedViewModel>!
var bindings: FeedViewModel.Bindings {
let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.mapToVoid()
.asDriverOnErrorJustComplete()
let refresh = tableView.refreshControl!.rx
.controlEvent(.valueChanged)
.asDriver()
return FeedViewModel.Bindings(
fetchTrigger: Driver.merge(viewWillAppear, refresh),
selection: tableView.rx.itemSelected.asDriver()
)
}
override func viewDidLoad() {
super.viewDidLoad()
}
func bind(viewModel: FeedViewModel) -> FeedViewModel {
viewModel.posts
.drive(tableView.rx.items(cellIdentifier: FeedTableViewCell.reuseID, cellType: FeedTableViewCell.self)) { _, viewModel, cell in
cell.bind(to: viewModel)
}
.disposed(by: disposeBag)
viewModel.fetching
.drive(tableView.refreshControl!.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.errors
.delay(0.1)
.map { $0.localizedDescription }
.drive(errorAlert)
.disposed(by: disposeBag)
return viewModel
}
}
This is an existing one, that works but uses storyboards
final class PostsListViewController: TableViewController, ViewModelAttaching {
var viewModel: Attachable<PostsListViewModel>!
var bindings: PostsListViewModel.Bindings {
let viewWillAppear = rx.sentMessage(#selector(UIViewController.viewWillAppear(_:)))
.mapToVoid()
.asDriverOnErrorJustComplete()
let refresh = tableView.refreshControl!.rx
.controlEvent(.valueChanged)
.asDriver()
return PostsListViewModel.Bindings(
fetchTrigger: Driver.merge(viewWillAppear, refresh),
selection: tableView.rx.itemSelected.asDriver()
)
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
// MARK: - View Methods
private func setupView() {
title = "Posts"
}
func bind(viewModel: PostsListViewModel) -> PostsListViewModel {
viewModel.posts
.drive(tableView.rx.items(cellIdentifier: PostTableViewCell.reuseID, cellType: PostTableViewCell.self)) { _, viewModel, cell in
cell.bind(to: viewModel)
}
.disposed(by: disposeBag)
viewModel.fetching
.drive(tableView.refreshControl!.rx.isRefreshing)
.disposed(by: disposeBag)
viewModel.errors
.delay(0.1)
.map { $0.localizedDescription }
.drive(errorAlert)
.disposed(by: disposeBag)
return viewModel
}
}
They are basically exactly the same. The exception is thrown on the let refresh = tableView.refreshControl!.rx line.
The working coordinator, using a Storyboard is
import RxSwift
class PostsCoordinator: BaseCoordinator<Void> {
typealias Dependencies = HasPostService
private let navigationController: UINavigationController
private let dependencies: Dependencies
init(navigationController: UINavigationController, dependencies: Dependencies) {
self.navigationController = navigationController
self.dependencies = dependencies
}
override func start() -> Observable<Void> {
let viewController = PostsListViewController.instance()
navigationController.viewControllers = [viewController]
let avm: Attachable<PostsListViewModel> = .detached(dependencies)
let viewModel = viewController.attach(wrapper: avm)
viewModel.selectedPost
.drive(onNext: { [weak self] selection in
self?.showDetailView(with: selection)
})
.disposed(by: viewController.disposeBag)
// View will never be dismissed
return Observable.never()
}
private func showDetailView(with post: Post) {
let viewController = PostDetailViewController.instance()
viewController.viewModel = PostDetailViewModel(post: post)
navigationController.showDetailViewController(viewController, sender: nil)
}
}
I have an extension to allow me to instantiate it also
protocol Reusable {
static var reuseID: String { get }
}
extension Reusable {
static var reuseID: String {
return String(describing: self)
}
}
// MARK: - View Controller
extension UIViewController: Reusable {
class func instance() -> Self {
let storyboard = UIStoryboard(name: reuseID, bundle: nil)
return storyboard.instantiateViewController()
}
}
extension UIStoryboard {
func instantiateViewController<T: UIViewController>() -> T {
guard let viewController = self.instantiateViewController(withIdentifier: T.reuseID) as? T else {
fatalError("Unable to instantiate view controller: \(T.self)")
}
return viewController
}
}
The 'broken' coordinator is exactly the same, except I swapped
let viewController = PostsListViewController.instance()
for
let viewController = FeedViewController()
I am at a complete loss at to why this is throwing. Print statements and breakpoints at various points haven't turned up a nil on any values.
Please let me know if it would be easier to share a sample app as I appreciate the code snippets may not be the most obvious.
tableView.refreshControl is nil. You are trying to force access the nil refreshControl.
The Refreshing property is Enabled for the UITableViewController in your storyboard that works. In the programmatic version, the refreshControl is not created automatically.
The default value of the refreshControl property is nil. You need to instantiate and assign a UIRefreshControl to self.refreshControl before it exists.
When you create your view using a Storyboard and enable it, this is taken care of behind the scenes for you. Programmatically you will be required to implement this yourself.

RxSwift properly dispose subscription in closure

I am writing a wrapper around Firebase authentication functions to return Observable and add additional profileIncomplete state. It basically first checks whether a user is logged in, if so, check whether the user's profile is complete. The following is my code, I wonder whether it is okay to subscribe to an observable in Observable.create and, if so, how do I properly dispose the disposable in this case? create a DisposeBag inside the closure?
enum State {
case loggedIn
case profileIncomplete
case notLoggedIn
}
func listenToAuthState() -> Observable<State> {
return Observable.create { observable in
let authStateHandle = Auth.auth().addStateDidChangeListener() { [weak self] (_, user) in
guard let user = user else {
observable.onNext(.notLoggedIn)
return
}
let disposable = self?.listenToProfileCompleted(uid: user.uid).subscribe(onNext: { (completed) in
if completed {
observable.onNext(.loggedIn)
observable.onCompleted()
} else {
observable.onNext(.profileIncomplete)
}
})
// How to dispose the disposable???
}
return Disposables.create {
Auth.auth().removeStateDidChangeListener(authStateHandle) }
}
}
func listenToProfileCompleted(uid: String) -> Observable<Bool> { ... }
I think subscribing inside a Observable.create (or inside a different subscribe block) is a code-smell.
It seems you have two separate concerns. stateChanged and profileCompleted.
I would split those into two different methods, having listenToAuthState only in charge of reflecting the result of addStateDidChangeListener, and have a separate one for listenToProfileCompleted.
This will let you have a separate "ready" (or however you want to call it) that can zip the two. Or otherwise use flatMap, if the auth status must change before you listen to the profile completion.
To dispose resource you can add it to DisposeBag. Like below
func listenToAuthState() -> Observable<State> {
return Observable.create { observable in
var disposeBag:DisposeBag! = DisposeBag()
let authStateHandle = Auth.auth().addStateDidChangeListener() { [weak self] (_, user) in
guard let user = user else {
observable.onNext(.notLoggedIn)
return
}
let disposable = self?.listenToProfileCompleted(uid: user.uid).subscribe(onNext: { (completed) in
if completed {
observable.onNext(.loggedIn)
observable.onCompleted()
} else {
observable.onNext(.profileIncomplete)
}
}).disposed(by: disposeBag)
// How to dispose the disposable???
}
return Disposables.create {
Auth.auth().removeStateDidChangeListener(authStateHandle)
disposeBag = nil
}
}
}

Prevent disposal of PublishSubject (RxSwift)

I'm struggling with specific use-case incorporating RxSwift's PublishSubject.
For sake of simplicity unimportant details were omitted.
There is a MVVM setup. In VC I have a UIButton, on tap of which a network call should dispatch. In ViewModel I have a buttonDidTapSubject: PublishSubject<Void>.
class ViewModel {
let disposeBag = DisposeBag()
let buttonDidTapSubject = PublishSubject<Void>()
let service: Service
typealias Credentials = (String, String)
var credentials: Observable<Credentials> {
return Observable.just(("testEmail", "testPassword"))
}
init(_ service: Service) {
self.service = service
buttonDidTapSubject
.withLatestFrom(credentials)
.flatMap(service.login) // login method has signature func login(_ creds: Credentials) -> Observable<User>
.subscribe(onNext: { user in print("Logged in \(user)") },
onError: { error in print("Received error") })
.disposed(by: disposeBag)
}
}
class ViewController: UIViewController {
let viewModel: ViewModel
let button = UIButton()
init(_ viewModel: ViewModel) {
self.viewModel = viewModel
}
}
In controller's viewDidLoad I make a binding:
override func viewDidLoad() {
button.rx.tap.asObservable()
.subscribe(viewModel.buttonDidTapSubject)
.disposed(by: disposeBag)
}
The problem is, since network request can fail and Observable that is returned from login(_:) method will produce an error, the whole subscription to buttonDidTapSubject in ViewModel will be disposed. And all other taps on a button will not trigger sequence to login in ViewModel.
Is there any way to avoid this kind of behavior?
You can use retry to prevent finishing the subcription. If you only want to retry in specific cases or errors you can also use retryWhen operator
In the view model:
lazy var retrySubject: Observable<Void> = {
return viewModel.buttonDidTapSubject
.retryWhen { error in
if (error == .networkError){ //check here your error
return .just(Void())
} else {
return .never() // Do not retry
}
}
}()
In the view controller I would have done it in another way:
override func viewDidLoad() {
super.viewDidLoad()
button.rx.tap.asObservable()
.flatMap { [weak self] _ in
return self?.viewModel.retrySubject
}
.subscribe(onNext: {
//do whatever
})
.disposed(by: disposeBag)
}
Not sure if still relevant - Use PublishRelay ( although it is RxCocoa )

Unit-test RxSwift observable in ViewController

I'm quite new to RxSwift. I have a view controller that has a typeahead/autocomplete feature (i.e., user types in a UITextField and as soon as they enter at least 2 characters a network request is made to search for matching suggestions). The controller's viewDidLoad calls the following method to set up an Observable:
class TypeaheadResultsViewController: UIViewController {
var searchTextFieldObservable: Observable<String>!
#IBOutlet weak var searchTextField: UITextField!
private let disposeBag = DisposeBag()
var results: [TypeaheadResult]?
override func viewDidLoad() {
super.viewDidLoad()
//... unrelated setup stuff ...
setupSearchTextObserver()
}
func setupSearchTextObserver() {
searchTextFieldObservable =
self.searchTextField
.rx
.text
.throttle(0.5, scheduler: MainScheduler.instance)
.map { $0 ?? "" }
searchTextFieldObservable
.filter { $0.count >= 2 }
.flatMapLatest { searchTerm in self.search(for: searchTerm) }
.subscribe(
onNext: { [weak self] searchResults in
self?.resetResults(results: searchResults)
},
onError: { [weak self] error in
print(error)
self?.activityIndicator.stopAnimating()
}
)
.disposed(by: disposeBag)
// This is the part I want to test:
searchTextFieldObservable
.filter { $0.count < 2 }
.subscribe(
onNext: { [weak self] _ in
self?.results = nil
}
)
.disposed(by: disposeBag)
}
}
This seems to work fine, but I'm struggling to figure out how to unit test the behavior of searchTextFieldObservable.
To keep it simple, I just want a unit test to verify that results is set to nil when searchTextField has fewer than 2 characters after a change event.
I have tried several different approaches. My test currently looks like this:
class TypeaheadResultsViewControllerTests: XCTestCase {
var ctrl: TypeaheadResultsViewController!
override func setUp() {
super.setUp()
let storyboard = UIStoryboard(name: "MainStoryboard", bundle: nil)
ctrl = storyboard.instantiateViewController(withIdentifier: "TypeaheadResultsViewController") as! TypeaheadResultsViewController
}
override func tearDown() {
ctrl = nil
super.tearDown()
}
/// Verify that the searchTextObserver sets the results array
/// to nil when there are less than two characters in the searchTextView
func testManualChange() {
// Given: The view is loaded (this triggers viewDidLoad)
XCTAssertNotNil(ctrl.view)
XCTAssertNotNil(ctrl.searchTextField)
XCTAssertNotNil(ctrl.searchTextFieldObservable)
// And: results is not empty
ctrl.results = [ TypeaheadResult(value: "Something") ]
let tfObservable = ctrl.searchTextField.rx.text.subscribeOn(MainScheduler.instance)
//ctrl.searchTextField.rx.text.onNext("e")
ctrl.searchTextField.insertText("e")
//ctrl.searchTextField.text = "e"
do {
guard let result =
try tfObservable.toBlocking(timeout: 5.0).first() else {
return }
XCTAssertEqual(result, "e") // passes
XCTAssertNil(ctrl.results) // fails
} catch {
print(error)
}
}
Basically, I'm wondering how to manually/programmatically fire an event on searchTextFieldObservable (or, preferably, on the searchTextField) to trigger the code in the 2nd subscription marked "This is the part I want to test:".
The first step is to separate the logic from the effects. Once you do that, it will be easy to test your logic. In this case, the chain you want to test is:
self.searchTextField.rx.text
.throttle(0.5, scheduler: MainScheduler.instance)
.map { $0 ?? "" }
.filter { $0.count < 2 }
.subscribe(
onNext: { [weak self] _ in
self?.results = nil
}
)
.disposed(by: disposeBag)
The effects are only the source and the sink (another place to look out for effects is in any flatMaps in the chain.) So lets separate them out:
(I put this in an extension because I know how much most people hate free functions)
extension ObservableConvertibleType where E == String? {
func resetResults(scheduler: SchedulerType) -> Observable<Void> {
return asObservable()
.throttle(0.5, scheduler: scheduler)
.map { $0 ?? "" }
.filter { $0.count < 2 }
.map { _ in }
}
}
And the code in the view controller becomes:
self.searchTextField.rx.text
.resetResults(scheduler: MainScheduler.instance)
.subscribe(
onNext: { [weak self] in
self?.results = nil
}
)
.disposed(by: disposeBag)
Now, let's think about what we actually need to test here. For my part, I don't feel the need to test self?.results = nil or self.searchTextField.rx.text so the View controller can be ignored for testing.
So it's just a matter of testing the operator... There's a great article that recently came out: https://www.raywenderlich.com/7408-testing-your-rxswift-code However, frankly I don't see anything that needs testing here. I can trust that throttle, map and filter work as designed because they were tested in the RxSwift library and the closures passed in are so basic that I don't see any point in testing them either.
The problem is that self.ctrl.searchTextField.rx.text.onNext("e") won't trigger searchTextFieldObservable onNext subscription.
The subscription is also not triggered if you set the text value directly like this self.ctrl.searchTextField.text = "e".
The subscription will trigger (and your test should succeed) if you set the textField value like this: self.ctrl.searchTextField.insertText("e").
I think the reason for this is that UITextField.rx.text observes methods from UIKeyInput.
I prefer to keep UIViewControllers far away from my unit tests. Therefore, I suggest moving this logic to a view model.
As your bounty explanation details, basically what you are trying to do is mock the textField's text property, so that it fires events when you want it to. I would suggest replacing it with a mock value altogether. If you make textField.rx.text.bind(viewModel.query) the responsibility of the view controller, then you can focus on the view model for the unit test and manually alter the query variable as needed.
class ViewModel {
let query: Variable<String?> = Variable(nil)
let results: Variable<[TypeaheadResult]> = Variable([])
let disposeBag = DisposeBag()
init() {
query
.asObservable()
.flatMap { query in
return query.count >= 2 ? search(for: $0) : .just([])
}
.bind(results)
.disposed(by: disposeBag)
}
func search(query: String) -> Observable<[TypeaheadResult]> {
// ...
}
}
The test case:
class TypeaheadResultsViewControllerTests: XCTestCase {
func testManualChange() {
let viewModel = ViewModel()
viewModel.results.value = [/* .., .., .. */]
// this triggers the subscription, but does not trigger the search
viewModel.query.value = "1"
// assert the results list is empty
XCTAssertEqual(viewModel.results.value, [])
}
}
If you also want to test the connection between the textField and the view model, UI tests are a much better fit.
Note that this example omits:
Dependency injection of the network layer in the view model.
The binding of the view controller's textField value to query (i.e., textField.rx.text.asDriver().drive(viewModel.query)).
The observing of the results variable by the view controller (i.e., viewModel.results.asObservable.subscribe(/* ... */)).
There might be some typos in here, did not run it past the compiler.
If you look at the underlying implementation for rx.text, you'll see it relies on controlPropertyWithDefaultEvents which fires the following UIControl events: .allEditingEvents and .valueChanged.
Simply setting the text, it won't fire any events, so your observable is not triggered. You have to send an action explicitly:
textField.text = "Something"
textField.sendActions(for: .valueChanged) // or .allEditingEvents
If you are testing within a framework, sendActions won't work because the framework is missing the UIApplication. You can do this instead
extension UIControl {
func simulate(event: UIControl.Event) {
allTargets.forEach { target in
actions(forTarget: target, forControlEvent: event)?.forEach {
(target as NSObject).perform(Selector($0))
}
}
}
}
...
textField.text = "Something"
textField.simulate(event: .valueChanged) // or .allEditingEvents

Resources