I need to have var isOn: Variable<Bool> that is computed from 2 other Variables. How can I subscribe to value changes of the other Variables while being able to read latest value from my isOn Variable as well?
Below is the bad solution using 2 variables of type Observable and Variable. But I am looking for the correct solution, basically to merge my isOn and isOnVariable into single one.
let from = Variable<Date?>(nil)
let to = Variable<Date?>(nil)
let isOnVariable = Variable<Bool>(false)
var isOn: Observable<Bool> {
return Observable.combineLatest(from.asObservable(), to.asObservable()) { [weak self] to, from in
switch (from, to) {
case (.none, .none):
self?.isOnVariable.value = false
return false
default:
self?.isOnVariable.value = true
return true
}
}
}
let from = Variable<Date?>(nil)
let to = Variable<Date?>(nil)
let isOnVariable = Variable<Bool>(false)
Observable.merge(from.asObservable(), to.asObservable())
.map { [weak self] (_) -> Void in
guard let `self` = self else {
return
}
switch (self.from.value, self.to.value) {
case (.none, .none):
self.isOnVariable.value = false
default:
self.isOnVariable.value = true
}
}
.subscribe()
.disposed(by: disposeBag)
Now you can subscribe to isOnVariable an listen for value changes, and in any moment you can access values from the from and to variable. I'm not sure what you are trying to do, so you can just change logics in the .map but you got the idea.
If you are interested in why I'm using weak self this is a good reading http://adamborek.com/memory-managment-rxswift/
While working with #markoaras's answer, there is another option to use combineLatest and bind it to isOn Variable. It follows the same principle.
let isFromOpen = Variable<Bool>(false)
let isToOpen = Variable<Bool>(false)
let isOn = Variable<Bool>(false)
Observable.combineLatest(from.asObservable(), to.asObservable()).map{ (from, to) -> Bool in
return from != nil || to != nil
}.bind(to: isOn).disposed(by: bag)
Related
I would like to know the best possible way to handle the following situation, I have tried an approach as it will be described but I have encountered an issue of events calling each other repeatedly in a circular way hence it causes stackoverflow 😂
I have 4 observables as follows: -
let agreeToPrivacyPolicyObservable = BehaviorRelay<Bool>(value: false)
let agreeToTermsObservable = BehaviorRelay<Bool>(value: false)
let agreeToMarketingEmailObservable = BehaviorRelay<Bool>(value: false)
let agreeToAllOptionsObservable = BehaviorRelay<Bool>(value: false)
Goal:
Sync agree to all button with individual options. ie if agree to all is true/checked then force other options to be checked as well and vice-versa. Additionally if the previous state of all items were checked and either of them emit unchecked then remove a checkmark on Agree to all button.
The image below visualizes my goal above.
What I have tried:
Observable.combineLatest(
agreeToPrivacyPolicyObservable,
agreeToTermsObservable,
agreeToMarketingEmailObservable,
agreeToAllOptionsObservable
, resultSelector:{(termsChecked,privacyChecked,marketingChecked,agreeToAllChecked) in
switch (termsChecked,privacyChecked,marketingChecked,agreeToAllChecked) {
case (true, true, true,true):
//All boxes are checked nothing to change.
break
case (false,false,false,false):
//All boxes are unchecked nothing to change.
break
case (true,true,true,false):
// I omitted the `triggeredByAgreeToAll` flag implementation details for clarity
if triggeredByAgreeToAll {
updateIndividualObservables(checked: false)
}else {
agreeToAllOptionsObservable.accept(true)
}
case (false,false,false,true):
if triggeredByAgreeToAll {
updateIndividualObservables(checked: true)
}else {
agreeToAllOptionsObservable.accept(false)
}
default:
if triggeredByAgreeToAll && agreeToAllChecked {
updateIndividualObservables(checked: true)
}else if triggeredByAgreeToAll && agreeToAllChecked == false {
updateIndividualObservables(checked: false)
} else if (termsChecked == false || privacyChecked == false || marketingChecked == false ) {
agreeToAllOptionsObservable.accept(false)
}
}
}
})
.observeOn(MainScheduler.instance)
.subscribe()
.disposed(by: rx.disposeBag)
// Helper function
func updateIndividualObservables(checked: Bool) {
agreeToPrivacyPolicyObservable.accept(checked)
agreeToTermsObservable.accept(checked)
agreeToMarketingEmailObservable.accept(checked)
}
Explanation:
My attempt gives me Reentracy anomaly was detected error , which according to my observations is caused by events being triggered repeatedly. This seems to occurs in the default switch case (on my solution above). I think this solution is not good as I have to check which event triggered the function execution.
Is there any better approach or is it possible to refactor this solution into something easily manageable? Btw Feel free to ignore my implementation and suggest a different better approach if any. Thanks!
UPDATES (WORKING SOLUTION)
I successfully implemented a working solution by using #Rugmangathan idea (Found on the accepted answer). So I leave my solution here to help anyone in the future facing the same issue.
Here is the working solution: -
import Foundation
import RxSwift
import RxRelay
/// This does all the magic of selecting checkboxes.
/// It is shared across any view which uses the license Agreement component.
class LicenseAgreemmentState {
static let shared = LicenseAgreemmentState()
let terms = BehaviorRelay<Bool>(value: false)
let privacy = BehaviorRelay<Bool>(value: false)
let marketing = BehaviorRelay<Bool>(value: false)
let acceptAll = BehaviorRelay<Bool>(value: false)
private let disposeBag = DisposeBag()
func update(termsChecked: Bool? = nil, privacyChecked: Bool? = nil, marketingChecked: Bool? = nil, acceptAllChecked: Bool? = nil) {
if let acceptAllChecked = acceptAllChecked {
// User toggled acceptAll button so change everything to it's value.
acceptAll.accept(acceptAllChecked)
updateIndividualObservables(termsChecked: acceptAllChecked, privacyChecked: acceptAllChecked, marketingChecked: acceptAllChecked)
} else {
// If either of the individual item is missing change acceptAll to false
if termsChecked == nil || privacyChecked == nil || marketingChecked == nil {
acceptAll.accept(false)
}
updateIndividualObservables(termsChecked: termsChecked, privacyChecked: privacyChecked, marketingChecked: marketingChecked)
}
// Deal with the case user triggered select All from individual items and vice-versa.
Observable.combineLatest(terms, privacy, marketing,resultSelector: {(termsChecked,privacyChecked, marketingChecked) in
switch (termsChecked,privacyChecked, marketingChecked) {
case (true, true, true):
self.acceptAll.accept(true)
case (false,false,false):
self.acceptAll.accept(false)
default:
break
}
})
.observeOn(MainScheduler.instance)
.subscribe()
.disposed(by: disposeBag)
}
// MARK: - Helpers
private func updateIndividualObservables(termsChecked: Bool?,privacyChecked: Bool?, marketingChecked:Bool?) {
if let termsChecked = termsChecked {
terms.accept(termsChecked)
}
if let privacyChecked = privacyChecked {
privacy.accept(privacyChecked)
}
if let marketingChecked = marketingChecked {
marketing.accept(marketingChecked)
}
}
}
Your helper function updateIndividualObservables(:) triggers an event every time you update which in turn triggers the combineLatest you implemented above.
I would suggest you to keep a State object instead
struct TermsAndConditionState {
var terms: Bool
var privacy: Bool
var marketing: Bool
var acceptAll: Bool
}
In updateIndividualObservables method change this state and implement this state change with your respective checkboxes
func render(state: TermsAndConditionState) {
if state.acceptAll {
// TODO: update all checkboxes
} else {
// TODO: update individual checkboxes
}
}
This is a simple state machine. State machines are implemented in Rx using the scan(_:accumulator:) or scan(into:accumulator:) operator like so:
struct Input {
let agreeToPrivacyPolicy: Observable<Void>
let agreeToTerms: Observable<Void>
let agreeToMarketingEmail: Observable<Void>
let agreeToAllOptions: Observable<Void>
}
struct Output {
let agreeToPrivacyPolicy: Observable<Bool>
let agreeToTerms: Observable<Bool>
let agreeToMarketingEmail: Observable<Bool>
let agreeToAllOptions: Observable<Bool>
}
func viewModel(input: Input) -> Output {
enum Action {
case togglePrivacyPolicy
case toggleTerms
case toggleMarketingEmail
case toggleAllOptions
}
let action = Observable.merge(
input.agreeToPrivacyPolicy.map { Action.togglePrivacyPolicy },
input.agreeToTerms.map { Action.toggleTerms },
input.agreeToMarketingEmail.map { Action.toggleMarketingEmail },
input.agreeToAllOptions.map { Action.toggleAllOptions }
)
let state = action.scan(into: State()) { (current, action) in
switch action {
case .togglePrivacyPolicy:
current.privacyPolicy = !current.privacyPolicy
case .toggleTerms:
current.terms = !current.terms
case .toggleMarketingEmail:
current.marketingEmail = !current.marketingEmail
case .toggleAllOptions:
if !current.allOptions {
current.privacyPolicy = true
current.terms = true
current.marketingEmail = true
}
}
current.allOptions = current.privacyPolicy && current.terms && current.marketingEmail
}
return Output(
agreeToPrivacyPolicy: state.map { $0.privacyPolicy },
agreeToTerms: state.map { $0.terms },
agreeToMarketingEmail: state.map { $0.marketingEmail },
agreeToAllOptions: state.map { $0.allOptions }
)
}
struct State {
var privacyPolicy: Bool = false
var terms: Bool = false
var marketingEmail: Bool = false
var allOptions: Bool = false
}
I have an array of object which I used to create a checkbox. The model has an id, name. I created a stackView to handle checkbox with id now I want to append items of selected checkbox to an array and be able to remove them when deselected. I am able to present all the views and it works well
below is my code
NetworkAdapter.instance.getFeaturesAmeneities()
.subscribe(onNext: {feat in
guard let data = feat.data else {return}
self.features.append(contentsOf: data)
self.stackFeature.axis = .vertical
self.stackFeature.distribution = .fill
self.stackFeature.spacing = 8
data.forEach {
print($0.id)
self.stv = CheckboxStackView()
self.stv?.label.text = $0.name
self.stv?.checkBox.tag = $0.id ?? 0
self.stackFeature.addArrangedSubview(self.stv!)
}
}).disposed(by: disposeBag)
Any help is appreciated
In order to answer this question, we first have to make the stack view reactive and declarative. This means that we have to be able to set the view using a single assignment and that needs to be an observer. This is just like what the RxCocoa library does for UICollectionView, UITableView and UIPickerView.
Writing the function is a bit advanced. First we take the signature from the other views above to define the shape of the function.
func items<Sequence: Swift.Sequence, Source: ObservableType>(_ source: Source) -> (_ viewForRow: #escaping (Int, Sequence.Element, UIView?) -> UIView) -> Disposable where Source.Element == Sequence
The above probably looks daunting. It's a function that takes a source sequence of sequences and returns a function that takes a closure for assembling the views and returns a Dispoable.
The completed function looks like this:
extension Reactive where Base: UIStackView {
func items<Sequence: Swift.Sequence, Source: ObservableType>(_ source: Source) -> (_ viewForRow: #escaping (Int, Sequence.Element, UIView?) -> UIView) -> Disposable where Source.Element == Sequence {
return { viewForRow in
return source.subscribe { event in
switch event {
case .next(let values):
let views = self.base.arrangedSubviews
let viewsCount = views.count
var valuesCount = 0
for (index, value) in values.enumerated() {
if index < viewsCount {
// update views that already exist
_ = viewForRow(index, value, views[index])
}
else {
// add new views if needed
let view = viewForRow(index, value, nil)
self.base.addArrangedSubview(view)
}
valuesCount = index
}
if valuesCount + 1 < viewsCount {
for index in valuesCount + 1 ..< viewsCount {
// remove extra views if necessary
self.base.removeArrangedSubview(views[index])
views[index].removeFromSuperview()
}
}
case .error(let error):
fatalError("Errors can't be allowed: \(error)")
case .completed:
break
}
}
}
}
}
The above can be used like this:
self.stackFeature.axis = .vertical
self.stackFeature.distribution = .fill
self.stackFeature.spacing = 8
let features = NetworkAdapter.instance.getFeaturesAmeneities()
.map { $0.data }
.share(replay: 1)
features
.bind(onNext: { [weak self] in self?.features.append(contentsOf: $0) })
.disposed(by: disposeBag)
features
.bind(to: stackFeature.rx.items) { (row, element, view) in
let myView = (view as? CheckboxStackView) ?? CheckboxStackView()
myView.label.text = element.name
myView.checkBox.tag = element.id ?? 0
return myView
}
.disposed(by: disposeBag)
I'm facing a problem when selecting the table view row on RxSwift. For details, the code on the do(onNext:) function is called twice, thus lead to the navigation pushed twice too. Here is my code in the viewModel, please help me resolve it. Thanks so much.
struct Input {
let loadTrigger: Driver<String>
let searchTrigger: Driver<String>
let selectMealTrigger: Driver<IndexPath>
}
struct Output {
let mealList: Driver<[Meal]>
let selectedMeal: Driver<Meal>
}
func transform(_ input: HomeViewModel.Input) -> HomeViewModel.Output {
let popularMeals = input.loadTrigger
.flatMap { _ in
return self.useCase.getMealList()
.asDriver(onErrorJustReturn: [])
}
let mealSearchList = input.searchTrigger
.flatMap { text in
return self.useCase.getMealSearchList(mealName: text)
.asDriver(onErrorJustReturn: [])
}
let mealList = Observable.of(mealSearchList.asObservable(), popularMeals.asObservable()).merge().asDriver(onErrorJustReturn: [])
let selectedMeal = input.selectMealTrigger
.withLatestFrom(mealList) { $1[$0.row] }
.do(onNext: { meal in
self.navigator.toMealDetail(meal: meal)
})
return Output(mealList: mealList, selectedMeal: selectedMeal)
}
Edit: Here's the implemetation on the ViewController:
func bindViewModel() {
self.tableView.delegate = nil
self.tableView.dataSource = nil
let emptyTrigger = searchBar
.rx.text.orEmpty
.filter { $0.isEmpty }
.throttle(0.1, scheduler: MainScheduler.instance)
.asDriver(onErrorJustReturn: "")
let loadMealTrigger = Observable
.of(emptyTrigger.asObservable(), Observable.just(("")))
.merge()
.asDriver(onErrorJustReturn: "")
let searchTrigger = searchBar.rx.text.orEmpty.asDriver()
.distinctUntilChanged()
.filter {!$0.isEmpty }
.throttle(0.1)
let selectMealTrigger = tableView.rx.itemSelected.asDriver()
let input = HomeViewModel.Input(
loadTrigger: loadMealTrigger,
searchTrigger: searchTrigger,
selectMealTrigger: selectMealTrigger
)
let output = viewModel.transform(input)
output.mealList
.drive(tableView.rx.items(cellIdentifier: MealCell.cellIdentifier)) { index, meal, cell in
let mealCell = cell as! MealCell
mealCell.meal = meal
}
.disposed(by: bag)
output.selectedMeal
.drive()
.disposed(by: bag)
}
Firstly, is this RxSwift?
If so, the .do(onNext:) operator provides side effects when you receive a new event via a subscription; Therefore, two "reactions" will happen when a table row is tapped: 1. subscription method and 2. .do(onNext:) event. Unfortunately, I do not have any further insight into your code, so there may be other stuff creating that error aswell.
Good luck!
I have a few functions that are basically the same apart from a few variable names that they reference. I want to abstract the function so that I don't have to keep duplicating the code. This is an example function:
func listenToParticipantNumber() {
guard let reference = participantNumberReference else {
return
}
guard participantNumberListener == nil else {
return
}
participantNumberListener = backendClient.listenToRtdbProperty(reference) { [weak self] (result: Result<Int, RequestError>) in
guard let strongSelf = self else {
return
}
switch result {
case .success(let participantNumber):
strongSelf.participantNumber = participantNumber
case .failure:
break
}
}
}
In another function, I'd switch out participantNumberReference, participantNumber, and participantNumberListener for different variables (which are all private to my class), and the block return type of Int. But the core layout of the function would be the same.
How can I make this process cleaner to reuse this code rather than having to duplicate it? Is it possible to somehow use KeyPaths to reference different variables of my class?
It takes a lot of pre-amble to achieve this abstraction level, so you have to ask larger questions about What You're Really Trying To Do, and Is It Worth It, but here's an outline. The essence is to use a dictionary with properties instead of explicitly declared variables.
Now you can add future cases to PersonType and initialize them (which may be automate-able) and the function will apply to all of them regardless. PersonType could conceivably be declared outside the class to increase separation.
This is probably a better suited for Code Review, where you can post a lot more context.
enum PersonType { case Participant, case Leader, case Observer }
struct TrackedObject { var number: Int; var numberReference: ReferenceProtocol; var numberListener: ListenerProtocol; }
// Instead of 9 private variables, make a Dictionary of 3 objects each of
// which have 3 properties
private var persons: [ PersonType: TrackedObject ] = ...
func listenToPersonOfType(personType: PersonType) {
guard let reference = persons[personType].numberReference else {
return
}
guard persons[personType].numberListener == nil else {
return
}
persons[personType].numberListener = backendClient.listenToRtdbProperty(reference) { [weak self] (result: Result<Int, RequestError>) in
guard let strongSelf = self else {
return
}
switch result {
case .success(let number):
strongSelf.persons[personType].number = number
case .failure:
break
}
}
}
I think you're on the right road with a KeyPath. I would probably refactor this a little bit so that the listen method returns Listener? (whatever type that is), and hoist the "if listener is already set, don't do this," outside of this function.
That would leave you something along these lines:
func listen<Response>(reference: Int?, // Or whatever type this is
updating keyPath: WritableKeyPath<C, Response>,
completion: (Result<Response, Error>) -> Void) -> Listener? {
guard let reference = reference else { return nil }
return backendClient.listenToRtdbProperty(reference) { [weak self] (result: Result<Response, Error>) in
guard var strongSelf = self else {
return
}
switch result {
case .success(let result):
strongSelf[keyPath: keyPath] = result
case .failure:
break
}
}
Or you can keep your existing structure with something like this:
func listen<Response>(reference: Int?,
updating keyPath: WritableKeyPath<C, Response>,
forListener listener: WritableKeyPath<C, Listener?>,
completion: (Result<Response, Error>) -> Void) {
guard let reference = reference else { return }
guard self[keyPath: listener] == nil else {
return
}
var mutableSelf = self
mutableSelf[keyPath: listener] = backendClient.listenToRtdbProperty(reference) { [weak self] (result: Result<Response, Error>) in
guard var strongSelf = self else {
return
}
switch result {
case .success(let result):
strongSelf[keyPath: keyPath] = result
case .failure:
break
}
}
}
(C in all of this code is "this class." We can't yet write Self to mean "the current class.")
So I solved it like this:
private func listen<T>(
to property: ReferenceWritableKeyPath<LivestreamModel, T?>,
withReference propertyReference: ReferenceWritableKeyPath<LivestreamModel, String?>,
listener: ReferenceWritableKeyPath<LivestreamModel, DatabaseHandle?>) {
guard let reference = self[keyPath: propertyReference] else {
return
}
guard self[keyPath: listener] == nil else {
return
}
self[keyPath: listener] = backendClient.listenToRtdbProperty(reference) { [weak self] (result: Result<T, RequestError>) in
switch result {
case .success(let value):
self?[keyPath: property] = value
case .failure:
self?[keyPath: property] = nil
}
}
}
I actually did this before Rob replied, but I had to use ReferenceWritableKeyPath because it gives a warning that self has no subscript members when using WritableKeyPath.
I need to style UIButton depends on values from two textFields:
Observable.combineLatest(loginProperty.asObservable(), passwordProperty.asObservable()) { _, _ in
self.viewModel.isValid
}.bind(to: mainView.loginButton.rx.isValid).disposed(by: bag)
Also I need to style UIButton depends on value from one text field, and above solution doesn't work. Why? What is the simple way to do that?
You can use combinedLatest to combine email and password to create isButtonEnabled Observable and bind the values to BehaviorRelay, then subscribe to onNext events from the BehaviorRelay and set values
func rxLogin() {
let isValidPassword = username.rx.text.orEmpty
.map { $0.count > 8 }
.distinctUntilChanged()
let isValidEmail = password.rx.text.orEmpty
.map { $0.contains("#") }
.distinctUntilChanged()
let isButtonEnabled: Observable<Bool> = Observable.combineLatest(isValidEmail, isValidPassword) { $0 && $1 }.share()
//option 1
let submitButtonState: BehaviorRelay<Bool> = BehaviorRelay<Bool>(value: false)
isButtonEnabled
.bind(to: submitButtonState)
.disposed(by: disposeBag)
submitButtonState
.bind { (isEnabled) in
self.loginButton.isEnabled = isEnabled
self.loginButton.backgroundColor = isEnabled ? UIColor.green : UIColor.red
}.disposed(by: disposeBag)
//OR
isButtonEnabled
.subscribe(onNext: { (isValidCredentials) in
self.loginButton.isEnabled = isValidCredentials
self.loginButton.backgroundColor = isValidCredentials ? .green : .red
}).disposed(by: disposeBag)
}
As Al_ mentioned in his comment, combineLatest will emit first next value when both streams will emit at least one value.
To style button depending on one of the text field rx properties:
you can use side effects (ex: .do(onNext: { ... }))
or just make another subscription to the property
Are you using any extension? As there is no property isValid, try to use mainView.loginButton.rx.isHidden if you want to change visibility of the button.