Как выполнить многократное приведение результатов действующего действия или начать новое - PullRequest
0 голосов
/ 19 мая 2018

У меня есть следующий сценарий - я использую ReactiveSwift Action для запуска сетевого запроса в моем приложении.Этот сетевой запрос потенциально дорогой из-за обработки, выполняемой в ответе.Итак, когда вызывающий пытается применить действие, я хотел бы сделать следующее:

  • Определить, выполняется ли уже действие
    • Если это так, вернуть SignalProducer, который наблюдаетрезультаты выполняемого действия
    • Если это не так, верните SignalProducer, который применит действие при запуске

В идеале решение будет поточно-ориентированнымвызывающие могут попытаться применить Action из разных потоков.

Теперь мне удалось собрать воедино что-то, что работает, используя примеры кэширования в ReactiveSwift, но я почти уверен, что что-то делаюнеправильно, особенно в том, как я должен сбросить свой MutableProperty на nil, когда Action завершится.Обратите внимание, что я также использую статические переменные, чтобы гарантировать, что мои несколько экземпляров UseCase не смогут обойти мое намеченное поведение.Кроме того, мой пример выводит сигналы Never, но в реальном мире они могут:

class UseCase {
  private static let sharedAction = Action<Void, Never, AnyError> {
    return SignalProducer.empty.delay(10, on: QueueScheduler.main).on(completed: {
      print("Done")
      UseCase.sharedProducer.value = nil
    })
  }
  private static let sharedProducer = MutableProperty<SignalProducer<Never, AnyError>?>(nil)

  func sync() -> SignalProducer<Never, AnyError> {
    let result = UseCase.sharedProducer.modify { value -> Result<SignalProducer<Never, AnyError>, NoError> in
        if let inProgress = value {
          print("Using in progress")
          return Result(value: inProgress)
        } else {
          print("Starting new")
          let producer = UseCase.sharedAction.apply().flatMapError { error -> SignalProducer<Never, AnyError> in
              switch error {
              case .disabled:                   return SignalProducer.empty
              case .producerFailed(let error):  return SignalProducer(error: error)
              }
            }.replayLazily(upTo: 1)

          value = producer
          return Result(value: producer)
        }
    }

    guard let producer = result.value else {
      fatalError("Unexpectedly found nil producer")
    }

    return producer
  }
}

1 Ответ

0 голосов
/ 30 мая 2018

Это также может быть немного длинным, но по крайней мере должно быть немного легче следовать.Не стесняйтесь задавать любые вопросы.

ПРИМЕЧАНИЕ. Я заставил этот объект начать обработку самостоятельно, вместо того, чтобы возвращать SignalProducer, который будет вызывать вызывающая сторона.Вместо этого я добавил свойство только для чтения, которое слушатели могут наблюдать, не запуская обработку.

Я стараюсь сделать своих наблюдателей настолько пассивными, насколько это возможно, что делает их более «реактивными», чем «проактивными».Этот шаблон должен соответствовать вашим потребностям, даже если он немного отличается.

Я пытался включить в этот пример:

  1. Общие результаты из единственной единицыработа.
  2. Обработка ошибок.
  3. Рекомендации по сохранению сигнала.
  4. Общие пояснения в комментариях, поскольку найти хорошие учебники очень сложно.
    • Извините, если я объясняю то, что вы уже знаете.Я не был уверен, сколько предположить, что вы уже знаете.
  5. Задуманная задержка обработки (удалить в рабочем коде).

Это далеко от совершенства, нодолжен предоставить сплошную схему, которую можно изменять и расширять.

struct MyStruct {}

final class MyClass {
    // MARK: Shared Singleton
    static let shared = MyClass()

    // MARK: Initialization
    private init() {}

    // MARK: Public Stuff
    @discardableResult
    func getValue() -> Signal<MyStruct, NoError> {

        if !self.isGettingValue {
            print("Get value")
            self.start()
        } else {
            print("Already getting value.")
        }

        return self.latestValue
            .signal
            .skipNil()
    }
    var latestValue: Property<MyStruct?> {
        // By using a read-only property, the listener can:
        // 1. Choose to take/ignore the previous value.
        // 2. Choose to listen via Signal, SignalProducer, or binding operator '<~'
        return Property(self.latestValueProperty)
    }

    // MARK: Private Stuff
    private var latestValueProperty = MutableProperty<MyStruct?>(nil)

    private var isGettingValue = false {
        didSet { print("isGettingValue: changed from '\(oldValue)' to '\(self.isGettingValue)'") }
    }

    private func start() {
        // Binding with `<~` automatically starts the SignalProducer with the binding target (our property) as its single listener.
        self.latestValueProperty <~ self.newValueProducer()

            // For testing, delay signal to mock processing time.
            // TODO: Remove in actual implementation.
            .delay(5, on: QueueScheduler.main)

            // If `self` were not a Singleton, this would be very important.
            // Best practice says that you should hold on to signals and producers only as long as you need them.
            .take(duringLifetimeOf: self)

            // In accordance with best practices, take only as many as you need.
            .take(first: 1)

            // Track status.
            .on(
                starting: { [unowned self] in
                    self.isGettingValue = true
                },
                event: { [unowned self] event in
                    switch event {
                    case .completed, .interrupted:
                        self.isGettingValue = false
                    default:
                        break
                    }
                }
            )
    }

    private func newValueProducer() -> SignalProducer<MyStruct?, NoError> {
        return SignalProducer<MyStruct?, AnyError> { observer, lifetime in

            // Get Struct with possible error
            let val = MyStruct()

            // Send and complete the signal.
            observer.send(value: val)
            observer.sendCompleted()

            }

            // Don't hold on to errors longer than you need to.
            // I like to handle them as close to the source as I can.
            .flatMapError { [unowned self] error in
                // Deal with error
                self.handle(error: error)

                // Transform error type from `AnyError` to `NoError`, to signify that the error has been handled.
                // `.empty` returns a producer that sends no values and completes immediately.
                // If you wanted to, you could return a producer that sends a default or alternative value.
                return SignalProducer<MyStruct?, NoError>.empty
        }
    }

    private func handle(error: AnyError) {

    }
}

TEST

// Test 1: Start processing and observe the results.
MyClass.shared
    .getValue()
    .take(first: 1)
    .observeValues { _ in
        print("Test 1 value received.")
}

// Test 2: Attempt to start (attempt ignored) and observe the same result from Test 1.
MyClass.shared
    .getValue()
    .take(first: 1)
    .observeValues { _ in
        print("Test 2 value received.")
}

// Test 3: Observe Value from Test 1 without attempting to restart.
MyClass.shared
    .latestValue
    .signal
    .skipNil()
    .take(first: 1)
    .observeValues { _ in
        print("Test 3 value received.")
}

// Test 4: Attempt to restart processing and discard signal
MyClass.shared.getValue()

Вывод:

Get value
isGettingValue: changed from 'false' to 'true'
Already getting value.
Already getting value.

(5 секундпозже)

Test 1 value received.
Test 2 value received.
Test 3 value received.
isGettingValue: changed from 'true' to 'false'
...