Search code examples
arraysswiftdictionaryobservablerx-swift

How can a mutable dictionary be made observable in RxSwift


We would like to use a dictionary as observable to notify any subscribers upon changes. So far this is implemented as BehaviorRelay<[KeyType: ValueObjectType]>

Now we need to add/remove/change values of the dictionary inside the observable. We tried the following (in simplified terms), but it did not work:

let list = BehaviorRelay<[String: MyClassType]>.init([:])
let newElem = MyClassType()
list.value.updateValue(newElem, forKey: "anykey")

The compiler complains: Cannot use mutating member on immutable value: 'value' is a get-only property

The following works, but I find it cumbersome and probably inefficient performance wise:

let list = BehaviorRelay<[String: MyClassType]>.init([:])
let newElem = MyClassType()
let newList = list.value
newList.updateValue(newElem, forKey: "anykey")
list.accept(newList)

Typical subscribers for the list on the UI side would be e.g. a UITableView or UICollectionView.

Is there a better way to handle this?


Solution

  • Your second way is is the way to go. It can be improved cumbersome wise by writing like this:

    let list = BehaviorRelay<[String: MyClassType]>.init(value: [:])
    list.accept(list.value.merging(["anykey": MyClassType()]){ (_, new) in new })
    

    If this has to be done too many times, the following extension can come in handy

    extension Dictionary where Key == String, Value == MyClassType {
        static func + (lhs: [String : MyClassType], rhs: [String : MyClassType]) -> [String : MyClassType] {
            return lhs.merging(rhs) { (_, new) in new }
        }
    }
    

    Now you can just do this list.accept(list.value + ["anykey": MyClassType()])

    Please note that if the right side operand has a key that is also present in the left side operand, the right side value will override the left one. According to my understanding this is your desired behaviour.

    Also the first way that you were trying would work with Variable. But Variable is considered deprecated now in favour of BehaviorRelay and value property of BehaviourRelay is get only.

    After discussing with @JR in the comments, a generic extension can be written for BehaviorRelay where the Element is an Array/ Dictionary

    extension BehaviorRelay {
        func addElement<T>(element: T) where Element == [T] {
            accept(value + [element])
        }
    
        func removeElement<T>(element: T) where Element == [T], T: Equatable {
            accept(value.filter {$0 != element})
        }
    
        func addElement<T, U>(key: T, value: U) where Element == [T: U] {
            accept(self.value.merging([key: value]) { (_, new) in new })
        }
        func removeElemnent<T>(key: T) where Element == [T: Any] {
            accept(self.value.filter {dictElemnt in dictElemnt.key != key})
        }
    }