Separating Concerns With The Delegate Pattern

Jimmy M Andersson
Jan 12, 2019 · 6 min read

In the previous article, we discussed the syntax of Closures and how they are used inside their receiving functions. Today, we take a look at a technique we can use to enforce separation and extensibility in our application, by making sure that our Views are as decoupled from our Controllers as possible.

What Is A Delegate?

The Delegate Pattern was thought of as a possible solution to deal with Separation of Concerns, the idea that any object should deal with its own, distinct problem set. For example, a Rectangle struct should not deal with calculating the area of circles, and so on. This is a pretty obvious example, but the idea of delegates is that one object should use specialized helper objects to get a task done.

In MVC, the View should be responsible for displaying content to the user. Since the user will interact with the Views by clicking, tapping or pressing on them, they will also have to receive input events. However, it is the Controllers responsibility to actually interpret the input and decide what actions to take to respond to it, and this is where we need to really think about our design.

So, how do we pass the input from our View to our Controller without making one of them too reliant on the implementation of the other? Could we think of the Controller as being a delegate in this aspect?

Let’s Start With The Code

Let’s look at the classes that we are going to use for our experiments. Here is our BaseController, that specifies some nice-to-have functionality for when we want to expand our app:

protocol Controller {
associatedtype ViewType
var typedView: ViewType { get }
}

And here is our BaseView, that does the same type of thing for our Views:

class BaseView: UIView {
convenience init() {
self.init(frame: .zero)
}

override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}

func setupView() {
// Use this method to do any set up that is shared between all your views,
// for example drawing backgrounds, etc.
}
}

The actual Controller and View that we are going to work with are going to start out like this:

class MyController: BaseViewController<MyView> {

}

A wonder of UI design, don’t you agree?

We’ve got a MyController class that will instantiate a MyView object. MyView only contains and displays a button that we are going to use to receive user input.

Getting User Input

Our original problem was that we wanted to receive and respond to user input, but we didn’t want our View to intrude on the Controllers responsibilities, and vice versa. We also want to keep from tightly coupling our two classes, in case we want to replace one of them later on. What if we added the following functionality to MyView?

public func setButtonAction(target: Any?, action: Selector) {
self.button.addTarget(target, action: action, for: .touchUpInside)
}

By doing so, we could call the following bit of code in MyController to set the appropriate response to a button tap:

self.typedView.setButtonAction(target: self, action: #selector(respondToButtonTap(_:)))

This is actually not a terrible solution compared to keeping all the View components in our Controller. We may however run into some weird bugs if we connect several instances to receive signals from the button. If any of those instances are deallocated, the selector will start looking for the next available object to receive that same signal, so the user input may end up making changes to another object than was originally intended. To see what I mean, change the MyController class to the one below and see what happens when you hit the button:

class MyController: BaseViewController<MyView> {
private var counter: Int = 0

override func viewDidLoad() {
self.typedView.setButtonAction(target: self, action: #selector(respondToButtonTap(_:)))
let c = MyController()
self.typedView.setButtonAction(target: c, action: #selector(respondToButtonTap(_:)))
let d = MyController()
self.typedView.setButtonAction(target: d, action: #selector(respondToButtonTap(_:)))
let e = MyController()
self.typedView.setButtonAction(target: e, action: #selector(respondToButtonTap(_:)))
let f = MyController()
self.typedView.setButtonAction(target: f, action: #selector(respondToButtonTap(_:)))
}

@objc func respondToButtonTap(_ sender: UIButton) {
counter += 1
print(counter)
}
}
s
// Tap 1 prints "1 2 3 4 5"
// Tap 2 prints "6 7 8 9 10" and so on

This is a contrived example, but it illustrates what could happen if several Controllers were listed as targets in the same button. It would be a nightmare to hunt down a bug like this if it ever appeared in your code, especially since the Controllers that are initialized in viewDidLoad() are deallocated directly after the method returns.

What About The Delegate Pattern?

What if we changed MyView to look like this:

protocol MyViewActionDelegate: AnyObject {
func buttonWasTapped(_ sender: UIButton) -> Void
}

And MyController to look like this:

class MyController: BaseViewController<MyView>, MyViewActionDelegate {
override func viewDidLoad() {
self.typedView.buttonActionDelegate = self
}

func buttonWasTapped(_ sender: UIButton) {
// Do your thing
}
}

What happens here is that MyView signs up as the receiver of button events, eliminating the risk of registering multiple targets. MyView then delegates the button event to a single action delegate through a well defined protocol. It doesn’t matter if we switch the Controller, as long as it conforms to MyViewActionDelegate, it will be totally fine and only one signal will be sent. Thanks to the optional chaining, it’s guaranteed that the signal will only be sent if buttonActionDelegate holds a reference to another object. If the variable is nil, the signal will be dropped and disregarded in the delegateButtonTap(_:) method.

The main gain in this solution is that it keeps our objects separated and our View unaware of the Controllers existence. It relies solely on the use of a protocol-defined channel that may or may not have anyone listening on the other end. The View won’t care, it will relay the signal to whoever might be there, and move on.

That’s it for this week! Hopefully this discussion sparked a few ideas on how you can use delegates in your own projects.

Feel free to comment if you have questions, and follow to get notifications about future articles.

To learn more about iOS Development, check out my previous articles:

This story is published in The Startup, Medium’s largest entrepreneurship publication followed by +409,714 people.

Subscribe to receive our top stories here.

The Startup

Get smarter at building your thing. Join The Startup’s +725K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store