Skip to content

Instantly share code, notes, and snippets.

@mgp
Created November 7, 2015 18:39
Show Gist options
  • Save mgp/a23ba074310ee745b0ab to your computer and use it in GitHub Desktop.
Save mgp/a23ba074310ee745b0ab to your computer and use it in GitHub Desktop.

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 its loadMoreResults:
    • 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 its QuerySearcher:
    • A request begins for loading page 2 of results.
    • The signal emits LoadingNextPage on its signal with the memoized page 1 of results as prevResults.
  • 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!

@andymatuschak
Copy link

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?

protocol QuerySearcher {
  func fetchResultsForQuery(query: String, filters: [Filter], count: Int) -> SignalProducer<SearchResults>
}

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 in FeaturedItemView.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment