Search code examples
iosswiftreactive-programmingrx-swiftrx-cocoa

Handling circular style events on observable sequence RxSwift


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. Observables

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)
        }
    }
}


Solution

  • 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
      }
    }