RxSwift Tap Event not triggered - ios

I'm implementing the mvvm using RxSwif.
Here is what happens:
Validate Fields (write 7 characters in both textFields).
Tap the login button.
API is called which is fine.
Tap the button again.
API is not called.
I noticed that the "validObservable" changes every time that I write something in textFields which is fine.
ViewController:
var viewModel: LoginViewModelType!
let disposeBag = DisposeBag()
#IBOutlet weak var dniTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var logInButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
logInButton.rx
.tap
.bind(to: viewModel.inputs.logInButtonDidTap)
.disposed(by: disposeBag)
dniTextField.rx
.text
.bind(to: viewModel.inputs.dniChanged)
.disposed(by: disposeBag)
passwordTextField.rx
.text
.bind(to: viewModel.inputs.passwordChanged)
.disposed(by: disposeBag)
/*viewModel.outputs.isLoginButtonEnabled
.drive(onNext: { [weak self] isEnabled in
guard let `self` = self else { return }
self.logInButton.isEnabled = isEnabled
})
.disposed(by: disposeBag)
*/
viewModel.outputs.logIn
.drive(onNext: { [weak self] user in
guard let `self` = self else { return }
self.performSegue(withIdentifier: "showMainController", sender: user)
})
.disposed(by: disposeBag)
}
override func awakeFromNib() {
super.awakeFromNib()
viewModel = LoginViewModel()
}
ViewModel:
var inputs: LoginViewModelInputs { return self }
var outputs: LoginViewModelOutputs { return self }
// ---------------------
// MARK: - Inputs
// ---------------------
var dniChanged: BehaviorRelay<String?>
var passwordChanged: BehaviorRelay<String?>
var logInButtonDidTap: PublishSubject<Void> = PublishSubject<Void>()
// ---------------------
// MARK: - Outpuds
// ---------------------
var logIn: Driver<User>
var isLoginButtonEnabled: Driver<Bool>
private let disposeBag = DisposeBag()
public init () {
dniChanged = BehaviorRelay<String?>(value: "")
passwordChanged = BehaviorRelay<String?>(value: "")
let dniObservable = dniChanged.asDriver().filterNil().asObservable()
let passwordObservable = passwordChanged.asDriver().filterNil().asObservable()
let dniValidation = dniObservable.map { $0.count > 4 }
let passwordValidation = passwordObservable.map { $0.count > 3 }
let validObservable = Observable.combineLatest(dniValidation, passwordValidation) { return $0 && $1 }.filter { $0 }
isLoginButtonEnabled = validObservable.asDriver(onErrorDriveWith: .empty())
let loginSuccessObservable = Observable.combineLatest(dniObservable, passwordObservable, validObservable) { (dni, password, valid) -> LogInRequest in
return LogInRequest(dni: dni, password: password)
}
logIn = logInButtonDidTap.withLatestFrom(loginSuccessObservable).flatMapLatest({ request -> Observable<User> in
return API.shared.post(endpoint: EndPoints.Authorize, type: User.self, body: request)
}).asDriver(onErrorDriveWith: .empty())
}

I'd say that .empty() is causing the observable to complete, and the subscription to be disposed of as a consequence.
I'd put some .debug() instructions to make sure what gets disposed and when.

I got this problem because the button size is zero

Related

How to implement firebase auth into MVVM-C RxSwift

I'm trying to implement a MVVM-C rx swift app.
I have a VM for my sign up view controller with the username and password as behaviour subjects. I also have a firebase handler injected into the VM. What is the best way to pass the sign up result back to the VC?
my VM code:
class CreateVM {
let firebase: FirebaseHandler
let email: String
var password = BehaviorSubject<String>(value: "")
var confirmPassword = BehaviorSubject<String>(value: "")
var shouldHideButton: Observable<Bool> {
return Observable.combineLatest(password.asObservable(), confirmPassword.asObservable()) { pass, confPass in
!(pass.count >= 5 && pass == confPass)
}
}
init(firebase: FirebaseHandler, email: String) {
self.firebase = firebase
self.email = email
}
func submit() {
let pass = try! password.value()
firebase.createWithEmail(email: email, password: pass) { (result) in
switch result {
case .success(let uid):
print(uid, "created")
//handle successful creation
case .failure(let err):
print("failed with error:", err)
//handler error
}
}
}
}
My VC code:
class CreateVC: UIViewController, Storyboarded {
#IBOutlet weak var createButton: Rounded!
#IBOutlet weak var passwordEntry: UITextField!
#IBOutlet weak var confirmPasswordEntry: UITextField!
weak var coordinator: AuthCoordinator?
var displayName: String!
var viewModel: CreateVM!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindUI()
}
func bindUI() {
passwordEntry.rx.text.orEmpty.bind(to: viewModel.password).disposed(by: disposeBag)
confirmPasswordEntry.rx.text.orEmpty.bind(to: viewModel.confirmPassword).disposed(by: disposeBag)
viewModel.shouldHideButton.bind(to: createButton.rx.isHidden).disposed(by: disposeBag)
createButton.rx.tap.bind { [unowned self] _ in
self.viewModel.submit()
}.disposed(by: disposeBag)
}
}
I do my view models as a function, and a lot depends on exactly what you want to do with the result, but here is some sample code that might help you:
struct CreateInput {
let password: Observable<String>
let confirm: Observable<String>
let submit: Observable<Void>
}
struct CreateOutput {
let displayName: String
let shouldHideButton: Observable<Bool>
let signUpResult: Observable<Result<Int, Error>>
}
func createVM(firebase: FirebaseHandler, email: String) -> (CreateInput) -> CreateOutput {
return { input in
let shouldHideButton = Observable.combineLatest(input.password, input.confirm) { $0.count < 5 || $0 != $1 }
let credentials = Observable.combineLatest(Observable.just(email), input.password) { (email: $0, password: $1) }
let signUpResult = input.submit
.withLatestFrom(credentials)
.flatMapLatest {
firebase.create(email: $0.email, password: $0.password)
}
return CreateOutput(
displayName: email,
shouldHideButton: shouldHideButton,
signUpResult: signUpResult
)
}
}
extension FirebaseHandler {
func create(email: String, password: String) -> Observable<Result<Int, Error>> {
Observable.create { observer in
self.createWithEmail(email: email, password: password) { (result) in
observer.onNext(result)
observer.onCompleted()
}
return Disposables.create()
}
}
}
final class CreateViewController: UIViewController {
#IBOutlet weak var displayNameLabel: UILabel!
#IBOutlet weak var createButton: UIButton!
#IBOutlet weak var passwordEntry: UITextField!
#IBOutlet weak var confirmPasswordEntry: UITextField!
var bindUI: (CreateInput) -> CreateOutput = { _ in fatalError() } // assign `createVM(firebase: myFirebaseHandler, email: "myEmail")` to this before it loads.
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let input = CreateInput(
password: passwordEntry.rx.text.orEmpty.asObservable(),
confirm: confirmPasswordEntry.rx.text.orEmpty.asObservable(),
submit: createButton.rx.tap.asObservable()
)
let output = bindUI(input)
displayNameLabel.text = output.displayName
output.shouldHideButton
.bind(to: createButton.rx.isHidden)
.disposed(by: disposeBag)
output.signUpResult
.bind { result in
switch result {
case .success(let uid):
print("uid:", uid)
case .failure(let error):
print("error:", error.localizedDescription)
}
}
.disposed(by: disposeBag)
}
}
If higher order functions make you nervous, then you could wrap it in a type:
struct CreateVM {
struct Input {
let password: Observable<String>
let confirm: Observable<String>
let submit: Observable<Void>
}
struct Output {
let displayName: String
let shouldHideButton: Observable<Bool>
let signUpResult: Observable<Result<Int, Error>>
}
let firebase: FirebaseHandler
let email: String
func bind(_ input: Input) -> Output {
let shouldHideButton = Observable.combineLatest(input.password, input.confirm) { $0.count < 5 || $0 != $1 }
let credentials = Observable.combineLatest(Observable.just(email), input.password) { (email: $0, password: $1) }
let signUpResult = input.submit
.withLatestFrom(credentials)
.flatMapLatest { [unowned firebase] in
firebase.create(email: $0.email, password: $0.password)
}
return Output(
displayName: email,
shouldHideButton: shouldHideButton,
signUpResult: signUpResult
)
}
}
Then your view controller would have a property:
var viewModel: CreateVM!
and build the output with:
let output = viewModel.bind(input)

RxSwift MVVM Validate Form on Button Submit then Make API Request

I'm new to RxSwift and attempting to do as the title states with an MVVM input output approach.
I can't figure out the best approach to do the following.
Validate the phoneNumberTextField values when submitButton is tapped
Stop the Alamofire Request from being submitted if phoneNumberTextField is invalid and throw a client side error
Show a display indicator when loading takes place. This is the least important right now
A few things to note.
There is nothing tracking the phone number text at the moment
I do not want to disable the submit button until the form is valid as seen in examples all over.
Here is my view controller
import UIKit
import RxSwift
import RxCocoa
class SplashViewController: BaseViewController {
// MARK: – View Variables
#IBOutlet weak var phoneNumberTextField: UITextField!
#IBOutlet weak var phoneNumberBackgroundView: UIView!
#IBOutlet weak var submitButton: BaseButton!
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var separatorView: UIView!
#IBOutlet weak var countryCodeButton: UIButton!
#IBOutlet weak var parentVerticalStackView: UIStackView!
// MARK: – View Model & RxSwift Setup
private let disposeBag = DisposeBag()
private let viewModel: SplashMVVM = SplashMVVM()
// MARK: – View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// RxSwift handling
setupViewModelBinding()
setupCallbacks()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: true)
}
// MARK: – RxSwift Handling
private func setupViewModelBinding() {
submitButton.rx.controlEvent(.touchUpInside)
.bind(to: viewModel.input.submit)
.disposed(by: disposeBag)
}
private func setupCallbacks() {
viewModel.output.success.asObservable()
.filter { $0 != nil }
.observeOn(MainScheduler())
.subscribe({ _ in
self.pushVerifyPhoneNumberViewController()
})
.disposed(by: disposeBag)
viewModel.output.error.asObservable()
.filter { $0 != nil }
.observeOn(MainScheduler())
.subscribe({ _ in
SwiftMessages.show(.error, message: "There was an error. Please try again.")
})
.disposed(by: disposeBag)
}
// MARK: – Navigation
func pushVerifyPhoneNumberViewController() {
let viewController = VerifyPhoneNumberViewController.fromStoryboard("Authentication")
self.navigationController?.pushViewController(viewController, animated: true)
}
}
Here is my view model.
import Foundation
import RxSwift
import RxCocoa
import Alamofire
final class SplashMVVM: InputOutputModelType {
let input: SplashMVVM.Input
let output: SplashMVVM.Output
var submitSubject = PublishSubject<Void>()
struct Input {
let submit: AnyObserver<Void>
}
struct Output {
let success: Observable<VerifyMobilePhone?>
let error: Observable<Error?>
}
init() {
input = Input(submit: submitSubject.asObserver())
let request = Alamofire.request(VerifyMobileRouter.post("+16306996540")).responseDecodableRx(VerifyMobilePhone.self)
let requestData = submitSubject.flatMapLatest {
request
}
let success = requestData.map { $0.value ?? nil }
let error = requestData.map { $0.error ?? nil }
output = Output(
success: success,
error: error
)
}
}
Here is what I came up with.
final class SplashMVVM: InputOutputModelType {
let input: SplashMVVM.Input
let output: SplashMVVM.Output
var submitSubject = PublishSubject<Void>()
var phoneNumberSubject = PublishSubject<String>()
struct Input {
let phoneNumber: AnyObserver<String>
let submit: AnyObserver<Void>
}
struct Output {
let validationError: Observable<String>
let success: Observable<VerifyMobilePhone>
let error: Observable<Error>
}
init() {
input = Input(phoneNumber: phoneNumberSubject.asObserver(), submit: submitSubject.asObserver())
let request = submitSubject.asObservable().withLatestFrom(phoneNumberSubject.asObservable()).filter {
$0.isValidPhoneNumber(region: "US")
}.flatMap { number in
Alamofire.request(VerifyMobileRouter.post(number)).responseDecodableRx(VerifyMobilePhone.self)
}.share()
let validationError = submitSubject.asObservable().withLatestFrom(phoneNumberSubject.asObservable()).filter {
!$0.isValidPhoneNumber(region: "US")
}.map { _ in
"This phone number is invalid"
}
let success = request.filter { $0.isSuccess }.map { $0.value! }
let error = request.filter { $0.isFailure }.map { $0.error! }
output = Output(
validationError: validationError,
success: success,
error: error
)
}
}
View controller changes…
private func setupViewModelBinding() {
submitButton.rx.controlEvent(.touchUpInside).bind(to: viewModel.input.submit).disposed(by: disposeBag)
phoneNumberTextField.rx.text.orEmpty.bind(to: viewModel.input.phoneNumber).disposed(by: disposeBag)
}
private func setupCallbacks() {
viewModel.output.validationError.bind { string in
SwiftMessages.show(.error, message: string)
}.disposed(by: disposeBag)
viewModel.output.success.bind { verifyMobilePhone in
self.pushVerifyPhoneNumberViewController()
}.disposed(by: disposeBag)
viewModel.output.error.bind { error in
SwiftMessages.show(.error, message: "There was an error. Please try again.")
}.disposed(by: disposeBag)
}
You are close, you're just missing the phone number text as input into your view model.
struct SplashInput {
let phoneNumber: Observable<String>
let submit: Observable<Void>
}
struct SplashOutput {
let invalidInput: Observable<Void>
let success: Observable<VerifyMobilePhone>
let error: Observable<Error>
}
extension SplashOutput {
init(_ input: SplashInput) {
let request: Observable<Event<VerifyMobilePhone>> = input.submit.withLatestFrom(input.phoneNumber)
.filter { $0.isValidPhoneNumber }
.flatMap { number in
Alamofire.request(VerifyMobileRouter.post(number)).responseDecodableRx(VerifyMobilePhone.self)
.materialize()
}
.share()
invalidInput = input.submit.withLatestFrom(input.phoneNumber)
.filter { $0.isValidPhoneNumber == false }
success = request
.map { $0.element }
.filter { $0 != nil }
.map { $0! }
error = request
.map { $0.error }
.filter { $0 != nil }
.map { $0! }
}
}
Your SplashViewController would have:
override func viewDidLoad() {
super.viewDidLoad()
let input = SplashInput(
phoneNumber: phoneNumberTextField.rx.text.orEmpty.asObservable(),
submit: submitButton.rx.tap.asObservable()
)
let viewModel = SplashOutput(input)
viewModel.invalidInput
.bind {
SwiftMessages.show(.invalid, message: "You entered an invalid number. Please try again.")
}
.disposed(by: bag)
viewModel.success
.bind { [unowned self] verifyMobilePhone in
self.pushVerifyPhoneNumberViewController(verifyMobilePhone)
}
.disposed(by: bag)
viewModel.error
.bind { error in
SwiftMessages.show(.error(error), message: "There was an error. Please try again.")
}
}
(I took some liberties with what you already have written, but the above should make sense.)

How to dispose RxSwift observable in viewmodel

I am learning RxSwift and I have tried a basic login UI using it. My implementation is as follows.
LoginViewController:
fileprivate let loginViewModel = LoginViewModel()
fileprivate var textFieldArray: [UITextField]!
override func viewDidLoad() {
super.viewDidLoad()
textFieldArray = [textFieldUserName, textFieldPassword, textFieldConfirmPassword]
textFieldUserName.delegate = self
textFieldPassword.delegate = self
textFieldConfirmPassword.delegate = self
loginViewModel.areValidFields.subscribe(
onNext: { [weak self] validArray in
for i in 0..<validArray.count {
if validArray[i] {
self?.showValidUI(index: i)
} else {
self?.showInValidUI(index: i)
}
}
},
onCompleted: {
print("### COMPLETED ###")
},
onDisposed: {
print("### DISPOSED ###")
}).disposed(by: loginViewModel.bag)
}
func showValidUI(index: Int) {
textFieldArray[index].layer.borderColor = UIColor.clear.cgColor
}
func showInValidUI(index: Int) {
textFieldArray[index].layer.borderColor = UIColor.red.cgColor
textFieldArray[index].layer.borderWidth = 2.0
}
extension LoginViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let inputText = (textField.text! as NSString).replacingCharacters(in: range, with: string)
switch textField {
case textFieldUserName:
loginViewModel.updateUserName(text: inputText)
case textFieldPassword:
loginViewModel.updatePassword(text: inputText)
case textFieldConfirmPassword:
loginViewModel.updateConfirmedPassword(text: inputText)
default:
return false
}
return true
}
}
LoginViewModel:
class LoginViewModel {
private var username: String!
private var password: String!
private var confirmedPassword: String!
fileprivate let combinedSubject = PublishSubject<[Bool]>()
let bag = DisposeBag()
var areValidFields: Observable<[Bool]> {
return combinedSubject.asObservable()
}
init() {
self.username = ""
self.password = ""
self.confirmedPassword = ""
}
/*deinit {
combinedSubject.onCompleted()
}*/
func updateUserName(text: String) {
username = text
if username.count > 6 {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([false, true, true])
}
}
func updatePassword(text: String) {
password = text
if password.count > 6 {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([true, false, true])
}
}
func updateConfirmedPassword(text: String) {
confirmedPassword = text
if confirmedPassword == password {
combinedSubject.onNext([true, true, true])
} else {
combinedSubject.onNext([true, true, false])
}
}
}
With this code, the disposed message gets printed when i move back the navigation stack.
However, if I move forward, the disposed message is not printed. What is the proper way to dispose the observable?
When you move forward, the view controller is not removed from the stack. It remains so that when the user taps the back button, it is ready and still in the same state as the last time the user saw it. That is why nothing is disposed.
Also, since you said you are still learning Rx, what you have is not anywhere near best practices. I would expect to see something more like this:
class LoginViewModel {
let areValidFields: Observable<[Bool]>
init(username: Observable<String>, password: Observable<String>, confirm: Observable<String>) {
let usernameValid = username.map { $0.count > 6 }
let passValid = password.map { $0.count > 6 }
let confirmValid = Observable.combineLatest(password, confirm)
.map { $0 == $1 }
areValidFields = Observable.combineLatest([usernameValid, passValid, confirmValid])
}
}
In your view model, prefer to accept inputs in the init function. If you can't do that, for e.g. if some of the inputs don't exist yet, then use a Subject property and bind to it. But in either case, your view model should basically consist only of an init function and some properties for output. The DisposeBag does not go in the view model.
Your view controller only needs to create a view model and connect to it:
class LoginViewController: UIViewController {
#IBOutlet weak var textFieldUserName: UITextField!
#IBOutlet weak var textFieldPassword: UITextField!
#IBOutlet weak var textFieldConfirmPassword: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = LoginViewModel(
username: textFieldUserName.rx.text.orEmpty.asObservable(),
password: textFieldPassword.rx.text.orEmpty.asObservable(),
confirm: textFieldConfirmPassword.rx.text.orEmpty.asObservable()
)
let textFieldArray = [textFieldUserName!, textFieldPassword!, textFieldConfirmPassword!]
viewModel.areValidFields.subscribe(
onNext: { validArray in
for (field, valid) in zip(textFieldArray, validArray) {
if valid {
field.layer.borderColor = UIColor.clear.cgColor
}
else {
field.layer.borderColor = UIColor.red.cgColor
field.layer.borderWidth = 2.0
}
}
})
.disposed(by: bag)
}
private let bag = DisposeBag()
}
Notice that all of the code ends up in the viewDidLoad function. That's the ideal so you don't have to deal with [weak self]. In this particular case, I would likely put the onNext closure in a curried global function, in which case it would look like this:
class LoginViewController: UIViewController {
#IBOutlet weak var textFieldUserName: UITextField!
#IBOutlet weak var textFieldPassword: UITextField!
#IBOutlet weak var textFieldConfirmPassword: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = LoginViewModel(
username: textFieldUserName.rx.text.orEmpty.asObservable(),
password: textFieldPassword.rx.text.orEmpty.asObservable(),
confirm: textFieldConfirmPassword.rx.text.orEmpty.asObservable()
)
let textFieldArray = [textFieldUserName!, textFieldPassword!, textFieldConfirmPassword!]
viewModel.areValidFields.subscribe(
onNext:update(fields: textFieldArray))
.disposed(by: bag)
}
private let bag = DisposeBag()
}
func update(fields: [UITextField]) -> ([Bool]) -> Void {
return { validArray in
for (field, valid) in zip(fields, validArray) {
if valid {
field.layer.borderColor = UIColor.clear.cgColor
}
else {
field.layer.borderColor = UIColor.red.cgColor
field.layer.borderWidth = 2.0
}
}
}
}
Notice here that the update(fields:) function is not in the class. That way we aren't capturing self and so don't have to worry about weak self. Also, this update function may very well be useful for other form inputs in the app.
You have added disposable in to the dispose bag of LoginViewModel object, which gets released when LoginViewController object gets released.
This means the disposable returned by LoginViewModel observable won't be disposed until LoginViewController gets released or you receive completed or error on areValidFields Observable.
This is in sync with the accepted behaviour in most of the observable cases.
But, in case if you want to dispose the observable when LoginViewController moves out of screen, you can manually dispose:
var areValidFieldsDisposbale:Disposable?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
areValidFieldsDisposbale = loginViewModel.areValidFields.subscribe(
onNext: { [weak self] validArray in
for i in 0..<validArray.count {
if validArray[i] {
self?.showValidUI(index: i)
} else {
self?.showInValidUI(index: i)
}
}
},
onCompleted: {
print("### COMPLETED ###")
},
onDisposed: {
print("### DISPOSED ###")
})
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
areValidFieldsDisposbale?.dispose()
}

RxSwift, MVVM, Alamofire/Moya. Update ticker using interval

Currently learning RxSwift (Rx in general). I want to update a UILabel regularly with the latest ticker price.
How can .interval and the updateTicker function every period and then update the UILabel accordingly.
ViewController.swift
class ViewController: UIViewController {
private let disposeBag = DisposeBag()
// Dependencies
private var viewModel = ViewModel()
// Outlets
#IBOutlet var tickerLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setupViewModel()
}
func setupViewModel() {
self.viewModel.ticker.asObservable()
.bind(to: self.tickerLabel.rx.text)
.addDisposableTo(self.disposeBag)
}
}
ViewModel.swift
struct ViewModel {
private let disposeBag = DisposeBag()
let provider = RxMoyaProvider<StockAPI>()
var ticker = Variable<String>("")
init() {
startTimer()
// ???
}
func startTimer() -> Observable<Int> {
return Observable<Int>.interval(5, scheduler: MainScheduler.instance)
}
func updateTicker() {
_ = self.provider.request(.ticker(symbol: "AAPL")).subscribe { (event) in
switch event {
case .next(let response):
print(response)
// do something with the data
case .error(let error):
// handle the error
print(error)
break
default:
break
}
}
}
Maybe something like this
var ticker = Observable.just("")
func startTimer() {
self.ticker = Observable<Int>.interval(1, scheduler: MainScheduler.instance)
.map({ _ in self.updateTicker() })
}
The func updateTicker will need return a String.

RxSwift and isSelected property on UIButton

I have three buttons and I want them to be selected only one at a time:
and:
etc...
My approach is this:
class MyController: UIViewController {
#IBOutlet var buttonOne: UIButton!
#IBOutlet var buttonTwo: UIButton!
#IBOutlet var buttonThree: UIButton!
var buttonOneIsSelected = Variable(true)
var buttonTwoIsSelected = Variable(false)
var buttonThreeIsSelected = Variable(false)
override func viewDidLoad() {
super.viewDidLoad()
buttonOne.isSelected = true
buttonOneIsSelected.asDriver()
.drive(buttonOne.rx.isSelected)
.disposed(by: disposeBag)
buttonTwoIsSelected.asDriver()
.drive(buttonTwo.rx.isSelected)
.disposed(by: disposeBag)
buttonThreeIsSelected.asDriver()
.drive(buttonThree.rx.isSelected)
.disposed(by: disposeBag)
buttonOne.rx.tap.asObservable().map { (_) -> Bool in
return !self.buttonOne.isSelected
}
.do(onNext: { (isSelected) in
self.buttonTwoIsSelected.value = !isSelected
self.buttonThreeIsSelected.value = !isSelected
})
.bindTo(buttonOne.rx.isSelected)
.disposed(by: disposeBag)
buttonTwo.rx.tap.asObservable().map { (_) -> Bool in
return !self.buttonTwo.isSelected
}
.do(onNext: { (isSelected) in
self.buttonOneIsSelected.value = !isSelected
self.buttonThreeIsSelected.value = !isSelected
})
.bindTo(buttonTwo.rx.isSelected)
.disposed(by: disposeBag)
buttonThree.rx.tap.asObservable().map { (_) -> Bool in
return !self.buttonThree.isSelected
}
.do(onNext: { (isSelected) in
self.buttonOneIsSelected.value = !isSelected
self.buttonTwoIsSelected.value = !isSelected
})
.bindTo(buttonThree.rx.isSelected)
.disposed(by: disposeBag)
}
Is there a better approach? It works, but is there a better 'reactive' way to do it by using RxSwift?
Subject and by extension Variable are most of the time only useful when bridging from imperative to reactive world. Here, you could do without them.
.do(onNext:) is also a way to perform side effect, something you usually don't want in your reactive code.
// force unwrap to avoid having to deal with optionals later on
let buttons = [button1, button2, button3].map { $0! }
// create an observable that will emit the last tapped button (which is
// the one we want selected)
let selectedButton = Observable.from(
buttons.map { button in button.rx.tap.map { button } }
).merge()
// for each button, create a subscription that will set its `isSelected`
// state on or off if it is the one emmited by selectedButton
buttons.reduce(Disposables.create()) { disposable, button in
let subscription = selectedButton.map { $0 == button }
.bindTo(button.rx.isSelected)
// combine two disposable together so that we can simply call
// .dispose() and the result of reduce if we want to stop all
// subscriptions
return Disposables.create(disposable, subscription)
}
.addDisposableTo(disposeBag)

Resources