'How to use BehaviorRelay as an alternate to Variable in RxSwift?

As of RxSwift4, Variable is moved to Deprecated.swift marking the possible deprecation of Variable in future. An alternate proposed to Variable is BehaviorRelay. While posting this question, as I could not find much of the tutorial on web using BehaviorRelay am posting such a fundamental question here in SO.

Assume I have a webService call going on and I receive a chunk of data which is JSONArray, on parsing JSON object one by one I update my Variable's value property

Here is my variable declaration

var myFilter = Variable<[MyFilterModel]>([MyFilterModel(data: "{:}")])

on getting a new element each time I would update my Variable as

myFilter.value.append(newModel)

As Variable was bind to CollectionView, collectionVie would update its UI immediately with the newly added object.

Issue with BehaviorRelay

Now my declaration looks like

var myFilter = BehaviorRelay<[MyFilterModel]>(value: [MyFilterModel(data: "{:}")])

But biggest issue is myFilter.value is readOnly. So obviously

myFilter.value.append(newModel) 

is not a solution. I figured out that I can use accept rather.

But now when I try to parse each element in response and update the value of myFilter

self?.expertsFilter.accept(newModel)

The above statement gives error quoting

Can not convert the value of NewModel to expected arguement type [NewModel]

Obviously, its expecting a array and not a individual element.

Workaround:

Solution 1:

So one solution is accumulate all the response in a temporary array and once done trigger self?.expertsFilter.accept(temporary_array)

Solution 2:

If I have to send onNext event to subscriber on parsing each element, I need to copy the value of self?.expertsFilter to new Array, add the newly parsed element to it and return the new array.

Solution 3:

Get rid of BehaviorRelay and use BehaviorSubject/PublishSubject

First two sounds depressing, because there may be a need to trigger UI on parsing each element I cant wait till entire response is parsed. So obviously solution1 is not much of use.

Second solution is much more horrible because it creates a new array (I know its temporary and will be released) every time to send onNext event.

Question:

Because BehaviorRelay is proposed as a alternate to Variable am in dilemma, am using accept correctly?? Is there a better way to solve it?

Please help



Solution 1:[1]

Have you considered simply creating a new array from the existing value on the relay, appending, then calling accept?

myFilter.accept(myFilter.value + [newModel])

Solution 2:[2]

Building on Dalton's answer, here is a handy extension:

extension BehaviorRelay where Element: RangeReplaceableCollection {
    func acceptAppending(_ element: Element.Element) {
        accept(value + [element])
    }
}

Solution 3:[3]

I wrote this extension for replacing Variables with BehaviorRelays. You can add whatever method you need based on this pattern to migrate easily.

public extension BehaviorRelay where Element: RangeReplaceableCollection {

    public func insert(_ subElement: Element.Element, at index: Element.Index) {
        var newValue = value
        newValue.insert(subElement, at: index)
        accept(newValue)
    }

    public func insert(contentsOf newSubelements: Element, at index: Element.Index) {
        var newValue = value
        newValue.insert(contentsOf: newSubelements, at: index)
        accept(newValue)
    }

    public func remove(at index: Element.Index) {
        var newValue = value
        newValue.remove(at: index)
        accept(newValue)
    }
}

Instead of Variable.value.funcName, now you write BehaviorRelay.funcName.

The idea to use where Element: RangeReplaceableCollection clause comes from retendo's answer

Also note that the index is of type Element.Index, not Int or whatever else.

Solution 4:[4]

I created this extension, with two methods to facilitate migration in case you have a Variable of Array and you have to use append.

    extension BehaviorRelay where Element: RangeReplaceableCollection {

        func append(_ subElement: Element.Element) {
            var newValue = value
            newValue.append(subElement)
            accept(newValue)
        }

        func append(contentsOf: [Element.Element]) {
            var newValue = value
            newValue.append(contentsOf: contentsOf)
            accept(newValue)
        }

        public func remove(at index: Element.Index) {
            var newValue = value
            newValue.remove(at: index)
            accept(newValue)
        }

        public func removeAll() {
            var newValue = value
            newValue.removeAll()
            accept(newValue)
        }

    }

and you call it like this

    var things = BehaviorRelay<[String]>(value: [])
    things.append("aa")
    let otherThings = ["bb", "cc"]
    things.append(contentsOf: otherThings) 
    things.remove(at: 0)
    things.removeAll()

Solution 5:[5]

I would do something like that -

let requests = PublishSubject<Observable<ServerResponse>>.create()
let responses: Observable<ServerResponse> = requests.switchLatest()

let parsed: Observable<[ParsedItem]> = responses
  .flatMap { Observable.from($0).map { parse($0) }.toArray() }

parsed.bind(to: ui)

// repeated part
let request1: Observable<ServerResponse> = servive.call()
request.onNext(request1)

Solution 6:[6]

On Variable used to have:

let variable = Variable("Hello RxSwift")
variable.value = "Change text" 
print(variable.value) // "Change text"

On BehaviorRelay you have to use:

let relay = BehaviorRelay(value: "Hello RxSwift")
relay.accept("Change text")
print(relay.value) // "Change text"

Solution 7:[7]

AshKan answer is great but I came here looking for a missing method from the solution. Append:

extension BehaviorRelay where Element: RangeReplaceableCollection {
        
    func append(_ subElement: Element.Element) {
        var newValue = value
        newValue.append(subElement)
        accept(newValue)
    }
}

Solution 8:[8]

How about this kind of extension:

extension BehaviorRelay {

    var val: Element {
        get { value }
        set { accept(newValue) }
    }
}

Then use it as you were using the Variable, but instead of calling value, call val:

myFilter.val.append(newModel)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 dalton_c
Solution 2 retendo
Solution 3 Ashkan Sarlak
Solution 4
Solution 5 Maxim Volgin
Solution 6 babbuki
Solution 7 Lukas Würzburger
Solution 8 Heikki Hautala