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

Jano
  • 62,815
  • 21
  • 164
  • 192
Sandeep Bhandari
  • 19,999
  • 5
  • 45
  • 78
  • @maxim-volgin : Please have a look – Sandeep Bhandari Nov 23 '17 at 09:59
  • Have you got any new solutions for array? – Nominalista Jan 06 '18 at 17:27
  • I am unable to access BehaviorRelay. Any ideas why? My pod file only points to pod 'RxSwift', '~> 4.0' – Shabarinath Pabba Apr 23 '18 at 18:21
  • @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 necessary – Sandeep Bhandari Apr 24 '18 at 05:51
  • 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. – Shabarinath Pabba Apr 26 '18 at 17:07
  • 2
    @ShabarinathPabba Import RxCocoa. The BehaviorRelay is declared in RxCocoa. –  Aug 13 '18 at 04:42

8 Answers8

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])
dalton_c
  • 6,876
  • 1
  • 33
  • 42
  • 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 :) +1 – Sandeep Bhandari May 01 '18 at 14:38
  • 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. – Dima Gimburg Jul 20 '18 at 19:12
  • How to remove members? – Chanchal Raj Sep 10 '18 at 11:21
  • @ChanchalRaj create a newArray from the original myFilter and then remove the newModel from newArray and then do a myFilter.accept(newArray) – jaytrixz Sep 14 '18 at 12:08
  • 3
    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? – Chuck Krutsinger Oct 05 '18 at 22:09
  • @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. – dalton_c Oct 06 '18 at 03:45
  • @daltonclaybrook I think you misunderstand how Array works. When you call Array.append you do not receive back a new Array. https://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. – Chuck Krutsinger Oct 07 '18 at 12:41
  • 1
    @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. – Chuck Krutsinger Oct 08 '18 at 14:34
  • Thanks for the info, @ChuckKrutsinger. That's pretty interesting. I take back what I said. – dalton_c Oct 09 '18 at 00:55
  • @daltonclaybrook You are correct that Array is a value object but it is backed by a reference type, at least according to https://medium.com/commencis/stop-using-structs-e1be9a86376f – Chuck Krutsinger Oct 09 '18 at 01:46
13

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

extension BehaviorRelay where Element: RangeReplaceableCollection {
    func acceptAppending(_ element: Element.Element) {
        accept(value + [element])
    }
}
retendo
  • 1,309
  • 2
  • 12
  • 18
  • 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. – Shai Mishali May 04 '19 at 15:17
  • 1
    @ShaiMishali why are you not supposed to commonly do this? – m0s Jun 12 '19 at 10:56
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.

Ashkan Sarlak
  • 7,124
  • 6
  • 39
  • 51
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()
sazz
  • 3,193
  • 3
  • 23
  • 33
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)
Maxim Volgin
  • 3,957
  • 1
  • 23
  • 38
  • 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. – Sandeep Bhandari Nov 23 '17 at 10:47
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)
    }
}
Lukas Würzburger
  • 6,543
  • 7
  • 41
  • 75
Reimond Hill
  • 4,278
  • 40
  • 52
  • IMO, this should be included as part of RxSwift. Then again, so should BehaviorRelay, but there you go. – Womble Jul 02 '19 at 02:37
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)
Heikki Hautala
  • 491
  • 4
  • 5
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"
babbuki
  • 21
  • 2