Prevent disposal of PublishSubject (RxSwift) - ios

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 )

Related

Where to trigger Loading in Clean Architeture Swift

Where is the correct place I should put the code that would trigger a loading to display in my app.
It is correct to do is on view? since it is displaying something on screen, so it fits as a UI logic
class ViewController: UIViewController {
func fetchData() {
showLoading()
interactor?.fetchData()
}
}
or on interactor? since it's a business logic. something like, everytime a request is made, we should display a loading. View only knows how to construct a loading, not when to display it.
class Interactor {
func fetchData() {
presenter?.presentLoading(true)
worker?.fetchData() { (data) [weak self] in
presenter?.presentLoading(false)
self?.presenter?.presentData(data)
}
}
}
same question applies to MVVM and MVP.
it is totally up to you . i am showing loading using an Observable .
in my viewModel there is an enum called action :
enum action {
case success(count:Int)
case deleteSuccess
case loading
case error
}
and an Observable of action type :
var actionsObservable = PublishSubject<action>()
then , before fetching data i call onNext method of actionObservable(loading)
and subscribing to it in viewController :
vm.actionsObserver
.observeOn(MainScheduler.instance)
.subscribe(onNext: { (action) in
switch action {
case .success(let count):
if(count == 0){
self.noItemLabel.isHidden = false
}
else{
self.noItemLabel.isHidden = true
}
self.refreshControl.endRefreshing()
self.removeSpinner()
case .loading:
self.showSpinner(onView : self.view)
case .error:
self.removeSpinner()
}
}, onError: { (e) in
print(e)
}).disposed(by: disposeBag)
You can use the delegate or completion handler to the update the UI from view model.
class PaymentViewController: UIViewController {
// for UI update
func showLoading() {
self.showLoader()
}
func stopLoading() {
self.removeLoader()
}
}
protocol PaymentOptionsDelegate : AnyObject {
func showLoading()
func stopLoading()
}
class PaymentOptionsViewModel {
weak var delegate : PaymentOptionsDelegate?
func fetchData() {
delegate?.showLoading()
delegate?.stopLoading()
}
}

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, binding Action to UIButton stops producing events after view transitions

I'm trying to implement a MVVM login screen using RxSwift and encountered some difficulties.
My login screen (user, password, login button) transitions to a camera view screen, where the app checks for camera permissions, and if those are not approved, logs the user out and returns to the login screen.
I have a loginAction in my LoginViewModel that returns an Action<Void, LoginResult>, where LoginResult is a Result<Bool, Error> and my loginProvider service returns an Observable<LoginResult>:
struct LoginViewModel {
let sceneCoordinator: SceneCoordinatorType
let loginProvider: LoginProviderType
var usernameText = Variable<String>("")
var passwordText = Variable<String>("")
var isValid: Observable<Bool> {
return Observable.combineLatest(usernameText.asObservable(), passwordText.asObservable()) { username, password in
username.count > 0 && password.count > 0
}
}
init(loginProvider: LoginProvider, coordinator: SceneCoordinatorType) {
self.loginProvider = loginProvider
self.sceneCoordinator = coordinator
}
lazy var loginAction: Action<Void, LoginResult> = { (coordinator: SceneCoordinatorType, service: LoginProviderType, username: String, password: String) in
return Action<Void, LoginResult>(enabledIf: self.isValid) { _ in
return service.login(username: username, password: password)
.observeOn(MainScheduler.instance)
.do(onNext: { result in
guard let loggedIn = result.value else { return }
if loggedIn {
let cameraViewModel = CameraViewModel(coordinator: coordinator)
coordinator.transition(to: Scene.camera(cameraViewModel), type: .modal)
}
}(self.sceneCoordinator, self.loginProvider, self.companyText.value, self.usernameText.value, self.passwordText.value)
}
Everything works fine, valid input logs in successfully (loginProvider sends the request the my server, get the response and handles all additional steps accordingly).
In case that the user doesn't grant camera permissions, I have a Observable for that in my CameraViewModel which I bind to my CameraViewController, subscribe to and in case needed, log the user out and pop the view back to the login screen using CocoaAction that pops the current view (using a scene coordinator class).
Problem is, when I try to log in again after transitioning back to the Login screen, the subscription for the elements emitted by the loginAction doesn't receive any elements.
Here's the code for the LoginViewController:
class LoginViewController: UIViewController, BindableType {
var viewModel: LoginViewModel!
private let disposeBag = DisposeBag()
private var loginAction: Action<Void, LoginResult>!
#IBOutlet weak var usernameTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var loginButton: UIButton!
#IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
private var usernameObservable: Observable<String> {
return usernameTextField.rx.text
.throttle(0.5, scheduler: MainScheduler.instance)
.map() { text in
return text ?? ""
}
}
private var passwordObservable: Observable<String> {
return passwordTextField.rx.text
.throttle(0.5, scheduler: MainScheduler.instance)
.map() { text in
return text ?? ""
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
func bindViewModel() {
usernameObservable
.bind(to: viewModel.usernameText)
.disposed(by: disposeBag)
passwordObservable
.debug()
.bind(to: viewModel.passwordText)
.disposed(by: disposeBag)
loginAction = viewModel.loginAction
loginAction.elements
.subscribe(onNext: { [weak self] result in
self?.loadingIndicator.stopAnimating()
var message = ""
switch result {
case .failure(.unknownError):
message = "unknown error"
case .failure(.wrongCredentials):
message = "wrong credentials"
case .failure(.serviceUnavailable):
message = "service unavailable"
case let .success(loggedIn):
return
}
self?.errorMessage(message: message)
}).disposed(by: disposeBag)
loginButton.rx.tap
.subscribe(onNext: { [unowned self] in
self.loadingIndicator.startAnimating()
self.loginAction.execute(Void())
})
.disposed(by: disposeBag)
viewModel.isValid
.bind(to: loginButton.rx.isEnabled)
.disposed(by: disposeBag)
}
I can see that tapping the login button produces the tap event, however the Action itself stops being called. Any idea what I'm missing?
I am not sure how ScreenCoordinator works, but I would start with moving creation of usernameObservable/passwordObservable to viewDidLoad() because the issue seems to be lifecycle-related. Also, you can add a few more .debug() calls (with parameter to be able to distinguish between them in the logs).
Found the issue: Apparently the action didn't reach a complete state because one on the underlying services returned an observer that was subscribed in the login view controller and not disposed when returning to it from the camera view controller.
I've added a clearing method in that service which creates a new DisposeBag when returning to the login view, which allows the action to complete and released for reuse.

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

How to show/hide the progressHUD, with MVVM and RxSwift in swift

I'm beginning with MVVM in order to well separate logic code from the view. But I have some concern about where to put the progressHUD related code when tapping a button that makes a request.
Before, I used to do that:
//Before
#IBAction func startRequestTapped() {
SVProgressHUD.show()
self.apiClient.requestObservable().subscribe(onError: { (error) in
SVProgressHUD.hide()
}, onCompleted: {
SVProgressHUD.hide()
})
}
But when I use mvvm, I do like that:
//In the viewModel
public var validateButtonDidTap = PublishSubject<Void>()
init() {
validateButtonDidTap.flatMap { (_)
return self.apiClient.requestObservable()
}
}
// In the viewController
viewDidLoad() {
let tap = self.validateButton.rx.tap
tap.bindTo(self.viewModel.validateButtonDidTap)
}
And amongst that, I don't know where to put the the ProgressHUD hide or show.
Mark answer is right, but I am going to guide you step by step.
Let's supose you're going to try signing in.
Copy ActivityIndicator.swift file in your project.
In the viewModel:
//MARK: - Properties
/// The http client
private let apiClient: YourApiClient
/// Clousure when button is tapped
var didTappedButton: () -> Void = {}
/// The user
var user: Observable<User>
/// Is signing process in progress
let signingIn: Observable<Bool> = ActivityIndicator().asObservable()
//MARK: - Initialization
init(client: YourApiClient) {
self.client = client
self.didTappedButton = { [weak self] in
self.user = self.apiClient
.yourSignInRequest()
.trackActivity(self.signingIn)
.observeOn(MainScheduler.instance)
}
}
Create an extension of SVProgressHUD: (I don't know SVProgressHUD library, but it would be something like this. Please fix it if needed)
extension Reactive where Base: SVProgressHUD {
/// Bindable sink for `show()`, `hide()` methods.
public static var isAnimating: UIBindingObserver<Base, Bool> {
return UIBindingObserver(UIElement: self.base) { progressHUD, isVisible in
if isVisible {
progressHUD.show() // or other show methods
} else {
progressHUD.dismiss() // or other hide methods
}
}
}
}
In your viewController:
#IBAction func startRequestTapped() {
viewModel.didTappedButton()
}
override func viewDidLoad() {
// ...
viewModel.signingIn
.bindTo(SVProgressHUD.rx.isAnimating)
.addDisposableTo(disposeBag)
}
Accepted answer updated to Swift 4, RxSwift 4.0.0 and SVProgressHUD 2.2.2:
3- Extension:
extension Reactive where Base: SVProgressHUD {
public static var isAnimating: Binder<Bool> {
return Binder(UIApplication.shared) {progressHUD, isVisible in
if isVisible {
SVProgressHUD.show()
} else {
SVProgressHUD.dismiss()
}
}
}
}
4- Controller:
viewModel.signingIn.asObservable().bind(to: SVProgressHUD.rx.isAnimating).disposed(by: disposeBag)
You could try using an ActivityIndicator.
See the example here:
https://github.com/RxSwiftCommunity/RxSwiftUtilities

Resources