Local Reasoning in Swift
Swift is an incredibly expressive and powerful programming language. We’re going to explore some of Swift’s language features that can make your code easier to read.
Let’s take a look at a common iOS pattern that prints a message when a button is pressed.
If you’ve done any iOS tutorial, it’s very likely you’ve seen this before. The selector pattern is used throughout
UIKit, and it’s not necessarily bad practice. However, we can do better.
The issue arises when there is a lot of code between the selector declaration and the button’s action. If a developer is reading this for the first time, it’s quite common for them to lose track of what your functions do at a high level.
The two lines of code are loosely coupled. They require the reader to jump between them to fully understand the button.
Let’s try a new approach, using a closure to assign an action to the button.
Don’t worry — this isn’t possible with
UIKit out of the box, but it’s easy to make a
UIButton work like this.
In addition to containing the first snippet in one function, we’ve used a closure to more clearly communicate what happens when a button is tapped. This makes it easier to comprehend what this button does.
Implementing a closure for your button-tapped action is quite straightforward. Create a custom
UIButton subclass with an
action property, which is a closure. We hook up the button’s selectors internally, and execute the closure in the target function.
This solution can expand to include closures for a
UIButton's other actions as well, such as
touchDragExit, and more.
Keeping relevant information together improves local reasoning. Local reasoning is the idea that the reader can make sense of the code directly in front of them, without going on a journey discovering how the code works.
At WWDC 2016, Apple engineers gave a great talk titled “Protocol and Value Oriented Programming in UIKit Apps”, where they discussed the concept of local reasoning. Their presentation covered protocols and how to leverage their benefits over traditional inheritance-based solutions.
It’s hard to call a code organization strategy invalid, as they often vary based on personal preference. However, I am proposing that the best form of code organization is the one that allows the reader to accurately reason about the code while minimizing the distance traveled through the codebase.
Let’s take a look at another common Swift design pattern that focuses on improving local reasoning.
A common practice is to perform setup work in the
viewDidLoad function on a
Like the previous button example, this works just fine.
Notice that we’re split between a stored property declaration and an overridden function. This is the issue we want to focus on — finding all the code related to the view often requires a search.
Instead, we can use a closure to initialize the view.
The closure is marked as
self can be accessed within the closure. This allows the properties to reference any constants declared on
self and set target actions for any controls.
This strategy works with storyboard outlets as well. Instead of using a closure, use the
didSet property observer which is called when the outlet is set by the storyboard.
This is especially useful for view properties that cannot be set in the storyboard like corner radii, gradients, etc. It localizes our changes to the view itself.
Instead of spreading the code throughout our file, we can keep it together in one place.
Protocol Conformance Extensions
Protocols are used throughout the Swift Standard Library and Cocoa Touch API’s. They give a compile-time guarantee that an object has certain properties or functions.
Here is another common pattern, implementing a collection view’s delegate and data source in a view controller.
Once again, this works. However, delegate functions are often quite long, and tend to bloat the size of the controlling object implementing them. It’s hard to tell when a property or function is fulfilling protocol requirements, and which protocol it belongs to.
Instead, let’s declare protocol conformance in an extension to better group related code together.
This not only groups the code for each protocol, but also displays the name of the protocol directly above the implementations. At the type declaration, this decreases cognitive load, since the conformances no longer need to be written after the type name.
Protocol conformances that are not needed to understand what an object does (like
CustomStringConvertible) can be placed after the type’s definition, where more important details about the type need to be understood first.
The implementation of the Swift Standard Library makes heavy use of protocol conformance in extensions since it’s easier to determine which functions belong to which protocol. Take a look at the open-source repo on GitHub and see for yourself.
We looked at three common patterns in Swift code and alternatives that leverage Swift’s features to improve local reasoning.
When writing code, it’s important to think from the perspective of the person reading it. This includes coworkers, teammates, and even your future self. Good local reasoning allows others to understand and modify code more easily, improving its maintainability and flexibility.
While many of these techniques could be considered “code style”, they serve a specific purpose to improve the quality of the code. There are some parallels to user experience design. In this case, the user interface is the text that comprises the code, and the user is the developer. If we take a user-centric approach to writing code, we can make it easier to understand and modify in the future.
Do you have any other places where you could give local reasoning a try in Swift? Let me know in the responses below!