Stas Ivanov
Stas Ivanov

Reputation: 947

ReactiveCocoa 5, ReactiveSwift network sub-request handling & best practice

I'm trying to find best practice to handle multiple sub-requests per each value got from parent request. I'm trying to use the same logic as here - Reactive Cocoa 5 and ReactiveSwift network requests handling, but have some problems.

What we have and need:
1. TableView with infinite scrolling handler (SVPullToRefresh)
2. Fetch list of objects each time handler is called
3. Send "sub-request" for each object from response

Notes:
1. All requests (parent + sub-requests) should become cancelled once viewController is closed (deinit called)
2. I need to have an ability to cancel parent request at any time. This should also cancel all sub-requests.

What I currently have

I know what I do in "infinite handler" is kinda "duct tape", but I'm new with ReactiveSwift...

self.tableView.addInfiniteScrollingWithActionHandler { [unowned self] in
    self.tempMutableProperty.value = true
}

self.tempMutableProperty.producer.skipNil().flatMap(.latest) { [unowned self] tempValueThatIDontNeed in
    return self.producerForParentRequest(offset: self.offset)
        .take(during: self.reactive.lifetime)
        .on(
            // handlers for errors, completed, etc
            value: { [unowned self] items in
                self.items.append(items)
                self.tableView.reloadData()
                self.offset += items.count
                // SEND REQUEST #2 FOR EACH ITEM
            }
    ).flatMapError { error in
        return SignalProducer.empty
    }
}.observe(on: UIScheduler().start()

So, as you see, I have pagination with tableView. I'm fetching list of objects for each page. Then for each item from response I need to fetch an additional information with request #2.


Flow and problems:
1. Of course I want to get rid of tempMutableProperty and somehow start new parent request without some kinda of proxy
2. Each sub-request should be independent, which means I want to have value/error handler called for each sub-request separately, and NOT like it waits for all 10 sub-requests and then call success handler with all 10 responses collected. Also, fail on some specific sub-request should not affect on other sub-requests running
3. User can change his search request without waiting for whole request process beeing completed. This mean that once user changes some parameters, I will clear all items, and I need to cancel parent request within all sub-requests and start this all again.
4. In addition to #2, sometimes user can scroll down to fetch new portion of items. This will mean that new parent request should start, but sub-requests from previous response of parent request should continue working
5. All requests should become cancelled on self.deinit, so this all should work only during self.lifetime, but I'm not sure what is the correct place to put this parameter

I'm not sure if this all is possible without storing disposable/signals as properties of self, so that's not a problem if sub-request will be somehow stored as properties.


Thank you all for your help

Upvotes: 4

Views: 779

Answers (2)

Stas Ivanov
Stas Ivanov

Reputation: 947

So, I will post here solutions for my problems.

For point #1 I've made this:

let disposable = SerialDisposable()

self.tableView.addInfiniteScrolling(actionHandler: { [unowned self] in
    self.disposable.inner = nil // this is needed to force dispose current request before starting new one
    self.disposable.inner = self.producer().take(during: self.reactive.lifetime)
        .on(value: { [unowned self] value in
            // handle as you want
        }).start()
})

This helped me to get rid of tempMutableProperty. Instead of flatMap the SerialDisposable is used. So, this works fine for me, and it disposed request automatically when self is getting destroyed

For other points I've made this:

The idea was that I'm loading some items for table, then I need to send additional request per item to fetch detailed information for it. So, I created class, having 3 properties - item, itemDetail, requestSent.

Then in willDisplay cell I have

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if !self.items[indexPath.row].requestSent {
        self.items[indexPath.row].details <~ self.detailedProducerForItemID(self.items[indexPath.row].item.id)
        self.items[indexPath.row].requestSent = true
    }
}

Note: self.items[indexPath.row].details is a MutableProperty<Details?>

In the cell itself for representing details I have something like:

let (detailsSignal, detailsObserver) = Signal<MutableProperty<Details?>, NoError>.pipe(), where Details is the name of class for item's details.

In cell's awakeFromNib:

let details = self.detailsSignal.flatMap(.latest) { $0.producer }

self.detailsLabel.reactive.text <~ details.map { value -> String? in
    // handling `Details`
}

And in cellForRow I call cell.detailsObserver.send(value: self.items[indexPath.row].details)

And, when VC's deinit is called, or I perform a new main request, all requests are automatically getting cancelled, since when I use self.items.removeAll() it dispose all requests.

This is rough flow. If someone is interested in more details, don't hesitate to ask.

Upvotes: 0

jjoelson
jjoelson

Reputation: 5941

For part 1, I would add an extension that turns the infinite scrolling action handling into a Signal:

extension Reactive where Base: UITableView {
    public func infiniteScrollingSignal() -> Signal<Void, NoError>
    {
        return Signal { [unowned base = self.base] observer in
            base.addInfiniteScrollingWithActionHandler  {
                observer.send(value: ())
            }

            return ActionDisposable {
                // Unsubscribe the infinite scrolling action handler here if necessary
            }
        }
        .take(during: self.lifetime)
    }
}

Then you can hook up all of your logic to self.tableView.reactive.infiniteScrollingSignal()

Upvotes: 0

Related Questions