Reactive Cocoa 5 and ReactiveSwift network request

2019-06-22 09:32发布

问题:

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!

回答1:

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.