As I mentioned earlier, I feel like the interface that I've sketched out for loading more and more search results from the server isn't very RAC-friendly. Its design doesn't step beyond that of a delegate, honestly.
To start, here's my current representation for a single search result, and for a sublist of the total search results:
struct SearchResultItem {
let contentItem: ContentItem
let topicTreePath: TopicTreePath
var domain: KAContentDomain {
return topicTreePath.domain
}
}
struct SearchResults {
let items: [SearchResultItem]
let total: Int
var hasMore: Bool {
return items.countInConstantTime < total
}
}
A SearchResults
instance has all data needed for display in the list of search results, with the exception of the image for its thumbnail. Those will be loaded separately by tracking visibleSearchResults
, similar to what you did in Your List, or reacting to will-appear or did-disappear callbacks for the cell some result.
My first stab at an interface for loading a longer and longer prefix of search items was:
protocol QuerySearcher {
var results: Signal<SearchLoadingState, QuerySearcherError> { get }
func loadMoreResults();
}
And the loading state is defined as:
enum SearchLoadingState {
// The searcher is loading the first page of results.
case LoadingFirstPage
// The searcher is loading the next page of results. The given SearchResults
// value contains all SearchResultItems loaded so far.
case LoadingNextPage(prevResults: SearchResults)
// The searcher has loaded the next page of results. The SearchResultItems
// for that page are a suffix of the items in the given SearchResults, which
// contains all items for all pages loaded so far.
case Loaded(allResults: SearchResults)
}
So when the user initially displays search:
- The client creates a
QuerySearcher
and invokes itsloadMoreResults
:- A request begins for loading page 1 of results.
- The signal emits
LoadingFirstPage
on its signal.
- Later, the request completes:
- The searcher memoizes the loaded page 1 of search results.
- The signal emits
Loaded
on its signal with the memoized value, i.e. page 1 of results.
Later, when the user scrolls to the bottom of those search results:
- The client invokes
loadMoreResults
again on itsQuerySearcher
:- A request begins for loading page 2 of results.
- The signal emits
LoadingNextPage
on its signal with the memoized page 1 of results asprevResults
.
- Later, the request completes:
- The searcher appends the loaded page 2 of search results to its memoized page 1.
- The signal emits
Loaded
on its signal with the new memoized value, i.e. both pages of search results.
And so on. Again, I am completely aware that the signal is nothing more than a glorified delegate. The one thing about this I do like? The subscriber of the signal can be very dumb: Values can be trivially read of the signal and rendered in a UITableView
(setting aside the issue of loading thumbnail images). The things I don't like? That the side-effect of calling loadMoreResults
and making a new request isn't denoted by a SignalProducer
. That a client can call loadMoreResults()
while a request is in flight, and if this isn't preventable, that the expected behavior doesn't fall out of the type system. That this feels like a bad RAC2 solution instead of a good RAC4 one...
Also, there will be another implementation of QuerySearcher
that loads search results from CoreData instead of the search API endpoint. So any help in making QuerySearcher
robust would be greatly appreciated!
Hey, dude. I conclude with your analysis that this is basically a fancy delegate.
So, first observation: if something doesn’t want to be RAC-y, there’s no reason to artificially try to make it RAC-y! It’s okay to just make objects. The stream-based approach has to solve a problem.
Here, I’d suggest that your proposal does have a real problem: it’s not idempotent. The core method, loadMoreResults(), is inherently an imperative-theory-of-time interface.
What if, instead, the state for the request is as data-driven as the state for the response?
The implementation of
QuerySearcher
can contain a cache which knows how to, say, return the first 25 results when the second 25 are requested.Clients can then get a single flattened signal using
SignalMultiplexer
; see how Nacho used that for a similar "request" model inFeaturedItemView
.