Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

11. Time-Based Operators
Written by Florent Pillet

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Timing is everything. The core idea behind reactive programming is to model asynchronous data flow over time. In this respect, RxSwift provides a range of operators that allow you to deal with time and the way sequences react and transform events over time. As you’ll see throughout this chapter, managing the time dimension of your sequences is easy and straightforward.

To learn about time-based operators, you’ll practice with an animated playground that demonstrates visually how data flows over time. This chapter comes with an empty RxSwiftPlayground, divided in several pages. You’ll use each page to exercise one or more related operators. The playground also includes a number of ready-made classes that’ll come in handy to build the examples.

Getting started

For this chapter, you will be using an Xcode Playground that‘s been set up with the basic building blocks you need to go through the chapter tasks.

To get started, open the macOS Terminal application (found in your Mac’s Applications/Utilities folder), navigate to the current chapter’s starter project folder, then run the bootstrap script like so:

$ ./bootstrap.sh

You can keep the Debug Area visible, but what is most important is that you show the Live View pane. This will display a live view of the sequences you build in code. This is where the real action will happen!

To display the Live View, click the second-to-last button at top-right of the Xcode window’s editor (right below the title bar), as shown below:

The icon of this button changes depending on whether the live view is already visible.

Also make sure that anything you type automatically executes in the Assistant Editor’s preview area. Long-click the play/stop button at the bottom of the editor (if it is currently set to run, it will be a square) and make sure Automatically Run is selected, as in the screenshot below:

In the left Navigator pane, pick the first page named replay. You can then close the Navigator pane using its visibility control, which is the leftmost button at the top-right of the Xcode window.

Your layout should now look like this:

You’re now all set! It’s time to learn about the first group of time-based operators: buffering operators.

Note: This playground uses advanced features of Xcode playgrounds. Xcode does not fully support importing linked frameworks from within files in the common Sources subfolder. Therefore, each playground page has to include a small bit of code (the part of TimelineView that depends on RxSwift) to function properly. Just ignore this code and leave it at bottom of the page.

Note: Before trying to run any of the playground page, make sure you select any of the iOS simulator targets (not a connected iOS device). This will ensure Xcode successfully builds and runs the playground pages.

Buffering operators

The first group of time-based operators deal with buffering. They will either replay past elements to new subscribers, or buffer them and deliver them in bursts. They allow you to control how and when past and new elements get delivered.

Replaying past elements

When a sequence emits items, you’ll often need to make sure that a future subscriber receives some or all of the past items. This is the purpose of the replay(_:) and replayAll() operators. To learn how to use them, you’ll start coding in the replay page of the playground. To visualize what replay(_:) does, you’ll display elements on a timeline. The playground contains custom classes to make it easy to display animated timelines.

let elementsPerSecond = 1
let maxElements = 58
let replayedElements = 1
let replayDelay: TimeInterval = 3
let sourceObservable = Observable<Int>.create { observer in
  var value = 1
  let timer = DispatchSource.timer(interval: 1.0 / Double(elementsPerSecond), queue: .main) {
    if value <= maxElements {
      observer.onNext(value)
      value += 1
    }
  }
  return Disposables.create {
    timer.suspend()
  }
}
.replay(replayedElements)
let sourceTimeline = TimelineView<Int>.make()
let replayedTimeline = TimelineView<Int>.make()
let stack = UIStackView.makeVertical([
  UILabel.makeTitle("replay"),
  UILabel.make("Emit \(elementsPerSecond) per second:"),
  sourceTimeline,
  UILabel.make("Replay \(replayedElements) after \(replayDelay) sec:"),
  replayedTimeline])
_ = sourceObservable.subscribe(sourceTimeline)
DispatchQueue.main.asyncAfter(deadline: .now() + replayDelay) {
  _ = sourceObservable.subscribe(replayedTimeline)
}
_ = sourceObservable.connect()
let hostView = setupHostView()
hostView.addSubview(stack)
hostView

Unlimited replay

The second replay operator you can use is replayAll(). This one should be used with caution: only use it in scenarios where you know the total number of buffered elements will stay reasonable. For example, it’s appropriate to use replayAll() in the context of HTTP requests. You know the approximate memory impact of retaining the data returned by a query. On the other hand, using replayAll() on a sequence that may not terminate and may produce a lot of data will quickly clog your memory. This could grow to the point where the OS jettisons your application!

.replay(replayedElements)
.replayAll()

Controlled buffering

Now that you touched on replayable sequences, you can look at a more advanced topic: controlled buffering. You’ll first look at the buffer(timeSpan:count:scheduler:) operator. Switch to the second page in the playground called buffer. As in the previous example, you’ll begin with some constants:

let bufferTimeSpan: RxTimeInterval = .seconds(4)
let bufferMaxCount = 2
let sourceObservable = PublishSubject<String>()
let sourceTimeline = TimelineView<String>.make()
let bufferedTimeline = TimelineView<Int>.make()

let stack = UIStackView.makeVertical([
  UILabel.makeTitle("buffer"),
  UILabel.make("Emitted elements:"),
  sourceTimeline,
  UILabel.make("Buffered elements (at most \(bufferMaxCount) every \(bufferTimeSpan) seconds):"),
  bufferedTimeline])
_ = sourceObservable.subscribe(sourceTimeline)
sourceObservable
  .buffer(timeSpan: bufferTimeSpan, count: bufferMaxCount, scheduler: MainScheduler.instance)
  .map(\.count)
  .subscribe(bufferedTimeline)
let hostView = setupHostView()
hostView.addSubview(stack)
hostView
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
  sourceObservable.onNext("🐱")
  sourceObservable.onNext("🐱")
  sourceObservable.onNext("🐱")
}

let elementsPerSecond = 0.7
let timer = DispatchSource.timer(interval: 1.0 / Double(elementsPerSecond), queue: .main) {
  sourceObservable.onNext("🐱")
}

Windows of buffered observables

A last buffering technique very close to buffer(timeSpan:count:scheduler:) is window(timeSpan:count:scheduler:). It has roughly the same signature and does nearly the same thing. The only difference is that it emits an Observable of the buffered items, instead of emitting an array.

let elementsPerSecond = 3
let windowTimeSpan: RxTimeInterval = .seconds(4)
let windowMaxCount = 10
let sourceObservable = PublishSubject<String>()
let sourceTimeline = TimelineView<String>.make()

let stack = UIStackView.makeVertical([
  UILabel.makeTitle("window"),
  UILabel.make("Emitted elements (\(elementsPerSecond) per sec.):"),
  sourceTimeline,
  UILabel.make("Windowed observables (at most \(windowMaxCount) every \(windowTimeSpan) sec):")])
let timer = DispatchSource.timer(interval: 1.0 / Double(elementsPerSecond), queue: .main) {
  sourceObservable.onNext("🐱")
}
_ = sourceObservable.subscribe(sourceTimeline)
_ = sourceObservable
  .window(timeSpan: windowTimeSpan, count: windowMaxCount, scheduler: MainScheduler.instance)
.flatMap { windowedObservable -> Observable<(TimelineView<Int>, String?)> in
  let timeline = TimelineView<Int>.make()
  stack.insert(timeline, at: 4)
  stack.keep(atMost: 8)
  return windowedObservable
    .map { value in (timeline, value) }
    .concat(Observable.just((timeline, nil)))
}
.subscribe(onNext: { tuple in
  let (timeline, value) = tuple
  if let value = value {
    timeline.add(.next(value))
  } else {
    timeline.add(.completed(true))
  }
})
let hostView = setupHostView()
hostView.addSubview(stack)
hostView

Time-shifting operators

Every now and again, you need to travel in time. While RxSwift can’t help with fixing your past relationship mistakes, it has the ability to freeze time for a little while to let you wait until self-cloning is available.

Delayed subscriptions

You’ll start with delaySubscription(_:scheduler:). Since you are now used to creating animated timelines, this page comes with most of the setup code ready. Find the comment Setup the delayed subscription in the source and insert this code below it:

_ = sourceObservable
  .delaySubscription(delay, scheduler: MainScheduler.instance)
  .subscribe(delayedTimeline)

Delayed elements

The other kind of delay in RxSwift lets you time-shift the whole sequence. Instead of subscribing late, the operator subscribes immediately to the source observable, but delays every emitted element by the specified amount of time. The net result is a concrete time-shift.

_ = sourceObservable
  .delay(delay, scheduler: MainScheduler.instance)
  .subscribe(delayedTimeline)

Timer operators

A common need in any kind of application is a timer. iOS and macOS come with several timing solutions. Historically, Timer did the job, but had a confusing ownership model that made it tricky to get just right. More recently, the dispatch framework offered timers through the use of dispatch sources. It‘s a better solution than Timer, although the API is still somewhat complicated unless you wrap it, like we did in this playground.

Intervals

This chapter used DispatchSource several times to create interval timers through a handy custom function. You could replace these instances with RxSwift’s Observable.interval(_:scheduler:) function. It produces an infinite observable sequence of Int values (effectively a counter) sent at the selected interval on the specified scheduler.

let sourceObservable = Observable<Int>
  .interval(.milliseconds(Int(1000.0 / Double(elementsPerSecond))), scheduler: MainScheduler.instance)
  .replay(replayedElements)

One-shot or repeating timers

You may want a more powerful timer observable. You can use the Observable.timer(_:period:scheduler:) operator which is very much like Observable.interval(_:scheduler:) but adds the following features:

_ = Observable<Int>
  .timer(.seconds(3), scheduler: MainScheduler.instance)
  .flatMap { _ in
    sourceObservable.delay(delay, scheduler: MainScheduler.instance)
  }
  .subscribe(delayedTimeline)

Timeouts

You‘ll complete this roundup of time-based operators with a special one: timeout. Its primary purpose is to semantically distinguish an actual timer from a timeout (error) condition. Therefore, when a timeout operator fires, it emits an RxError.TimeoutError error event; if not caught, it terminates the sequence.

let button = UIButton(type: .system)
button.setTitle("Press me now!", for: .normal)
button.sizeToFit()
let tapsTimeline = TimelineView<String>.make()

let stack = UIStackView.makeVertical([
  button,
  UILabel.make("Taps on button above"),
  tapsTimeline])
let _ = button
  .rx.tap
  .map { _ in "•" }
  .timeout(5, scheduler: MainScheduler.instance)
  .subscribe(tapsTimeline)
let hostView = setupHostView()
hostView.addSubview(stack)
hostView
.timeout(5, other: Observable.just("X"), scheduler: MainScheduler.instance)

Challenge

Challenge: Circumscribe side effects

In the discussion of the window(_:scheduler:) operator, you created timelines on the fly inside the closure of a flatMap(_:) operator. While this was done to keep the code short, one of the guidelines of reactive programming is to “not leave the monad”. In other words, avoid side effects except for specific areas created to apply side effects. Here, the “side effect” is the creation of a new timeline in a spot where only a transformation should occur.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now