[ReactorKit/EN] ReactorKit - Pulse

2022-04-08

Today we’re going to talk about a Pulse in ReactorKit.

Added to version 3.1.0 and partially modified from version 3.2.0, the most recent version of the current (2022.04.08).

3.1.0
… Introduce Pulse 📡 (@tokijh)

3.2.0 Latest

Make public valueUpdatedCount on Pulse by @tokijh in #196

In fact, Pulse is currently being used for in-company projects, and I’m writing this because I’m not sure what this means.I think we can find out one by one.😁

First, let’s look at the documents.

The official document introduces Pulse like this.

Pulse has diff only when mutated To explain in code, the results are as follows.

Well… I see.I didn’t get it.

“shall we look at the code?”

  var messagePulse: Pulse<String?> = Pulse(wrappedValue: "Hello tokijh")

  let oldMessagePulse: Pulse<String?> = message
  message = "Hello tokijh"

  oldMessagePulse != messagePulse // true
  oldMessagePulse.value == messagePulse.value // true

Well… what is it? This looks similar to distinctUntilChanged operator in RxSwift in my think.

and I took the code and ran it in xcode.

image

Well, there’s an error…( An error-free modified code is at the end.)

If so, we’ll have no choice but to look at the following documents:

  // Reactor
  private final class MyReactor: Reactor {
    struct State {
      @Pulse var alertMessage: String?
    }

    func mutate(action: Action) -> Observable<Mutation> {
      switch action {
      case let .alert(message):
        return Observable.just(Mutation.setAlertMessage(message))
      }
    }

    func reduce(state: State, mutation: Mutation) -> State {
      var newState = state

      switch mutation {
      case let .setAlertMessage(alertMessage):
        newState.alertMessage = alertMessage
      }

      return newState
    }
  }

  // View
  reactor.pulse(\.$alertMessage)
    .compactMap { $0 } // filter nil
    .subscribe(onNext: { [weak self] (message: String) in
      self?.showAlert(message)
    })
    .disposed(by: disposeBag)

  // Cases
  reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
  reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
  reactor.action.onNext(.doSomeAction)    // showAlert() is not called
  reactor.action.onNext(.alert("Hello"))  // showAlert() is called with `Hello`
  reactor.action.onNext(.alert("tokijh")) // showAlert() is called with `tokijh`
  reactor.action.onNext(.doSomeAction)    // showAlert() is not called

Looking at the // Cases, perhaps something similar to ‘distinctUntilChanged’ is correct.

//
//  Pulse.swift
//  ReactorKit
//
//  Created by tokijh on 2021/01/11.
//

@propertyWrapper
public struct Pulse<Value> {

  public var value: Value {
    didSet {
      self.riseValueUpdatedCount()
    }
  }
  public internal(set) var valueUpdatedCount = UInt.min

  public init(wrappedValue: Value) {
    self.value = wrappedValue
  }

  public var wrappedValue: Value {
    get { return self.value }
    set { self.value = newValue }
  }

  public var projectedValue: Pulse<Value> {
    return self
  }

  private mutating func riseValueUpdatedCount() {
    self.valueUpdatedCount &+= 1
  }
}

The code for Pulse is as above. Genetic structure and PropertyWrapper has characteristics. If you want to know more about PropertyWrapper, you can look at the official document

Actually, I didn’t get it at first, but the important part is var value and didSet.Every time the value changes, it does something specific. The work is as follows.

  private mutating func riseValueUpdatedCount() {
    self.valueUpdatedCount &+= 1
  }

Whenever the value changes, the count valueUpdatedCount is +1. And if the valueUpdatedCount is UInt.max, we are assigning UInt.min back to the valueUpdatedCount.That’s all. Shall we move on?

//
//  Reactor+Pulse.swift
//  ReactorKit
//
//  Created by 윤중현 on 2021/03/31.
//

extension Reactor {
  public func pulse<Result>(_ transformToPulse: @escaping (State) throws -> Pulse<Result>) -> Observable<Result> {
    return self.state.map(transformToPulse).distinctUntilChanged(\.valueUpdatedCount).map(\.value)
  }
}

If you look at the code above, that’s added a method func pulse as an extension to the Reactor. and used distinctUntilChanged in operator in RxSwift.

The operator is the one that receives the keySelector as a parameter among the four supported by RxSwift.

  public func distinctUntilChanged<Key: Equatable>(_ keySelector: @escaping (Element) throws -> Key)
      -> Observable<Element> {
      self.distinctUntilChanged(keySelector, comparer: { $0 == $1 })
  }

usually use is as follows.

  struct Human {
    let name: String
    let age: Int
  }

  let myPublishSubject = PublishSubject<Human>.init()

  myPublishSubject
    .distinctUntilChanged(\.name)
    .debug()
    .subscribe()
    .disposed(by: disposeBag)

  myPublishSubject.onNext(Human(name: "a", age: 1))
  myPublishSubject.onNext(Human(name: "a", age: 2))
  myPublishSubject.onNext(Human(name: "c", age: 3))

  //-> subscribed
  //-> Event next(Human(name: "a", age: 1))
  //-> Event next(Human(name: "c", age: 3))

So if you summarize it here, Pulse emits events, but only when the values of the variables valueUpdatedCount declared inside change.

So when will the value of valueUpdatedCountchange? As mentioned above, this is when value changes.

The official document of ReactorKit provides additional explanations and examples as below.

Use when you want to receive an event only if the new value is assigned, even if it is the same value. like alertMessage (See follows or PulseTests.swift)

The most important part is if the new value is assigned. That is, the stream does not emit events unless a new value is assigned.

Let’s look at an additional example.

import XCTest
import RxSwift
@testable import ReactorKit

final class PulseTests: XCTestCase {
  func testRiseValueUpdatedCountWhenSetNewValue() {
    // given
    struct State {
      @Pulse var value: Int = 0
    }

    var state = State()

    // when & then
    XCTAssertEqual(state.$value.valueUpdatedCount, 0)
    state.value = 10
    XCTAssertEqual(state.$value.valueUpdatedCount, 1)
    XCTAssertEqual(state.$value.valueUpdatedCount, 1) // same count because no new values are assigned.
    state.value = 20
    XCTAssertEqual(state.$value.valueUpdatedCount, 2)
    state.value = 20
    XCTAssertEqual(state.$value.valueUpdatedCount, 3)
    state.value = 20
    XCTAssertEqual(state.$value.valueUpdatedCount, 4)
    XCTAssertEqual(state.$value.valueUpdatedCount, 4) // same count because no new values are assigned.
    state.value = 30
    XCTAssertEqual(state.$value.valueUpdatedCount, 5)
    state.value = 30
    XCTAssertEqual(state.$value.valueUpdatedCount, 6)
  }

The test is kindly annotated. It says // same count because no new values are assigned..

i.e. the value of valueUpdatedCount is not incremented because we didn’t assign a new value to the value like state.value = 2 , and consequently Pulse will not emit any events.

So, Pulse, how to use it? Again, as kindly described in the documentation, attach @Pulse attribute to State and import it in the same way as reactor.pulse(\.$alertMessage) inside func bind(reactor:).

  struct State {
    @Pulse var alertMessage: String?
  }

  // View
  reactor.pulse(\.$alertMessage)
    .compactMap { $0 } // filter nil
    .subscribe(onNext: { [weak self] (message: String) in
      self?.showAlert(message)
    })
    .disposed(by: disposeBag)

In conclusion, the official document above should be partially revised as below, right?

  var messagePulse: Pulse<String?> = Pulse(wrappedValue: "Hello tokijh")

  let oldMessagePulse: Pulse<String?> = messagePulse
  messagePulse.value = "Hello tokijh" // add valueUpdatedCount +1

  oldMessagePulse.valueUpdatedCount != messagePulse.valueUpdatedCount // true
  oldMessagePulse.value == messagePulse.value // true

Insert messagePulse into oldMessagePulse and assign a new value to the value of the messagePulse.

If you do that, the values of oldMessagePulse and messagePulse are the same, but valueUpdatedCount is +1 as the value is assigned, so the valueUpdatedCount of oldMessagePulse and messagePulse is not the same.

Above, we learned about Pulse in Reactorkit. I was a little confused because I didn’t know what it means to use it, but I hope that people who read this article will find it helpful. 😊

thank you.