How to observe the value in array with RxSwift - ios

There is class have a Bool property
class Coupon: NSObject {
var invalid: Bool = false
}
I create a array [Coupon],I need to modify the coupon.invalid to true one by one.when all of these coupon.invalid == true ,then I can process next task,How can I implement this logic with RxSwift ?Please help me ~~

First you make your Coupon type a struct instead of a class.
struct Coupon {
var invalid = false
}
And embed your array into a Variable.
let coupons = Variable<[Coupon]>([])
coupons.value = getAllTheCoupons()
Now you can create an observable that emits a value whenever all the values are true.
let allTrue = coupons.asObservable()
.map { coupons in
coupons.contains(where: { $0.invalid == false }) == false
}
.filter { $0 }
allTrue.subscribe(onNext: { _ in
print("process next task.")
}).disposed(by: bag)

Related

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

How to call a method once two variables have been set

I am using iOS Swift, and I am trying to understand how to execute a method once the value of two variables have been set up (non-null value) once the requests have finished.
After reading some documentation, I have found out some concepts which are interesting. The first one would be didSet, which works as an observer.
I could call the method using this method by simply using didSet if I would require just one variable
didSet
var myVar: String 0 {
didSet {
print("Hello World.")
}
}
Nevertheless, I also need to wait for the second one myVar2, so it would not work.
I have also found DispatchQueue, which I could use to wait a second before calling the method (the requests that I am using are pretty fast)
DispatchQueue
DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
print("Hello world")
})
but I consider that this solution is not efficient.
Is there anyway to combine these two variables or requests in order to call a method once they have finishing setting the value?
Update
I have tried to replicate David s answer, which I believe is correct but I get the following error on each \.
Type of expression is ambiguous without more context
I copy here my current code
var propertiesSet: [KeyPath<SearchViewController, Car>:Bool] = [\SearchViewController.firstCar:false, \SearchViewController.secondCar:false] {
didSet {
if propertiesSet.allSatisfy({ $0.value }) {
// Conditions passed, execute your custom logic
print("All Set")
} else {
print("Not yet")
}
}
}
var firstCar: Car? {
didSet {
propertiesSet[\SearchViewController.firstCar] = true
}
}
var secondCar: Car? {
didSet {
propertiesSet[\SearchViewController.secondCar] = true
}
}
The variables are set individually, each one on its own request.
You could make your properties optional and check they both have values set before calling your function.
var varA: String? = nil {
didSet {
if varA != nil && varB != nil {
myFunc()
}
}
}
var varB: String? = nil {
didSet {
if varA != nil && varB != nil {
myFunc()
}
}
}
Or you can call your function on each didSet and use a guard condition at the start of your function to check that both of your properties have values, or bail out:
var varA: String? = nil {
didSet {
myFunc()
}
}
var varB: String? = nil {
didSet {
myFunc()
}
}
func myFunc() {
guard varA != nil && varB != nil else { return }
// your code
}
First, you should think very carefully about what your semantics are here. When you say "set," do you mean "assigned a value" or do you mean "assigned a non-nil value?" (I assume you mean the latter in this case.) You should ask yourself, what should happen if your method has already fired, and then another value is set? What if one of the properties has a value is set, then nil is set, then another value set? Should that fire the method 1, 2, or 3 times?
Whenever possible you should work to make these kinds of issues impossible by requiring that the values be set together, in an init rather than mutable properties, for example.
But obviously there are cases where this is necessary (UI is the most common).
If you're targeting iOS 13+, you should explore Combine for these kinds of problems. As one approach:
class Model: ObservableObject {
#Published var first: String?
#Published var second: String?
#Published var ready = false
private var observers: Set<AnyCancellable> = []
init() {
$first.combineLatest($second)
.map { $0 != nil && $1 != nil }
.assign(to: \.ready, on: self)
.store(in: &observers)
}
}
let model = Model()
var observers: Set<AnyCancellable> = []
model.$ready
.sink { if $0 { print("GO!") } }
.store(in: &observers)
model.first = "ready"
model.second = "set"
// prints "GO!"
Another approach is to separate the incidental state that includes optionals, from the actual object you're constructing, which does not.
// Possible parameters for Thing
struct Parameters {
var first: String?
var second: String?
}
// The thing you're actually constructing that requires all the parameters
struct Thing {
let first: String
let second: String
init?(parameters: Parameters) {
guard let first = parameters.first,
let second = parameters.second
else { return nil }
self.first = first
self.second = second
}
}
class TheUIElement {
// Any time the parameters change, try to make a Thing
var parameters: Parameters = Parameters() {
didSet {
thing = Thing(parameters: parameters)
}
}
// If you can make a Thing, then Go!
var thing: Thing? {
didSet {
if thing != nil { print("GO!") }
}
}
}
let element = TheUIElement()
element.parameters.first = "x"
element.parameters.second = "y"
// Prints "GO!"
You need to add a didSet to all variables that need to be set for your condition to pass. Also create a Dictionary containing KeyPaths to your variables that need to be set and a Bool representing whether they have been set already.
Then you can create a didSet on your Dictionary containing the "set-state" of your required variables and when all of their values are true meaning that all of them have been set, execute your code.
This solution scales well to any number of properties due to the use of a Dictionary rather than manually writing conditions like if aSet && bSet && cSet, which can get out of hand very easily.
class AllSet {
var propertiesSet: [KeyPath<AllSet, String>:Bool] = [\.myVar:false, \.myVar2:false] {
didSet {
if propertiesSet.allSatisfy({ $0.value }) {
// Conditions passed, execute your custom logic
print("All Set")
} else {
print("Not yet")
}
}
}
var myVar: String {
didSet {
propertiesSet[\.myVar] = true
}
}
var myVar2: String {
didSet {
propertiesSet[\.myVar2] = true
}
}
init(myVar: String, myVar2: String) {
self.myVar = myVar
self.myVar2 = myVar2
}
}
let all = AllSet(myVar: "1", myVar2: "2")
all.myVar = "2" // prints "Not yet"
all.myVar2 = "1" // prints "All set"

RxSwift Build an Observable based on a Variable

I am trying to build an Observable which would output a value based on the value of a Variable.
Something like that:
let fullName = Variable<String>("")
let isFullNameOKObs: Observable<Bool>
isFullNameOKObs = fullName
.asObservable()
.map { (val) -> Bool in
// here business code to determine if the fullName is 'OK'
let ok = val.characters.count >= 3
return ok
}
Unfortunately the bloc in the map func is never called!
The reason behind this is that:
The fullName Variable is binded to a UITextField with the bidirectional operator <-> as defined in the RxSwift example.
The isFullNameOKObs Observable will be observed to hide or display the submit button of my ViewController.
Any help would be greatly appreciated.
Thanks
The model
class Model {
let fullName = Variable<String>("")
let isFullNameOKObs: Observable<Bool>
let disposeBag = DisposeBag()
init(){
isFullNameOKObs = fullName
.asObservable()
.debug("isFullNameOKObs")
.map { (val) -> Bool in
let ok = val.characters.count >= 3
return ok
}
.debug("isFullNameOKObs")
isRegFormOKObs = Observable.combineLatest(
isFullNameOKObs,
is...OK,
... ) { $0 && $1 && ... }
isRegFormOKObs
.debug("isRegFormOKObs")
.asObservable()
.subscribe { (event) in
// update the OK button
}
// removing this disposedBy resolved the problem
//.disposed(by: DisposeBag())
}
}
The ViewController:
func bindModel() -> Void {
_ = txFullName.rx.textInput <-> model!.fullName
......
}
Do you need the two-way binding between the UITextField and your Variable?
If not, I would suggest you try to just use bindTo() instead like this:
myTextField.rx.text.orEmpty.bindTo(fullName).disposed(by: disposeBag)

how can I remove a struct element from an array of structs based on the id of the element in Swift?

I have a structure in my Swift app:
open class Cluster : NSObject {
open var username: String? = ""
open var id: String? = ""
open var deleted: Bool? = false
}
and now I'm iterating over this array and I'm adding new elements to it, but only in case those elements are not there yet:
if(!self.array.contains(where: {$0.id==temp.id}))
{
self.array.append(temp);
}
I want to tweak this code so that it not only adds new elements if they're not there, but also removes the ones that - in the meantime - had their flag deleted changed to true.
I started writing this code:
if(!self.array.contains(where: {$0.id==temp.id}))
{
self.array.append(temp);
} else {
if(temp.deleted == true){
self.array.remove //how can I remove here this specific element?
}
}
To remove a particular element from an array, you are supposed to get index of that element first and then delete as shown below:
if let index:Int = self.array.index(where: {$0.id == temp.id && $0.deleted == true}) {
self.array.remove(at: index)
}
First, I suggest you fix your class:
An optional Bool makes no sense - the object is either deleted or not
An optional id doesn't make much sense either; All objects need an id
If you implement the hash and equality parts of NSObject then you get access to array's index(of:) method and you can use sets.
Cluster.swift
open class Cluster : NSObject {
open var username: String? = ""
open let id: String
open var isDeleted: Bool = false
init(id: String) {
self.id = id
}
open override var hashValue: Int {
get {
return self.id.hashValue
}
}
open override func isEqual(_ object: Any?) -> Bool {
guard let rhs = object as? Cluster else {
return false
}
let lhs = self
return lhs.id == rhs.id
}
}
Now, given an array of Cluster objects, you can remove the deleted ones using:
let cleanArray = dirtyArrayOfCluster.filter {
!$0.isDeleted
}
And you can remove duplicates by passing the array through a set:
let deDupedArray = Array(Set(cleanArray))
if temp.deleted == true, let index = array.index(where: { $0.id == temp.id }) {
array.remove(at: index)
}
What about this?
if array.contains(where: { $0.id == temp.id } ) {
array.append(temp)
}
array = array.filter { $0.deleted == true }
The first part add temp only if it is not into the array.
The last line removes all the elements marked as deleted.

Observe values added to Set - Swift

Is it possible to observe values being added to a Set data-structure?
What i'm trying to achieve:
var storedStrings = Set<String>() {
didSet (value) {
// where value is a string that has been added to the Set
}
}
Example:
storedStrings.insert("hello")
didSet called, as a new value has been added.
storedString.insert("world")
didSet called again.
storedString.insert("hello")
didSet not called, as the set already contains the string "hello"
This can be a bit expensive, but you still can do something like:
var storedStrings = Set<String>() {
didSet {
if storedStrings != oldValue {
print("storedStrings has changed")
let added = storedStrings.subtracting(oldValue)
print("added values: \(added)")
let removed = oldValue.subtracting(storedStrings)
print("removed values: \(removed)")
}
}
}
The insert function returns a tuple with the definition: (inserted: Bool, memberAfterInsert: Element).
Therefore the check for a new unique element can be made on insertion instead of using didSet.
var storedStrings = Set<String>()
var insertionResult = storedStrings.insert("Hello")
if insertionResult.inserted {
print("Value inserted") // this is called
}
insertionResult = storedStrings.insert("Hello")
if insertionResult.inserted {
print("Value inserted") // this isn't called
}
You could implement your own inserter for your set, which could emulate the use of a property observer, making use of the fact that the insert method of Set returns a tuple whose first member is false in case the element to be inserted is already present.
func insert(Element)
Inserts the given element in the set if it is not already present.
From the language reference.
E.g.:
struct Foo {
private var storedStrings = Set<String>()
mutating func insertNewStoredString(_ newString: String) {
if storedStrings.insert(newString).0 {
print("Inserted '\(newString)' into storedStrings")
}
}
}
var foo = Foo()
foo.insertNewStoredString("hello") // Inserted 'hello' into storedStrings
foo.insertNewStoredString("hello")
foo.insertNewStoredString("world") // Inserted 'world' into storedStrings

Resources