5

I'm trying to figure out if network requests handling can be implemented suitable to my needs using ReactiveSwift and RAC5.

Under topic Migrate from RACSignal to ReactiveSwift or RAC5 I was told it can be done with SignalProducer, but digging deeper into it didn't give me expected results

So, I would want to have:
1. Each time text changes in textField send request (search by keyword).
2. Once user closes current ViewController, the current request should be cancelled automatically
3. Have an ability to cancel request once keyword is changed

Here is what I have

self.textField.reactive.continuousTextValues.skipNil().filter({ (value) -> Bool in
        return value.characters.count > 0
    }).observeValues { [unowned self] (value) in
        self.fetchSignalDisposable?.dispose()
        self.fetchSignal = self.producerFor(keyword: value).on(started: {
            print("started")
        }, failed: { (error) in
            print("error")
        }, completed: {
            print("completed")
        }, value: { [unowned self] (items) in
            print("value")
            self.items.append(contentsOf: items)
            self.tableView.reloadData()
        })
        self.fetchSignalDisposable = self.fetchSignal!.start()
    }

And here is producer initializer

return SignalProducer<Any, NSError> { (observer, disposable) in
        let task: URLSessionDataTask? = NetworkClient.fetchRequestWith(uri: "test", parameters: ["keyword" : keyword], success: { response in
            observer.send(value: response)
            observer.sendCompleted()
        }, failure: { error in
            observer.send(error: error)
            observer.sendCompleted()
        })
        disposable += {
            task?.cancel()
        }
    }

Notes:
1. Sometimes I want to have kinda "both handler block" that would be called on both success and errors, so stuff like hiding loading indicators can be done under that block.

Few problems/questions here:
1. Once I close VC (dismiss action) observeValue handler is called one more time. It can be fixed by adding .skipRepeats(), but I guess it is just a workaround and not an exact solution. I would want to not have this observer active anymore, if I close VC
2. completed block not called in case of error, even if I call it manually right after calling send(error: error)
3. If request is still loading and I close VC, it is not getting disposed automatically, which looks strange to me. I thought dispose block will be called automatically once viewController lose reference to signalProducer. Even calling self.fetchSignalDisposable?.dispose() in deinit method of VC doesn't cancel request. It still finishes request and calls value handler which leads to crash with Bad Access error

My personal needs are:
1. Have some kind of "both" block that will be called after both success and failed cases of request
2. All observers for textFields' text values must be removed and not be active anymore once I close VC
3. Network request must be cancelled right when I close VC

P.S.: Of course, thanks to everyone who read this huge post and spent time helping me!

Community
  • 1
  • 1
Stas Ivanov
  • 917
  • 1
  • 10
  • 22
  • Thanks to jjoelson so much. We have found solution that works and covers all cases that I needed. You can find the final version in the chat - [link](http://chat.stackoverflow.com/rooms/142151/discussion-between-stas-ivanov-and-jjoelson) – Stas Ivanov Apr 20 '17 at 13:29

1 Answers1

5

The "Making network requests" example from the ReactiveSwift readme is a good example for this type of thing. Rather than using observeValues on your text field signal, typically you would use .flatMap(.latest) to hook it up directly to your SignalProducer like so (note I haven't checked this code, but hopefully it gets the idea across):

self.textField.reactive.continuousTextValues
    .skipNil()
    .filter { (value) -> Bool in
        return value.characters.count > 0
    }
    .flatMap(.latest) { [unowned self] value in
        return self.producerFor(keyword: value)
            // Handling the error here prevents errors from terminating
            // the outer signal. Now a request can fail while allowing
            // subsequent requests to continue.
            .flatMapError { error in
                print("Network error occurred: \(error)")
                return SignalProducer.empty
            }
    }
    .observe(on: UIScheduler())
    .observe { [unowned self] event in
        switch event {
        case let .value(items):
            print("value")
            self.items.append(contentsOf: items)
            self.tableView.reloadData()

        case let .failed(error):
            print("error")

        case .completed, .interrupted:
            print("completed")
        }
    }

Specifying .latest causes the previous network request to automatically be cancelled when a new one starts, so there's no need to keep track of the current request in a global variable.

As for managing lifetime, it's hard to say what the best thing is without knowing your broader code structure. Typically I would add something like .take(during: self.reactive.lifetime) to my signal to terminate the subscription when self is deallocated, probably right before the call to observe.

Error events terminate signals. There's no need to send a completed event after an error, and observers won't see it anyway. Basically, complete indicates that the signal terminated successfully while an error indicates that the signal terminated in failure.

jjoelson
  • 5,771
  • 5
  • 31
  • 51
  • thanks for response. I've tried to "play" with it a lot, but still cannot solve my first 2 problems. The problem is that "flatMap" block is still called one more time when I close the VC. Probably that's because "continuousTextValues" handles all events (I hide keyboard on close), so maybe this cannot be fixed without adding "skipRepeats()". Also, with observing event, ".completed" happens only when VC is getting disposed... So I still cannot find a way to have smth like "both" block, that would be called after both value and fail blocks... But I like that it cancels requests automatically. – Stas Ivanov Apr 19 '17 at 15:40
  • 1
    Using `skipRepeats()` seems fine to me. As for the other problem, you can put arbitrary code in your `observe` closure. Add a second switch and check for `case .value, .failed:`, and you can run whatever code you need there. – jjoelson Apr 19 '17 at 15:46
  • In addition, it doesn't cancel current request on VC's deinit action, even if I add "take(during: self.reactive.lifetime)". Maybe I don't understand how that works, but I would want to have the latest request automatically cancelled on VC destroy too, so I won't have cases when success block is called for destroyed self. – Stas Ivanov Apr 19 '17 at 15:48
  • Where did you put the `take(during:)` call? As long as you have `.observe(on: UIScheduler())` and the VC deallocation happens on the UI thread, I don't think the observe closure should ever be called after `self` is deallocated. – jjoelson Apr 19 '17 at 15:55
  • I have `continuousTextValues.take(during: self.reactive.lifetime)`. But have tried with having `.observe(on: UIScheduler())` right before the the last observe (event), and have the same result. If app gets network response after `deinit`, observe closure still is called, but it crashes of course since self is nil. I know I can use workaround here, but that's not an elegant solution. – Stas Ivanov Apr 19 '17 at 16:03
  • 1
    Hmm, maybe `take(during:)` needs to be between `observer(on:)` and `observer`. – jjoelson Apr 19 '17 at 16:07
  • Works!!! `take(during:)` between `observer(on:)` and `observer` makes it call `completed` right after `deinit` and then it cancels current request. I thought it doesn't matter the order of calling those functions in a row. Maybe I need to read more about it, but documentation is huge and don't cover a lot of examples (if I have tight deadline). Thank you very much again – Stas Ivanov Apr 19 '17 at 16:10
  • Great! There's one more issue with my answer with error handling. I'm updating the answer in a moment... – jjoelson Apr 19 '17 at 16:13
  • OK, I updated with some additions about error handling on the producer. – jjoelson Apr 19 '17 at 16:20
  • hmm... It seems like this will not allow me to have 1 closure for handling both value and fail cases, which is not acceptable for me. Sorry, I still don't understand what I'm observing on. On signalProducer itself returned in `flatMap ` closure or for something else created by other methods calls in a row? How can I implement it to have single closure for success/fail cases, and have it continue working after fail anyway? – Stas Ivanov Apr 20 '17 at 09:25
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/142151/discussion-between-stas-ivanov-and-jjoelson). – Stas Ivanov Apr 20 '17 at 09:25