TDD on Swift: A practical perspective developing Equations
Recently, I started having issues with the lifecycle of some bluetooth devices in my Mac. Since I am an iOS developer, it felt like I could fix the issues myself and learn something on the way. So I began the journey to create my first agent application for Mac. I am still off the SwiftUI train, but I like to code my views. I decided to use some third package to allow a more smooth programmatic constraints generation like Stevia, specifically the equation API. However, this package is not available for macOS! Since a quick search did not return any result, I decided to create my own package (even though it would be dead soon due to SwiftIU).
To create this package, I will use Test Driven Development (TDD) — as a shallow definition, TDD is a programming practice where tests are written first and the actual code is written only to pass such tests. I had never used this approach outside the university, but when I was learning Pharo in this mooc, it was a surprisingly good experience.
Let us write this API!
Equations
In order to generate constraints programmatically, the system provides the following ways:
Equations is a package to programmatically create constraints for your views with a friendlier API than the one is provided by the system.
Equations also forces to group related constraints together, leading to more organized code.
Using TDD
Let’s start the API design by doing the tests first. We want to use the previous block of code as our API. Our test should look like this:
Note that translatesAutoresizingMaskIntoConstraints
is set to false
in the child view in the configuration for the test, because this is outside the scope of our API. The of aim the test is that a constraint is generated when our API is executed.
Now, we need to create enough code so that the previous test passes. First, we need to add an extension for NSView
. This extension expects a completion where the arguments is a structure that proxies for the underlying constraint anchors. These proxy anchors can be equated with the system anchors to generate the constraints. These condition leads to:
Finally, we can define a custom operator ==
that equates a LayoutProxy<Anchor>
with a NSLayoutConstraint
. However, it is better to hide the concrete types under a specific protocol that we control. So, we use the standard technique of defining a protocol with the methods of NSLayoutConstraint
that we want to use.
LayoutAnchor
defines one method which is equal to the one defined in NSLayoutAnchor
, so the extension is straightforward. And with this, we can define the operator
And now the test passes!
Extending the API
The previous API is quite limiting. We can go further by extending it with the rest of the anchors, and interaction with multiplier and constants. Additionally, we can add a constraint generation without activation and a priority setter for each constraint equation. Following TDD, we define a test to capture all these extensions.
Priority and Activation
Let’s start with the method equateConstraint
. This method simply returns the equated constraint without activating it. However, the current operation ==
always activate the generated constraint. At some point, we need to establish that the constrains should no be activated. The only point where this is possible is at the ViewProxy
level, but ==
only knows about LayoutProxy
, so ViewProxy
has to propagate it to the LayoutProxy
level. Note that inside equateConstraint
, we are defining the priority for the specific constraint. This priority setting corresponds to a method of LayoutProxy
.
First, let start by extending NSView
with the new method. equations
and equateConstraint
are the entry point of the API. It is natural to define here the desired behavior. This means that we need to have the active flag here. This forces us to modify ViewProxy
to receive this new flag.
Now, we have to modify the constructor for ViewProxy
and it has to propagate the flag to LayoutProxy
. Also, we can add the rest of the anchors at this point.
Then, we proceed to modify LayoutProxy
to receive the flag and add the priority modifier.
Finally, we can use these constructs at the operation level ==
: lhs
has all the information regarding activation and priority.
These extensions appear naturally when trying to compile the test!
Operation and Anchors
The second extension corresponds to the addition of operations +
and *
, and the addition of width and height anchor. These two anchors are special with respect to the others, because these can be equal to a number or be a multiplier of another width/height anchor. We can use the same technique used before. In other words, we create a protocol that mimics its hierarchy and make them conforms:
Also, we need to add operations +
and *
, but we allow +
for LayoutAnchor
and LayoutDimension
, while *
is only allowed for LayoutDimension
. At this point, ==
must be receive only one parameter in rhs
, so we need to make a design decision. This leads to create a struct
that holds all the relevant information to generate the constraint:
With this struct, we can define the operations as follows:
The structure of these operations is arbitrary, but allows the compiler to resolve the order and to generate a unique LayoutConfiguration
value. In other words, the right part of ==
is equal to a single value of type LayoutConfiguration
. We now can overload ==
with:
Note that we are covering the case where a dimension anchor is equal to a constant. And the tests pass again!
Conclusion
This was a great experience. TDD definitely is something. Using TDD allowed the API to emerge naturally. You can find the repository here:
The repository contains more extensions, like supporting iOS/tvOS and the addition of <=
and >=
.
I can now procede to build the application that I actually need with this package … be careful with procrastination!