24

I have a Variable which is an array of enum values. These values change over time.

enum Option {
    case One
    case Two
    case Three
}

let options = Variable<[Option]>([ .One, .Two, .Three ])

I then observe this variable for changes. The problem is, I need to know the diff between the newest value and the previous value. I'm currently doing this:

let previousOptions: [Option] = [ .One, .Two, .Three ]

...

options
    .asObservable()
    .subscribeNext { [unowned self] opts in
        // Do some work diff'ing previousOptions and opt
        // ....
        self.previousOptions = opts
    }

Is there something built in to RxSwift that would manage this better? Is there a way to always get the previous and current values from a signal?

8 Answers8

35

Here is a handy generic extension, that should cover these "I want the previous and the current value" use cases:

extension ObservableType {
    
    func withPrevious(startWith first: Element) -> Observable<(Element, Element)> {
        return scan((first, first)) { ($0.1, $1) }.skip(1)
    }
}
Nikolay Suvandzhiev
  • 8,465
  • 6
  • 41
  • 47
retendo
  • 1,309
  • 2
  • 12
  • 18
  • 1
    Awesome! I just added parameter names like so: `Observable<(previous: E, current: E)>` – d4Rk Feb 04 '19 at 16:32
  • I just used Scan from your answer, I didn't know it before.. thanks! – Yitzchak Jun 27 '19 at 13:09
  • 4
    This method would never emit a tuple containing `first` element provided as a parameter, consider removing `.skip(1)` – swearwolf Jun 11 '20 at 12:27
  • What do I use for `startWith`? The current / last value of the observable? – Jaap Weijland Jun 12 '20 at 12:23
  • @swearwolf True, if you want that first value, remove the skip call. I can also see that this is misleading naming on my side. – retendo Jun 12 '20 at 13:22
  • @Jaap Weijland Use something that has the same type as the elements that go through your Observable. With this implementation that value is skipped anyway, it’s only there to kickstart the scan operator, which needs a starting value. – retendo Jun 12 '20 at 13:26
19

there you go

options.asObservable()
    .scan( [ [],[] ] ) { seed, newValue in
        return [ seed[1], newValue ]
    }
    // optional, working with tuple of array is better than array of array
    .map { array in (array[0], array[1])  } 
    //optional, in case you dont want empty array
    .skipWhile { $0.count == 0 && $1.count == 0 }

it will return Observable<([Options], [Options])> :)

Pham Hoan
  • 2,107
  • 2
  • 20
  • 34
13

Another way as an extension

extension ObservableType {

  func withPrevious() -> Observable<(E?, E)> {
    return scan([], accumulator: { (previous, current) in
        Array(previous + [current]).suffix(2)
      })
      .map({ (arr) -> (previous: E?, current: E) in
        (arr.count > 1 ? arr.first : nil, arr.last!)
      })
  }
}

Usage:

someValue
  .withPrevious()
  .subscribe(onNext: { (previous, current) in
    if let previous = previous { // previous is optional
      print("previous: \(previous)")
    }
    print("current: \(current)")
  })
  .disposed(by: disposeBag)
richy
  • 2,716
  • 1
  • 33
  • 42
11

As Pham Hoan said, scan(_) is the right tool for the job. Marin Todorov wrote a good post on doing exactly this.

Here's what I came up with, based on Marin's post:

options
        .asObservable()
        .scan([]) {
            (previous, current) in
                return Array(previous + [current]).suffix(2)
        }
        .subscribeNext {
            (lastTwoOptions) in
                let previousOptions = lastTwoOptions.first
                let currentOptions = lastTwoOptions.last
                // Do your thing.  Remember to check for nil the first time around!
        }
        .addDisposableTo(self.disposeBag)

Hope that helps

Paul
  • 1,897
  • 1
  • 14
  • 28
8

The .pairwise() operator does exactly what you want, and is the simplest way to do do it. This operator groups pairs of consecutive emissions together and emits them as an array of two values.

see: http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#instance-method-pairwise

or https://rxjs-dev.firebaseapp.com/api/operators/pairwise


UPDATE: as @courteouselk pointed out in his comment, I failed to notice that this was an RxSwift question, and my answer referenced an RxJS solution (oops!).

Turns out RxSwift does not have a built-in pairwise operator, but RxSwiftExt provides a pairwise extension operator similar to the built-in RxJS operator.

jjjjs
  • 1,500
  • 13
  • 9
  • @courteouselk, thank you for pointing this out, I have updated my answer with reference to an RxSwift specific implementation of the pairwise operator – jjjjs Feb 21 '19 at 23:44
4

Best solution in one line:

Observable.zip(options, options.skip(1))
duan
  • 8,515
  • 3
  • 48
  • 70
  • 3
    This is good for most cases, but it should be noted that 'options' should probably be a shared sequence then, otherwise you will get 2 subscriptions, which you might not want and potentially leads to unexpected results. – retendo May 29 '18 at 09:19
  • e.g. let options = randomNumberProducingObservable(); Observable.zip(options, options.skip(1)); would not produce what you expect. Make sure to declare let options = randomNumberProducingObservable().share(); like this – retendo May 29 '18 at 09:24
  • @retendo the question is about how to retrieve value from a `Variable`, which in RxSwift4 replaced by `BehaviorRelay`, is shareable by default. – duan May 29 '18 at 13:28
  • 3
    True, if it relates to a Variable/BehaviourRelay, you're right – retendo May 29 '18 at 14:47
  • This is not good because the first time options will fire on next nothing will happen, only the second time and on – evya Dec 06 '20 at 17:59
2

I would suggest something like this (for future visitors):

options.asObservable()
       .map { (old: [], new: $0) }   // change type from array to tuple
       .scan((old: [], new: [])) { previous, current in
           // seed with an empty tuple & return both information
           return (old: previous.new, new: current.new)
       }
       .subscribe(onNext: { option in
           let oldArray = option.old   // old
           let newArray = option.new   // new
       }
       .addDisposableTo(disposeBag)
sCha
  • 1,454
  • 1
  • 12
  • 22
0

tl;dr:

If you want a tuple with previous and current values, without skipping the first stream event and without specifying an initial value - here is a solution (in this case the previous value is an Optional).



I realise there are already a lot of answers, but when trying them out it always seemed I want something slightly different.

I like the tuple approach, but I don't want to miss the first emission (skip(1)) - I see some solutions with a startWith value, but I wanted a solution where I don't need to provide an initial seed value, and where the tuple.previous value is Optional (to represent a "no previous value yet").

Here is what I came up with, it's a bit verbose (intentionally, to make it easier to read, feel free to shorten syntax). Also I had to constraint Element to be Equatable. Maybe there are better ways (please comment), but for now this is what works for me:

extension ObservableType where Element: Equatable {
    /// Returns a tuple with `.previous` and `.current` value of the stream.
    /// `.previous` is optional, because it might be the first stream value and no previous values exist yet
    func withPrevious() -> Observable<(previous: Element?, current: Element)>  {
        scan((nil, nil)) { (accumulatedValue: (previous:Element?, current: Element?), newValue: Element) -> (previous: Element?, current: Element?) in
            if accumulatedValue == (nil, nil) {
                return (previous: nil, current: newValue)
            } else {
                return (previous: accumulatedValue.current, current: newValue)
            }
        }
        .compactMap { (previous: Element?, current: Element?) -> (previous: Element?, current: Element)? in
            guard let current else { return nil }
            return (previous: previous, current: current)
        }
    }
}

Example usage:

Observable<Int>.from([1, 2, 3, 4])
    .withPrevious()
    .debug()
    .subscribe()
    .dispose()

Console output:

-> subscribed
-> Event next((previous: nil, current: 1))
-> Event next((previous: Optional(1), current: 2))
-> Event next((previous: Optional(2), current: 3))
-> Event next((previous: Optional(3), current: 4))
-> Event completed
-> isDisposed

As an extra - here is the above method but with a startWith value if it makes sense for your use-case - so the .previous value is no longer an Optional. This is equivalent (but with longer syntax) of what others have already suggested in other answers.

/// Returns a tuple with `.previous` and `.current` value of the stream.
/// Expects a starting value which will be the `.previous` value of the first emission
func withPrevious(startWith: Element) -> Observable<(previous: Element, current: Element)>  {
    scan((startWith, startWith)) { (accumulatedValue: (previous: Element, current: Element), newValue: Element) -> (previous: Element, current: Element) in
        if accumulatedValue == (startWith, startWith) {
            return (previous: startWith, current: newValue)
        } else {
            return (previous: accumulatedValue.current, current: newValue)
        }
    }
}
Nikolay Suvandzhiev
  • 8,465
  • 6
  • 41
  • 47