The API Veneer

As you flesh out an app, the first place you dump behavior is the controller. As Colin Campbell said, "iOS architecture, where MVC stands for Massive View Controller".

Say we build an app that displays recent popular news stories:

override func viewWillAppear(animated:Bool) {
  super.viewWillAppear(animated)
  let endpoint = "https://api.example.com/stories"
  let url = URL(string:endpoint)
  let task = URLSession.shared.dataTaskWithURL(url) {
    (data, response, error) in 
    let dictionary = try! JSONSerialization.JSONObjectWithData(data,
                            options: nil) as! NSDictionary
    DispatchQueue.main.async {
      self.storiesArray = dictionary["stories"] as? NSArray
      self.storiesTableView.reloadData()
    }
  }
  task.resume()
}

That raw string for a URL should be a hint something's wrong. Your first inclination might be to cut the noise: turn those strings to constants and refactor the HTTP boilerplate into helper methods.

let StoriesEndpoint = "https://api.example.com/stories"

override func viewWillAppear(animated:Bool) {
  super.viewWillAppear(animated)  
  let url = NSURL(StoriesEndpoint)
  self.fetchDataFromURL(url) { (data, error) in 
    let dictionary = try! self.parseDataToDictionary(data)
    DispatchQueue.main.async {
      self.storiesArray = dictionary["stories"] as? NSArray
      self.storiesTableView.reloadData()
    }
  }
}

That clears things up a bit, but don't be fooled by brevity. It doesn't address the underlying problem.

What happens when several API endpoints require similar behavior? Say our PM comes up with a "Social Stories" screen. It's similar to the existing newsfeed, but fetches the top stories in our social circle. Hmm. Let's refactor that controller into a base class, StoriesViewController, with PopularStoriesController and SocialStoriesController subclasses.

Subclasses have their place, but Cocoa embraces the delegate pattern for a reason. Subclassing gets crazy fast, and here's a bad sign:

var apiEndpoint:String! {
  fatalError("Override storiesEndpoint")
  return nil
}

What happens when your PM decides to combine a news feed combined with a messaging feed? You can't inherit from both StoriesViewController and MessagesViewController. So you create a NetworkViewController, and the two inherit from that. Stop. Just stop.

The underlying problem is that your REST API logic doesn't belong in a view controller.

Once I pass the proof-of-concept phase in an app, the first class I break out is the API client. Then my view controllers looks something like this:

override func viewWillAppear(animated:Bool) {
  super.viewWillAppear(animated)
  CloudAPI.sharedAPI.fetchStories(.Popular) {
    (storiesOrNil, errorOrNil) in
    self.storiesArray = storiesOrNil
    self.storiesTableView.reloadData()
  }
}

This is very clear to follow. Semantically, we're asking for some data, and it's going to return asynchronously. We expose as little to our view controller as we can get away it. The URL doesn't matter. It doesn't matter if we're using HTTP, protocol buffers, or Gopher.

Pop quiz for the RESTifarians: if you do a search for News Stories, and there are no results, should it return a status 200 or 404? Whatever you pick, your API team picked the opposite. Don't worry, it'll change in six months when they read the Medium post, "I Switched Status Codes and Now I'm 10x More Productive." An API client can isolate you from bikeshedding to focus on content.

The architecture is looking strong from an academic perspective, too. Favoring composition over inheritance, it's going to be easier to mix and match API calls across view controllers. When we write tests, it's much easier to mock things out on a class you own than to patch NSURLSession.

Ok, things are looking great, but what happens when we want to load newer items?

func didTapRefresh(button:UIButton) {
  let newestID = self.stories[0]["positionID"]
  CloudAPI.sharedAPI.fetchStories(.Popular, from:newestID) {
    (latestStoriesOrNil, errorOrNil) in
    if let latestStories = latestStoriesOrNil {
      self.storiesArray.appendContentsOf(latestStories)
      self.storiesTableView.reloadData()
    }
  }
}

We're no longer dealing with HTTP, but details about interacting with our API are creeping back into our view controller. Let's abstract things a little further.

var storiesResource:StoriesResource {
  didSet {
    storiesResource.delegate = self
  }
}

func didTapRefresh(button:UIButton) {
  storiesStream.loadNewer()
}

func didUpdateStories(resource: StoriesResource){
  self.storiesArray = storiesResource.stories
  self.storiesTableView.reloadData()
}

Suddenly, the only job of our controller is to receive events from our views and dispatch them to our model. Like the fancy diagrams in our textbooks.

For now we can move back to a single StoryViewController class. It doesn't matter if you assign it a PopularStoriesResource or a SocialStoriesResource. In fact, those subclasses may not even exist; StoriesResource might just be a protocol.

It's powerful having models for business logic that are presentation agnostic. Now you can trigger loadNewer() on an iPhone via Pull-To-Refresh, while on Apple Watch you use a 3D-Touch menu, while on Apple TV you shake your Siri Remote.

MVC doesn't have to stand for "Massive View Controller". Anybody can copy-and-paste some sample code to fetch a URL. The job of a software engineer is to look beyond the API request, and design a small world of actors, while somehow maintaining simplicity.

Subscribe to Sandofsky

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe