How to use BehaviorRelay as an alternate to Variable in RxSwift?
Asked Answered
M

8

72

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

Maundy answered 23/11, 2017 at 9:59 Comment(6)
@maxim-volgin : Please have a lookMaundy
Have you got any new solutions for array?Daimon
I am unable to access BehaviorRelay. Any ideas why? My pod file only points to pod 'RxSwift', '~> 4.0'Genoa
@shabarinath-pabba : Make sure u import RxSwift and RxCocoa using import RxSwift import RxCocoa in your file which uses BehaviorRelay. BehaviorRelay is declared in RxSwift so logically importing import RxSwift should be enough but if you are using it with Cocoa import RxCocoa will be necessaryMaundy
Weirdly I do have "import RxSwift" but BehaviorRelay isn't accessible for me. I don't have "import RxCocoa" though, because I figured its probably for macOS app development.Genoa
@ShabarinathPabba Import RxCocoa. The BehaviorRelay is declared in RxCocoa.Cystolith
D
39

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

myFilter.accept(myFilter.value + [newModel])
Dixiedixieland answered 1/5, 2018 at 3:36 Comment(10)
Thats what I ended up doing :) But was wondering is that how its supposed to be used ? Is that the correct approach ? But by now I have used it so many times that now I feel like this is correct way :) hence will hold on to sometime before accepting ur answer :) +1Maundy
Hi @SandeepBhandari, do you have an answer about "is this supposed to be used like this?" I am really pretty much in the same corner you were few months ago. do I have each time replicate the BeahviorRelay object in order to accept it? thanks.Macmacabre
How to remove members?Lagas
@ChanchalRaj create a newArray from the original myFilter and then remove the newModel from newArray and then do a myFilter.accept(newArray)Blastopore
What is the performance hit when creating a new array and emitting a whole new array vs just appending to the end of an existing array?Hindquarter
@ChuckKrutsinger Because Swift Array is a value type, a new array is always created when one is mutated, including when you call append(_:), so there is no difference in performance.Dixiedixieland
@daltonclaybrook I think you misunderstand how Array works. When you call Array.append you do not receive back a new Array. developer.apple.com/documentation/swift/array/1538872-append discusses the append and makes clear that the new item(s) is appended to the existing instance, further reinforced by the fact that you don't have to capture the return from the append operation. So there is definitely a difference between appending and creating a new Array.Hindquarter
@daltonclaybrook As a follow up, I ran a test using an array of 1,000,000 Ints and appended an Array of 1,000,000 Ints. Then I combined the 2 Arrays to form a new Array. Difference in timing was about 70% extra for creating the new array using array1 + array2 vs using append. Good news is that even with such large Arrays, the time was below 2 seconds. Probably not going to affect my particular design very much.Hindquarter
Thanks for the info, @ChuckKrutsinger. That's pretty interesting. I take back what I said.Dixiedixieland
@daltonclaybrook You are correct that Array is a value object but it is backed by a reference type, at least according to medium.com/commencis/stop-using-structs-e1be9a86376fHindquarter
S
13

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

extension BehaviorRelay where Element: RangeReplaceableCollection {
    func acceptAppending(_ element: Element.Element) {
        accept(value + [element])
    }
}
Souse answered 28/8, 2018 at 15:15 Comment(2)
I personally wouldn't want to hide something like that under an operator - because it's not something you're supposed to commonly do. If you do, I'd say you should be as explicit as possible about it.Alain
@ShaiMishali why are you not supposed to commonly do this?Bemock
D
10

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.

Deianira answered 19/11, 2018 at 19:12 Comment(0)
S
8

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()
Supersedure answered 3/8, 2019 at 13:22 Comment(0)
H
2

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)
Hays answered 23/11, 2017 at 10:15 Comment(1)
I appreciate your effort and time :) Hence +1. But idea is to have a paginated web service call being called when user scrolls down the collectionView, so I cant really bind a parsed to ui, I need to have a variable/observable holding the data to UI and my web service call should only updated the variable with new value. So Variable was perfect. Now BehaviorRelay does the same thing but takes away the benefit of triggering UI every time a new element parsed.Maundy
T
1

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)
    }
}
Twittery answered 13/5, 2019 at 15:24 Comment(1)
IMO, this should be included as part of RxSwift. Then again, so should BehaviorRelay, but there you go.Radu
M
1

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)
Misgovern answered 17/12, 2020 at 7:0 Comment(0)
J
1

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"
Jillane answered 10/1, 2021 at 0:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.