UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How to move data sources and delegates out of your view controllers

Slim down your view controllers the smart way

Paul Hudson       @twostraws

Part 2 in a series of tutorials on fixing massive view controllers:

  1. How to use the coordinator pattern in iOS apps
  2. How to move data sources and delegates out of your view controllers
  3. How to move view code out of your view controllers

One of the easiest ways to create messy and confused view controllers is to ignore the single responsibility principle – that each part of your program should be responsible for one thing at a time.

A good sign that you’re ignoring this principle is writing code like this:

class MegaController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIPickerViewDataSource, UIPickerViewDelegate, UITextFieldDelegate, WKNavigationDelegate, URLSessionDownloadDelegate {

If I asked you what that view controller does, could you answer without having to pause for breath?

I’m not saying you must make everything do precisely one thing – sometimes sheer pragmatic development will stop that from being the case, as you’ll see soon.

However, there’s no reason that view controller should act as so many delegates and data sources, and in fact doing so makes your view controllers less composable and less reusable. If you split off those protocols into separate objects you can then re-use those objects in other view controllers, or use different objects in the same view controller to get different behavior at runtime – it’s a huge improvement.

In this article I want to walk you through examples of getting common data sources and delegates out of view controllers in a way you should be able to apply to your own projects without much hassle.

Before we begin, please use Xcode to create a new iOS app using the Master-Detail App template. This creates a pretty disastrous app template for a number of reasons, and it’s a thoroughly shaky foundation to use for any of your own work.

I could write many articles about fixing the problems it contains, but here we’re going to do the least amount of work required to fix two of its problems: the main view controller acts as its table view’s data source and delegate.

  • This article contains some content from my book Swift Design Patterns – it teaches you how to write better MVC code, but also goes into a huge amount of depth on why things work the way they do and how you can adopt time-tested techniques in your own projects.
Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Splitting off the data source

Apple’s default template has code in MasterViewController.swift to make that act as the table view delegate. While this is fine for simple apps or while you’re learning, for serious apps you should always (always) split this off into its own class that can be then be re-used as needed.

The process here is quite simple, so let’s walk through it step by step.

First, go to the File menu and choose New > File. Select Cocoa Touch Class from the list that Xcode offers you, then press Next. Make it a subclass of NSObject, give it the name “ObjectDataSource”, then click Next and Create.

Note: I’ve called it “ObjectDataSource” because Apple’s template code gave us var objects = [Any]() for the app data. This is one of many crimes that we won’t be fixing here.

The next step is to move all the table view data source code from MasterViewController.swift into ObjectDataSource.swift. So, select all this code and cut it to your clipboard:

// MARK: - Table View

override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return objects.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

    let object = objects[indexPath.row] as! NSDate
    cell.textLabel!.text = object.description
    return cell
}

override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    // Return false if you do not want the specified item to be editable.
    return true
}

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        objects.remove(at: indexPath.row)
        tableView.deleteRows(at: [indexPath], with: .fade)
    } else if editingStyle == .insert {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
    }
}

None of that has any business being in the view controller, so open ObjectDataSource.swift and paste it inside that class instead.

We need to make three small changes to ObjectDataSource before we can use it:

  1. Remove override from all the method definitions. This was required in our view controller because we inherited from UITableViewController, but now we don’t.
  2. Make the class conform to UITableViewDataSource by adding that next to NSObject like this: class ObjectDataSource: NSObject, UITableViewDataSource {.
  3. Move var objects = [Any]() from being a property on MasterViewController to being a property on ObjectDataSource.

That completes ObjectDataSource, but leaves problems inside MasterViewController because it’s trying to refer to an objects array it no longer has.

To fix this we must make two changes inside MasterViewController: give it a data source property using our new ObjectDataSource class, then refer to that data source wherever objects is used.

First, open MasterViewController.swift and give the class this new property:

var dataSource = ObjectDataSource()

Second, change the two references to objects to be dataSource.objects. That means changing insertNewObject() to this:

dataSource.objects.insert(NSDate(), at: 0)

And changing the prepare() method to this:

let object = dataSource.objects[indexPath.row] as! NSDate

Yes, I know; Apple’s template code here is really poor, but remember we’re trying to do the least amount of work required to fix our two problems.

At this point the code compiles cleanly, but it doesn’t work yet. For that we need one last change inside the viewDidLoad() method of MasterViewController. Add this line:

tableView.dataSource = dataSource

That tells the table view to load its data from our custom data source, and now the app will be back to the same state where it started. The difference is that the view controller has come down from 84 lines of code to 54 lines of code, plus you can now use that data source elsewhere.

This is definitely an improvement, although in practice you would probably want to move the data model out to your coordinator if you’re using one, or perhaps leave it in the view controller if that’s where you handle data fetching.

Splitting the delegate

The single responsibility principle helps us design apps in small, simpler parts that can then be combined together to make more complex components. However, as I said earlier sometimes being a pragmatic developer will make you take a different route, and I want to discuss that briefly before moving on.

You’ve seen how it’s pretty straightforward to get table view data sources out into their own object, so you might very well think we’ll create another object to be the table view delegate. However, this is more problematic for two reasons:

  1. The delegate usually needs to talk to the data source in order to do anything. For example, when a cell is tapped you need to look into the data source to figure out what that means.
  2. The divide between UITableViewDataSource and UITableViewDelegate is bizarre and seemingly arbitrary. For example, the data source has titleForHeaderInSection whereas the delegate has viewForHeaderInSection and heightForRowAt.

This means splitting UITableViewDelegate into its own class can be fraught with difficulties. As a result, I often see two solutions:

  1. Merge your UITableViewDataSource and UITableViewDelegate handling into a single class. This goes against the single responsibility principle, but if it avoids spaghetti code that’s the bigger win.
  2. Leave the delegate code inside your view controller. This isn’t necessarily a bad solution as long as the method implementations are trivial – nothing more than returning values or calling up to the coordinator. If you find yourself adding business logic you should re-think.

Which you prefer depends on your personal style, but in my own projects I prefer to keep my view controllers as simple as possible. That means they handle view lifecycle events (viewDidLoad(), etc), store some @IBOutlets and @IBActions, and occasionally to handle model storage depending on what I’m doing.

Remember: the goal here is to make your app design simpler, more maintainable, and more flexible – if you’re adding complexity just to stick to a principle, you’ll end up with problems.

Easier delegates

Although UITableViewDataSource and UITableViewDelegate are tricky to separate cleanly, not all delegates are like that. Instead, many delegates are easy to carve off into separate classes, and in doing so you’ll immediately benefit from the same kinds of reusability you already saw.

Let’s look at a practical example: you want to embed a WKWebView that enables access to only a handful of websites that have been deemed safe for kids. In a naïve implementation you would add WKNavigationDelegate to your view controller, give it a childFriendlySites array as a property, then write a delegate method something like this:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if let host = navigationAction.request.url?.host {
        if childFriendlySites.contains(where: host.contains) {
            decisionHandler(.allow)
            return
        }
    }

    decisionHandler(.cancel)
}

(If you haven’t used contains(where:) before, you should really read my book Pro Swift.)

To reiterate, that approach is perfectly fine when you’re building a small app, because either you’re just learning and need momentum, or because you’re building a prototype and just want to see what works.

However, for any larger apps – particularly those suffering from massive view controllers – you should split this kind of code into its own type:

  1. Create a new Swift class called ChildFriendlyWebDelegate. This needs to inherit from NSObject so it can work with WebKit, and conform to WKNavigationDelegate.
  2. Add an import for WebKit to the file.
  3. Place your childFriendlySites property and navigation delegate code in there.
  4. Create an instance of ChildFriendlyWebDelegate in your view controller, and make it the navigation delegate of your web view.

Here’s a simple implementation of just that:

import Foundation
import WebKit

class ChildFriendlyWebDelegate: NSObject, WKNavigationDelegate {
    var childFriendlySites = ["apple.com", "google.com"]

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if let host = navigationAction.request.url?.host {
            if childFriendlySites.contains(where: host.contains) {
                decisionHandler(.allow)
                return
            }
        }

        decisionHandler(.cancel)
    }
}

That solves the same problem, while neatly carving off a discrete chunk from our view controller. But you can – and should – go a step further, like this:

func isAllowed(url: URL?) -> Bool {
    guard let host = url?.host else { return false }

    if childFriendlySites.contains(where: host.contains) {
        return true
    }

    return false
}

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if isAllowed(url: navigationAction.request.url) {
        decisionHandler(.allow)
    } else {
        decisionHandler(.cancel)
    }
}

That separates your business logic (“is this website allowed?”) from WebKit, which means you can now write tests without trying to mock up a WKWebView. I said it previously but it’s worth repeating: any controller code that encapsulates any knowledge – anything more than sending a simple value back in a method – will be harder to test when it touches the user interface. In this refactored code, all the knowledge is stored in the isAllowed() method, so it’s easy to test.

This change has introduced another, more subtle but no less important improvement to our app: if you want a child’s guardian to enter their passcode to unlock the full web, you can now enable that just by setting webView.navigationDelegate to nil so that all sites are allowed.

The end result is a simpler view controller, more testable code, and more flexible functionality – why wouldn’t you carve off functionality like this?

Where next?

As I said at the beginning of this article, Apple’s Master-Detail App template is a pretty disastrous foundation for any real work, but in this article I’ve shown you how we can chip away at some of the rot to make the view controller simpler.

If you found this article interesting then you should definitely read my book Swift Design Patterns – it’s packed with similar tips for ways to simplify, streamline, and uncouple your components.

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.