Don't Reach Up

There are little details in app architecture that may not be obvious, but make a big difference over time. We'll start with modal view controllers, then step back for the larger lesson.

Say you have a view controller that displays the user's profile. There's an "Edit" button that shows a modal editor.

// ProfileController.swift
func didTapEdit(_ sender:UIButton){
  let editor = EditorController()
  present(editor, completion:nil)
}

When you're done editing, You tap a "Save" button inside the editor:

// EditorController.swift
func didTapSave(_ sender:UIButton){
  dismiss(animated:true, completion: nil)
}

According to the docs:

The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.

While self-dismissal is supported-- maybe for convenience-- UIKit tells us it's really the responsibility of the presenter. You may have noticed this the first time you used UIImagePickerController. "Why doesn't the camera close after I take a photo?" A half-hour later, "Oh. I have to dismiss it in the delegate."

Let's adopt the same pattern for our editor, if only because keeping present/dismiss in the same place makes things clearer.

func didTapEdit(_ sender:UIButton){
  let editor = EditorController()
  editor.delegate = self
  present(editor, animated:true)
}

func didFinishEditing(_ editor:EditorController){
  reloadProfile()
  dismiss(animated:true, completion:nil)
}

Our editor now makes fewer assumptions about how it's shown. Will it always be presented modally? What if we decide that on iPad, we should display it side-by-side with the profile, for a realtime preview?

I think it's OK for parent objects to be tightly coupled with children, but a child should be loosely coupled with its parent. Doesn't this code give you a little shiver?

func didTap(_ tap:UITapGestureRecognizer){
  let parent = self.superview! as! ParentView
  parent.isToggled = parent.isToggled!
}

We've shown controllers and views, so let's look at models. Say we have an Account class, which owns a REST API class.

class Account:NSObject {
  var displayName:String!
  var apiClient:RestAPI = RestAPI(self)
  func signRequest(request:NSMutableURLRequest){
    // Attach OAuth header
  }
}

class RestAPI:NSObject {
  unowned var account:Account
  init(account:Account){
    self.account = account
    super.init()
  }
  func fetchHomeTimeline(response: (results:NSArray) -> ()){
    let request = requestForHomeTimeline()
    self.account.signRequest(request)
    // rest of code
  }
}

We assume requests will always come from an account, but almost every app has a logged-out experience. Do we create a fake "Logged Out" user?

class LoggedOutAccount:Account {
  override var displayName:String! {
    return "Fake User"
  }
  override func signRequest(request:NSMutableURLRequest){
    // Special logged-out logic
  }
}

This should feel wrong. You're probably sharing view controllers between the logged-in and logged-out experience, so it's only a matter of time before you accidentaly expose a "Fake User" label in the UI.

When we write unit tests for authentication, we have to build a fake account just for firing requests. Maybe mocks will get involved. Don't get me started on mocks.

Instead, consider this:

class Account:NSObject, Authenticator {
  var apiClient:RestAPI = RestAPI(self)
  func signRequest(request:NSURLRequest){
    // Attach auth header
  }
}

protocol Authenticator:class {
  func signRequest(request:NSMutableURLRequest)
}

class RestAPI:NSObject {
  unowned var authenticator:Authenticator

  init(authenticator:Authenticator){
    self.authenticator = authenticator
    super.init()
  }

  func fetchHomeTimeline(response: (results:NSArray) -> ()){
    let request = requestForHomeTimeline()
    self.authenticator.signRequest(request)
    // rest of code
  }
}

That feels better.

Whether it's view hierarchies or vanilla object composition, Always reach down. Never reach up. You can send messages up, but don't making assumptions about who you're sending it to, and never modify your parent's state.

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