This article is also available in Chinese.
This the going to be the first post in a series on Advanced Coordinators. If you haven’t read the original post or the longer follow-up, make sure to check those out first. The series will cover a few advanced coordinator techniques, gotchas, FAQs, and other trivia. Let’s dig in.
I’m often asked how to migrate an app from using storyboards or per-view controller code-based flow to an app using coordinators. When done right, this refactoring can be done piecemeal. You will continuously be able to deploy your app, even if the refactoring isn’t complete.
To acheive this, the best thing to do is start from the root, which for coordinators is called the “app coordinator”. The app delegate holds on to the app coordinator, which is the coordinator that sets up all the view controllers for your app’s launch.
To understand why we start from the root of the app, consider the opposite. If we started from some leaf flow (like, say, a CheckoutCoordinator
), then something needs to maintain a strong reference to the coordinator so that it doesn’t deallocate. If the coordinator deallocates, none of its code can run. So, deep in an app, if we create a coordinator, something will have to hold on to it.
There are two ways to prevent this deallocation. The first option is to make a static reference. Because there will likely only ever be one CheckoutCoordinator
, it’ll be easy to stuff it in to a global variable. While this works, this isn’t an ideal choice, since globals hinder testability and flexibility. The second option is to have the presenting view controller maintain a reference to the coordinator. This will force a little complexity onto the presenting view controller, but will allow us to remove more complexity from all the view controllers that are managed by that coordinator. However, this relationship is fundamentally flawed. View controllers are usually “children” to coordinators, and when programming, children shouldn’t know who their parents are. I would liken this to a UIView
having a reference to a UIViewController
: it shouldn’t happen.
If you have a situation where you’ve decided that you absolutely must start with some child flow in your app, then you can make it work with one of the two methods above. However, if you have the power to start from the root, that’s my recommendation.
One other benefit to starting from the root is that the authentication flow is often close to the root of the app. Authentication is a great flow to isolate away into its own object, and a nice testbed for proving coordinators in your app.
Once you’ve moved the root view controller of the app to your AppCoordinator
, you can commit/pull request/code review/etc the code. Because every other view controller transition continues to work, the app will still be fully functional in this halfway state. At this point, working one-by-one, you can start to move more view controller transitions over to the coordinator. After each “flow step” is moved to your coordinator, you can commit or make a pull request, since the app will continue to work. Like the best refactorings, each of these steps are mostly just moving code around, sometimes creating new coordinators as needed.
Once all of your transitions have been moved over to coordinators, you can do further refactorings, like separating iPhone and iPad coordinators into individual objects (instead of one coordinator that switches on some state), making child flows reusable, and better dependency injection, all of which are enabled by your new architecture.
Swift is commonly described as a “safe” language. Indeed, the About page of swift.org says:
Swift is a general-purpose programming language built using a modern approach to safety, performance, and software design patterns.
and
Safe. The most obvious way to write code should also behave in a safe manner. Undefined behavior is the enemy of safety, and developer mistakes should be caught before software is in production. Opting for safety sometimes means Swift will feel strict, but we believe that clarity saves time in the long run.
Fast. Swift is intended as a replacement for C-based languages (C, C++, and Objective-C). As such, Swift must be comparable to those languages in performance for most tasks. Performance must also be predictable and consistent, not just fast in short bursts that require clean-up later. There are lots of languages with novel features — being fast is rare.
Expressive. Swift benefits from decades of advancement in computer science to offer syntax that is a joy to use, with modern features developers expect. But Swift is never done. We will monitor language advancements and embrace what works, continually evolving to make Swift even better.
For example, when working with things like the Optional
type, its clear how Swift increases safety. Before, you would never know which variables could be null and which couldn’t. With this new nullability information, you’re forced to handle the null case explicitly. When working with these “nullable” types, you can opt to crash, usually using an operator that involves an exclamation point (!
). What is meant by safety here is apparent. It’s a seatbelt that you can choose to unbuckle, at your own risk.
However, in other cases, the safety seems to be lacking. Let’s take a look at an example. If we have a dictionary, grabbing the value for some given key returns an optional:
let person: [String: String] = //...
type(of: person["name"]) // => Optional<String>
But if we do the same with an array, we don’t get an optional:
let users: [User] = //...
type(of: users[0]) // => User
Why not? The array could be empty. If the users
array were empty, the program would have no real option but to crash. That hardly seems safe. I want my money back!
Well, okay. Swift has an open development process. Perhaps we can suggest a change to the swift evolution
mailing list, and—
Nope, that won’t work either. The “commonly rejected” proposals page in the swift-evolution
GitHub repo says that they won’t accept such a change:
- Make
Array<T>
subscript access returnT?
orT!
instead ofT
: The current array behavior is intentional, as it accurately reflects the fact that out-of-bounds array access is a logic error. Changing the current behavior would slowArray
accesses to an unacceptable degree. This topic has come up multiple times before but is very unlikely to be accepted.
What gives? The stated reason is that speed is too important in this particular case. But referring back to the About page linked above, “safe” is listed as a description of the language before “fast”. Shouldn’t safety be more important than speed?
There is a fundamental contention here, and the solution lies in the definitions of the word “safe”. While the common understanding of “safe” is more or less “doesn’t crash”, the Swift core members usually use the same word to mean “will never access incorrect memory unintentionally”.
In this way, Swift’s Array
subscript is “safe”. It’ll never return data in memory beyond the bounds allocated for the array itself. It will crash before giving you a handle on memory that doesn’t contain what it should. In the same way that the Optional type prevents whole classes of bugs (null dereferencing) from existing, this behavior prevents a different class of bugs (buffer overflows) from existing.
You can hear Chris Lattner make this distinction at 24:39 in his interview with ATP:
We said the only way that this can make sense in terms of the cost of the disruption to the community is if we make it a safe programming language: not “safe” as in “you can have no bugs,” but “safe” in terms of memory safety while also providing high performance and moving the programming model forward.
Perhaps “memory-safe” is a better term than just “safe”. The idea is that, while some application programmers might prefer getting back an optional instead of trapping on out-of-bounds-array access, everyone can agree that they’d prefer to crash their program rather than let it continue with a variable that contains invalid data, a variable that could potentially be exploited in a buffer overflow attack.
While this second tradeoff (crashing instead of allowing buffer overflows) may seem obvious, some languages don’t give you this guarantee. In C, accessing an array out-of-bounds gives you undefined behavior, meaning that anything could happen, depending on the implementation of the compiler that you were using. Especially in cases when the programmer can quickly tell that they made a mistake, such as with out-of-bounds array access, the Swift team has shown that they feel like this is an acceptable place to (consistently!) crash, instead of returning an optional, and definitely instead of returning junk memory.
Using this definition of “safe” also clarifies what the “unsafe” APIs are designed for. Because they muck about in memory directly, the programmer herself has to take special care to ensure that she’ll never allow access to invalid memory. This is extremely hard, and even experts get it wrong. For an interesting read on this topic, check out Matt Gallagher’s post on bridging C to Swift in a safe fashion.
Swift and the core team’s definition of “safe” may not line up 100% with yours, but they do prevent classes of bugs so that programmers like you don’t have to think about them day-to-day. It can often help to replace their usage of “safe” with “memory safe” to help understand what they mean.
This article is also available in Chinese.
One often-misused piece of the Swift standard library is Sequence’s enumerated()
function. This function give you a new sequence with each element in the original sequence and a number that corresponds to that element.
enumerated()
is misunderstood. Because it provides a number with each element, it can be an easy solution for a number of problems. However, most of those problems are solved better with other techniques. Let’s go over some of those cases, highlight why they’re flawed, and go over better abstractions for solving them.
The primary issue with using enumerated()
is that people think it returns each index and its element, but that’s not quite what’s going on. Because it’s available on every Sequence, and sequences aren’t even guaranteed to have indexes, we know they’re not indexes. Within the code, the variable is called offset
, not index
, which is another clue that we’re not dealing with indexes. The offset is always an integer, starting from 0, increasing by one for each element. For Array
, this happens to correspond to the index, but for basically every other type, it doesn’t. Let’s take a look at an example.
let array = ["a", "b", "c", "d", "e"]
let arraySlice = array[2..<5]
arraySlice[2] // => "c"
arraySlice.enumerated().first // => (0, "c")
arraySlice[0] // fatalError
Our variable arraySlice
here is, unsurprisingly, an ArraySlice
. However, its startIndex
is notably 2, not 0. But when we call enumerated()
and first
on it, it returns a tuple with an offset of 0 and its first element, “c”.
Put another way, you think you’re getting
zip(collection.indices, array)
but you’re actually getting
zip(0..<collection.count, array)
and this will result in wrong behavior anytime you’re working with anything other than an Array
.
Besides the fact fact you’re getting an offset rather than an index, there are other issues with using enumerated()
as well. If you’re tempted to use enumerated()
, there might be a better abstraction to take advantage of. Let’s take a look at a few.
The most common use I see of of enumerated()
is using the offset from one enumerated array to grab a corresponding element from of another array.
for (offset, model) in models.enumerated() {
let viewController = viewControllers[offset]
viewController.model = model
}
While this code works, it relies on both models
and viewControllers
being arrays, indexed by integers, starting from 0. It also relies on each array having the same length. If the models
array is shorter than the viewControllers
array, nothing bad will happen, but if the viewControllers
array is shorter than the models
array, we’ll have a crash. We also have this extra variable offset
that isn’t doing very much. A nicer, Swiftier way to write this same code is:
for (model, viewController) in zip(models, viewControllers) {
viewController.model = model
}
This code is much more focused and now works with any Sequence
types. It will also handle mismatched array lengths safely.
Let’s look at another example. This code adds an autolayout constraint between the first imageView
and its container, and then also between each pair of image views.
for (offset, imageView) in imageViews.enumerated() {
if offset == 0 {
imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
} else {
let imageToAnchor = imageViews[offset - 1]
imageView.leadingAnchor.constraint(equalTo: imageToAnchor.trailingAnchor).isActive = true
}
}
This code sample has similar problems. We want consecutive pairs of elements, but using enumerated()
to get numbers that we can manipulate means we’re mucking about with indexes manually when we can we working at a higher level. zip
can help us here as well.
First, let’s write some code to deal with the container constraint on the first element:
imageViews.first?.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
Next, lets handle the pairs:
for (left, right) in zip(imageViews, imageViews.dropFirst()) {
left.trailingAnchor.constraint(equalTo: right.leadingAnchor).isActive = true
}
Done. No indexes, works with any (multi-pass) Sequence, and more focused.
(You can also box up the pairing code into an extension and call .eachPair()
if you like.)
There are some valid uses of enumerated()
. Because what you’re getting is not an index, but just an integer, the right time to use it is when you need to work with a number (not an index) that corresponds to each element. For example, if you need to position a bunch of views vertically, where each one has y
coordinate that corresponds to some height multiplied by the offset in the sequence, enumerated()
is appropriate. Here’s a concrete example:
for (offset, view) in views.enumerated() {
view.frame.origin.y = offset * view.frame.height
}
Because the offset
here is being used for its properties as a number, enumerated()
works great.
The rule of thumb here is simple: if you’re using enumerated()
for an index, there might be a better way to solve your problem. If you’re using it for its properties as a number, thumbs up emoji.
This article is also available in Chinese.
When writing code, the biggest enemy is complexity. Maintaining levels of abstraction and allowing the developers who work in our codebases to fluidly move up and down through those concepts is crucial to large software projects.
Comments are a tool that have a veneer of managing complexity, but subtly hurt the codebase instead of helping.
My perspective on comments comes from two facts: 1) Comments don’t compile. 2) Comments are usually a subdued color in your syntax highlighter. Because they don’t compile and because they fade away from sight, it’s very easy to ignore them as you make changes to the code. If you make changes and don’t update the comments, you end up with a comment that doesn’t accurately reflect the content of code.
In an ideal world, you’d be able to catch this in code review, but I’ve never seen that work in practice. Code review tools have syntax highlighting as well, causing comments to fade into the background in that context as well. Also, because code review diffs only show a few lines around the change, out-of-date comments can only be found if the comment is close to the change itself. If you change a line deep in a method that changes what the preconditions of the method are, your reviewers won’t see the comment and won’t be able to tell you to update it.
There are a few tips and tricks to avoid the most common comments, some of which are covered in a blog post by an old manager of mine, and others of which Andrew can’t even dream of because he writes code in Ruby, a dynamically typed language.
-
Name things well! The first step to avoiding comments is to avoid things like single-letter names, abstract names, and vague names. The more precise you can be with the name, the less you’ll need a comment.
-
If a method has preconditions, add an assertion that will crash the app (at least in debug, if not always!) if an invalid value is passed in. If you only accept positive, non-zero integers, write a bit of code that says so:
precondition(int > 0)
. -
Even better than run-time assertions is compile-time ones. If your method accepts only non-empty arrays, you could write
precondition(!array.isEmpty)
. But you could also use a type that can never express an empty array. Users of your API will now never be able to pass an empty array into that parameter.In the same vein, do you have a bool parameter that’s better expressed via an enum with two named cases? Are your optionals better expressed as other enums? Reveal your intentions in your naming.
-
Mark hacks, temporary code, and prototype code liberally. I often use a prefix like
hack_
to mark that a function is not written ideally. An underscore in a method name in Swift looks so out of place that I directly feel the pain of making my codebase worse, and I’m inspired to try and fix it. We recently made a function prefixed withshouldReallyBeInTheCoordinator_
because something needed to get through code review, but the code wasn’t in the right class. The needs of your codebase and your own sensibilities become aligned when bad code is made to be ugly code as well. Other good prefixes:perf_
andtemp_
. -
Via Mark Sands: you can encode the IDs from your bug tracker into method names and they’ll show up in stack traces. UIKit references radar numbers in a few cases. This is real:
-[UIViewController _hackFor11408026_beginAppearanceTransition:animated:]
-
Don’t be afraid to describe the “why” of a function in its name. You’re allowed to make methods called
updateFrameOnNextTickBecauseAutoLayoutHasntCompletedYet(frame: CGRect)
. The compiler doesn’t care how long your methods are named, and code is read much more than its written. Comments are just words, and so are method names. Future maintainers of that codebase will appreciate your verbosity. -
Make helper functions, like a function called
TODO(date: Date, message: String)
that logs an error (or even better, crashes in debug) if the TODO isn’t fixed by some date. Another example via Jordan Rose. -
Encode any algorithmic requirements into tests. If all of the above fails, and you can’t rely on the preconditions, types, and method names to solve a particular problem, write a test for it. This is especially good for edge cases. If someone rewrites that code, the tests will break, and they’ll know they have to handle that case with their new code.
Remember: this isn’t an excuse to write impenetrable code and not comment it! The code must be clear if you’re going to skip commenting it. Comments for me are a last resort. If I can find of any other way to express my intentions to the next programmer, I don’t add a comment.
Further Reading
Sometimes you need to modify your view controller to do a new thing. One thing leads to another, and you find yourself adding an optional variable property to the view controller, which will be set in some cases and not in others.
I think that more often than not, this is a flawed approach. There are a few reasons for this. First, a class with a optional property that is only used sometimes has a weak sense of identity. Put another way, adding optional properties blurs the fundamental meaning of the type. Second, this optional property doesn’t carry any semantic meaning. What does it mean about the state of this object if the type is nil? A casual reader of your code can’t tell in what cases the property will be nil, or what ramifications it has for the object. Third, code will continue to mutate. One optional property becomes two optional properties, which becomes three, and before you know it, you’ll end up at the bottom of a slippery slope. If we want to express that this value must exist if that value is nil, we can’t do that with simple optionals.
In all the codebases I’ve seen, I’ve found two primary reasons why you might need an optional property on your view controller. I’ll explore them both and suggest better patterns to solve each problem.
The first reason that you might have a problematic optional property is when you won’t necessarily have that object or piece of data every time this view controller is used.
One example of this that came up recently was a view controller that was being presented from a push notification in some cases. It needed to show a specific message that came from that notification. The simplest solution to this problem was to add an optional string property:
class LocationViewController: UIViewController {
//...
var notificationMessage: String?
//...
}
Other code in the view controller switched on the message’s existence to determine which views to allocate, how to lay them out, and so on. The optionality of the property represents more than existence of a simple string. It had implications in the rest of the view controller and the rest of the view layer.
The more essential point here is that the string now no longer represents a string that might be there or might not — it now represents the presentation style or mode that the view controller is in. Was it created in the context of a push notification or through normal browsing? The answer is in this barely related property!
To fix this problem, we have to make this mode fully explicit. Once it’s a first class citizen in this view controller, its effect on the view controller will be much more obvious.
class LocationViewController: UIViewController {
//...
enum Mode {
case fromNotification(message: String)
case normal
}
var mode: Mode
//...
}
As Sandi Metz says, there’s never one specialization. With an optional property, a sometimes-I-do-it-this-way, the code doesn’t admit that the nil
case of this property carries meaning as well and is really its own specialization. With an enum, this is reified and formalized in code.
Note that our new enum has a very similar shape to the enum that defines the Optional
type.
enum Optional<Wrapped> {
case some(Wrapped)
case none
}
It has a few extremely useful differences, though.
First, semantics. some
and none
are abstract; normal
and fromNotification
have meaning associated with them. Readers of your code will thank you.
Second, extensibility. If another mode is added to this view controller, we have a better means of fully describing it. If the new mode can never overlap with these two modes, we can add another enum case, with whatever necessary associated data. If that new mode can overlap with both of these current modes, it can be a new enum and a new property altogether. The state of the object is more readily describable. Either of these choices are better than adding yet another optional property for the new mode.
Third, extensibility again. Because LaunchViewController.Mode
is a first-class type, we can add functions and computed properties to it. For example, the height of the label that holds the notification message is probably dependent on the presence of the message itself. Therefore, we can move that code to the enum itself:
extension Mode {
var notificationLabelHeight: CGFloat {
switch self {
case .normal: return 0
case .fromNotification(let message): return message.size().height
}
}
}
Moving this data to a richer type gives you gains on all of these fronts, with very little code investment.
The second reason you might use an optional property is a subset of the first. In some cases, it’s not that the optional property represents a different mode of the view controller, it’s that it represents a temporal dimension to your code. You can’t initialize the property with a value because you don’t have it yet. Common examples of this are when you’re fetching something from the network, or waiting on some system resource that takes too long to retrieve and needs to be asynchronous.
class UserViewController: UIViewController {
//...
// will be loaded asynchronously
var user: User?
//...
}
This kind of problem can sometimes be papered over if the data comes back in array form. Since you can represent the non-existent state with the empty array, this problem may still be lurking, even if you’re not using an optional. The number of times that I’ve had a tough time adding empty states to a table view controller is a testament to this extent of this problem.
Since this problem is a subset of the first problem, we can solve it in the same way, with an enum:
enum LoadingState<Wrapped> {
case initial
case loading
case loaded(Wrapped)
case error(Error)
}
While this solution works, there is a better abstraction for managing asynchronous state. A value that doesn’t exist yet but will at some point in the future is best represented by a Promise
. Promises have several advantages, depending on your use case.
First, promises allow mutation, but only once and only from the pending state. Once a promise has been mutated (“fulfilled”), it can’t change again. (If you need something that can be updated multiple times, a Signal
or Observable
is what you’re looking for.) This means that you can have semantics that are similar to let
, but still manage asynchronous data.
Next, promises have facility for adding blocks that will get fired when a value comes in. If the property remained a simple optional property, these added blocks are equivalent to code in a didSet
property observer. However, promise blocks are more powerful, since more than one can be added, and they can be added on the fly from any place in the class. Further, if they’re added when the promise is already fulfilled with a value, they’ll immediately execute.
Lastly, if needed, promises handle errors as well. For certain kinds of asynchronous data, this is crucial, and you’ll get it for free.
If you’re using my Promise
library, you can create a pending Promise
with the empty initializer, and fulfill it at any time with the fulfill
method.
When it comes to optional properties, sometimes they reveal things about your code that may not have been apparent before. When you see yourself adding an optional parameter to a view controller, ask yourself, what does this optional really mean? Is there a better way to represent this data?
This article is also available in Chinese.
I often get a better picture of a type’s purpose from knowing what its instance variables are. Once you know the underlying structure of that type, it’s more obvious how to use it. The converse is also true: it can sometimes be hard to figure out what exactly an object is for, without seeing the layout of its members. This is especially apparent with Apple’s closed-source classes.
A great example of this is NSDate
. When I started programming, I had a tough time figuring out how to use the NSDate
and all of its corresponding sibling objects, like NSDateComponents
, NSDateFormatter
NSCalendar
. Why do you have to use NSCalendar
to add 2 days to date? The boundaries between these classes seem arbitrarily drawn, and that makes it hard to know where to look to find any specific functionality you’re looking for.
The key revelation for me was understanding what NSDate
really was under the hood, which made all the rest of the pieces fall together. NSDate
is just a fancy wrapper around a double. That’s it. And a close reading of the docs reveals this fact:
NSDate
objects encapsulate a single point in time, independent of any particular calendrical system or time zone.
All NSDate
does is store a double representing the number of seconds since 00:00:00 UTC on January 1, 2001. This number of seconds has nothing to do with timezones, days of the week, months, daylight savings time, leap seconds, or leap years. If the calculation can be done with just the number of seconds since that reference date, it goes on NSDate
. Otherwise, it goes somewhere else.
Examples of things that can be done with just this double are comparison (earlierDate
, laterDate
), equality, and time interval calculations (which also returns a number of seconds). distantFuture
and distantPast
are obvious now too, they’re just the largest expressible doubles, in positive and negative forms.
For other stuff, you have to go to other classes and objects. For example, adding one day to a moment in time, while it can done by just adding 24*60*60
seconds to an NSDate
, is best handled via NSCalendar
, to avoid running afoul of issues with Daylight Savings Time, leap seconds/days, and other non-standard issues that can crop up with time. This blog post lays out the case for using NSCalendar
for these calculations.
Because NSDate
doesn’t store any information about the expected month of a particular date, if you want to change that month, you have to use an object that knows about and can split apart the various “components” that make up a date. For this, we use a NSDateComponents
object. I find that thinking about how I might write a similar class can help me understand what it’s for and how its layout works. Take a look at the public interface for NSDateComponents
and see if you can figure out how its data might be stored internally.
I found another interesting example of using an object’s property storage layout to learn things about the nature of that object when I was working on my Promise library. If you look at the properties that each of my Promise objects stores, you’ll see three things:
public final class Promise<Value> {
private var state: State<Value>
private let lockQueue = DispatchQueue(label: "promise_lock_queue", qos: .userInitiated)
private var callbacks: [Callback<Value>] = []
Each promise has its current state (such as .pending
, fulfilled
, or .rejected
), a queue to ensure thread-safety, and an array of callbacks to call when the promise is either fulfilled or rejected.
After writing my library, I looked at some Signal
/Observable
implementations to see if I could understand them. I found JensRavens/Interstellar
to be the most straightforward. I look at the instance properties of each of its Signal
objects, and I found a very similar structure:
public final class Signal<T> {
private var value: Result<T>?
private var callbacks: [Result<T> -> Void] = []
private let mutex = Mutex()
Something to store the current state, something to store the callbacks, and something to store the thread-safety primitive. The order is different, and they used a mutex instead of a queue, but it’s otherwise identical. The only difference between these two types is a semantic one: promises can clear their callbacks when they’re completed (releasing them and the variables they capture), and signals must keep their callbacks around.
I think this principle can also help in the design of your own types too. Take a look at the properties on an object that you’re working on. Does each property have a purpose? Does it contribute to the overall identity of the object? Are there properties that are used in some instances of that type, but not others? If so, those properties might belong somewhere else. Making sure that the instance variables available on a type are tightly controlled and used fully ensures that our object has a well-defined purpose in your application.
This article is also available in Chinese.
When firing network requests, there are often many side effects that need to take place. Side effects are poison for testability, however, and may vary from app to app and request to request. If we can create a system where we can create and compose these side effects together, we can increase the testability and other factors.
Imagine a very simple network client:
final class NetworkClient {
let session: URLSession
init(session: URLSession = URLSession.shared) {
self.session = session
}
func send<Output: JSONInitializable>(request: Request<Output>) -> Promise<Output> {
let urlRequest = RequestBuilder(request: request).urlRequest
return session.data(with: urlRequest)
.then({ data, response in
let json = try JSONSerialization.jsonObject(with: data)
return Output(json: json)
})
}
}
This class wraps a URLSession
. It has a send
method that accepts Request
objects that have some associated phantom type Output
that conforms to JSONInitializable
. The send function returns a promise with the same type as Output
. In the body of the send
method, a URLRequest
is built using the RequestBuilder
type, and that request is sent via the URL session. The data that is returned from the network request is parsed as JSON, and hydrates a model, which is the value inside the returned promise.
This network client is greatly simplified (namely, I left out some optional handling stuff), but is basically the code I want to work with.
This class right now is great. It’s a simple object that doesn’t touch anything global (minus the network, gated through the URLSession
), and is easily testable. We could wrap the URLSession
in a protocol, inject it in the NetworkClient
initializer, and mock it in our tests, and test the logic around constructing an object from some JSON response. Testable network code!
The trouble here is that while it does do plenty of good stuff, there’s still lots that it doesn’t do. For example, we might want it to:
- register a background task with the application, so that the network request can keep working after the user presses the home button
- show and hide the application’s network activity indicator
- add a specific header for authorization with some API
We could add all of these things to our NetworkClient
class, but each of them touches some piece of global state or singleton. The first one touches the UIApplication
singleton, the second, a shared counter, and the third, some form of storage that keeps track of the auth token.
If we added all of these globals and special cases, it’ll be a lot harder to test this object. We’ll have to mock up and inject each of these globals in separately, and our tests will get that much more complicated.
These three behaviors are expected to fire on every network request, but other requests will have request-specific behaviors that would ideally be reusable. For example, many (but not all) requests will need error handling, to let the user know that some action they took didn’t go through successfully. Other requests will need to save things to Core Data, and I’d ideally like the network client to know nothing about Core Data. While I won’t be focusing on examples of request-specific behaviors here, I’ll show the code for how to integrate them.
I want a way to decouple these behaviors implementation details from the network client, so that we can test the client separately from each of the behaviors.
Let’s define a request behavior with a protocol:
protocol RequestBehavior {
var additionalHeaders: [String: String] { get }
func beforeSend()
func afterSuccess(result: Any)
func afterFailure(error: Error)
}
The protocol provides a pretty boring default implementation for each method:
extension RequestBehavior {
var additionalHeaders: [String: String] {
return [:]
}
func beforeSend() {
}
func afterSuccess(result: Any) {
}
func afterFailure(error: Error) {
}
}
The basic idea is each behavior gets callbacks when specific network events occur, and they can execute code. Two things will be useful for us as we develop this: an “empty” request behavior that doesn’t do anything, and a request behavior that combines many request behaviors. The “empty” behavior inherits all the implementations from the protocol extension, and the “combined” behavior stores an array of behaviors and calls the relevant method on each behavior:
struct EmptyRequestBehavior: RequestBehavior { }
struct CombinedRequestBehavior: RequestBehavior {
let behaviors: [RequestBehavior]
var additionalHeaders: [String : String] {
return behaviors.reduce([String: String](), { sum, behavior in
return sum.merged(with: behavior.additionalHeaders)
})
}
func beforeSend() {
behaviors.forEach({ $0.beforeSend() })
}
func afterSuccess(result: Any) {
behaviors.forEach({ $0.afterSuccess(result: result) })
}
func afterFailure(error: Error) {
behaviors.forEach({ $0.afterFailure(error: error) })
}
}
These are very abstract for the moment but will become useful soon.
Next, we need to modify our network client to call the methods in our request behavior at the right time.
final class NetworkClient {
let session: URLSession
let defaultRequestBehavior: RequestBehavior
init(session: URLSession = URLSession.shared, defaultRequestBehavior: RequestBehavior = EmptyRequestBehavior()) {
self.session = session
self.defaultRequestBehavior = defaultRequestBehavior
}
func send<Output: JSONInitializable>(request: Request<Output>, behavior: RequestBehavior = EmptyRequestBehavior()) -> Promise<Output> {
let combinedBehavior = CombinedRequestBehavior(behaviors: [behavior, defaultRequestBehavior])
let urlRequest = RequestBuilder(request: request, behavior: combinedBehavior).urlRequest
combinedBehavior.beforeSend()
return session.data(with: urlRequest)
.then({ data, response in
let json = try JSONSerialization.jsonObject(with: data)
let result = try Output(json: json)
combinedBehavior.afterSuccess(result: result)
return result
})
.catch({ error in
combinedBehavior.afterFailure(error: error)
})
}
}
The defaultRequestBehavior
from the client and the behavior
for the specific request both default to a new EmptyRequestBehavior
, so that they become effectively opt-in. Our network client combines the behavior for the individual request and the behavior for the whole client. It passes that to the RequestBuilder
so it can use the additionalHeaders
, and then it calls beforeSend
, afterSuccess
, and afterFailure
at the appropriate times and with the appropriate values.
With this simple separation of request and side effect, it’s now possible to test the client separately from each the behaviors we might want to add to it. These behaviors themselves, since they’re their own objects, can be easily instantiated and tested.
Let’s take a look at at the behaviors I mentioned above. First, registering a background task for each network request:
final class BackgroundTaskBehavior: RequestBehavior {
private let application = UIApplication.shared
private var identifier: UIBackgroundTaskIdentifier?
func beforeSend() {
identifier = application.beginBackgroundTask(expirationHandler: {
self.endBackgroundTask()
})
}
func afterSuccess(response: AnyResponse) {
endBackgroundTask()
}
func afterFailure(error: Error) {
endBackgroundTask()
}
private func endBackgroundTask() {
if let identifier = identifier {
application.endBackgroundTask(identifier)
self.identifier = nil
}
}
}
While these background tasks are often only registered at the time the app is closed, and usually a single one is registered, you can register as many as you want, at any time. Because this behavior requires the maintenance of state, it’s especially suited to being its own object, and thus a prime candidate for a request behavior. Testing this behavior involves wrapping the UIApplication
in a protocol, injecting it in an initializer, and confirming with a mock that the right methods were called at the right times.
Next, let’s look at the network activity indicator.
class ActivityIndicatorState {
static let shared = ActivityIndicatorState()
let application = UIApplication.shared
var counter = 0 {
didSet {
application.isNetworkActivityIndicatorVisible = counter != 0
}
}
}
class NetworkActivityIndicatorBehavior: RequestBehavior {
let state = ActivityIndicatorState.shared
func beforeSend() {
state.counter += 1
}
func afterFailure(error: Error) {
state.counter -= 1
}
func afterSuccess(response: AnyResponse) {
state.counter -= 1
}
}
This behavior is another one that’s typically impossible to test. Now that it’s broken up into two objects, instead of being embedded in the NetworkClient
, it’s now possible. Inject the application singleton (wrapped in a protocol, passed into the initializer) into ActivityIndicatorState
, and you can test that it correctly turns the network activity indicator on and off by changing the value of the counter. Inject a ActivityIndicatorState
(also wrapped in a protocol, passed into the initializer), and you can test the incrementing and decrementing of counter
property as well.
Finally, let’s look at how we might bring the global state of an auth token into a request behavior.
struct AuthTokenHeaderBehavior: RequestBehavior {
let userDefaults = UserDefaults.standard
var additionalHeaders: [String : String] {
if let token = userDefaults.string(forKey: "authToken") {
return ["X-Auth-Token": token]
}
return [:]
}
}
This one is simple enough that I might not test it, but nevertheless, something that was really hard before is a lot simpler now. Inject the standard UserDefaults
object (surprise, wrapped in a protocol again), and test that it correctly returns the header dictionary. One more test for if the UserDefaults
doesn’t have the key in question, and you’re all done.
To access the network client in an iOS application, it’s typically done with a global accessor. Because this client requires many lines of initialization, I might put it in an immediately-executed closure:
enum SharedNetworkClient {
static let main: NetworkClient = {
let behavior = CombinationRequestBehavior(behaviors: [
AuthTokenHeaderBehavior(),
NetworkActivityBehavior(),
BackgroundTaskBehavior(),
])
return NetworkClient(behavior: behavior)
}()
}
I’ve also started putting singletons in their own namespace (called SharedX
), instead of in the type itself, to help remind me that singletons should just be objects, and that they aren’t exempt from being testable objects and good citizens.
A few final notes: the network library Moya has a feature similar to this called “plugins”. While my implementation is a little bit different, it’s a very similar idea. Moya’s plugins only operate on a per-client basis, adding behaviors that operate per-request is also very useful for performing side-effects, like saving to some form of persistence or cache, or presenting an error.
You may find that you need other methods on your request behavior, which you should feel free to add. One that I omitted here for brevity is:
func modify(request: URLRequest) -> URLRequest
This function lets you mutate the URLRequest
in any way that you want, even returning a totally different URLRequest, if you want. This is obviously a powerful and dangerous action, but for some requirements it is useful and even necessary.
Network code is typically hard to maintain, because it touches an inherently global resource, which lends itself to tough-to-test singleton designs. Request behaviors are a useful way of separating the code that sends requests from any side-effects that need to happen during that request. This little abstraction simplifies network code, adds reusability for per-request behaviors, and vastly increases testability.
Once you feel comfortable with these topics, read Advanced Requests Behaviors as well.
Chris Dzombak and I started a podcast this summer called Fatal Error. This Monday, we published the 10th and final episode of the first season.
Each episode, we covered a different programming pattern. We covered coordinators, view models, promises, and signals. We discussed singletons, the single-responsibility principle, domain-driven design, and testing. We capped the season off with a discussion of why we think caring about code is the same as caring about your prodcuct.
I wasn’t sure how it would turn out, but I’m really pleased with the work that Chris and I did on it. Our episodes are between 30 minutes and an hour, and we post a new one every other week.
We’re taking a short break, and we’ll be back with Season 2 in early January. We have some cool stuff planned, and we hope you’ll join us!
Bryan Irace wrote a great post a earlier this year, detailing how to measure the compilation time of the swift functions that you write. It’s a great technique for spot checking your slow functions, but you’ll have to remember to run it every few months, to catch any new slow functions you write.
In May, Jordan Rose added a Swift flag for emitting warnings whenever a function takes longer than some threshold to compile. You can leave this flag enabled in your project, and it will warn you whenever any slow functions are added to the project.
To add the flag:
- Navigate to your target in Xcode (not the project)
- Click the “Build Settings” tab
- Make sure that “All” settings are being shown, not “Basic”
- Scroll to (or search for) “Other Swift Flags”
- Add two flags:
-Xfrontend
and-warn-long-function-bodies=100
, where 100 is the number of milliseconds you’d like the warning threshold to be - Compile your project
When you compile, you’ll notice some new warnings. They look something like:
Getter ‘uniqueID’ took 105ms to type-check (limit: 100ms)
You can click on the warning to jump to the function in question. Rewrite it to take less time to compile.
Sadly, in the commit message adding this feature, Jordan writes,
As a frontend option, this is UNSUPPORTED and may be removed without notice at any future date.
This feature is very useful, and I hope it stays around as long as Swift compile times are flaky.
Thanks to Olivier, Caleb and Sam.
What if arrays in Swift couldn’t be empty?
Hear me out: maybe Swift should have been designed with empty arrays being impossible. It’s madness, right? What other language has arrays that can’t be modeled as empty?
However, Swift already changes some of the rules from older languages like C. For example, there’s no break
required in switch
statements, opting for the more explicit fallthrough
to combine cases instead. There’s no ++
operator, either. It’s confusing, duplicative, and ultimately the language is better without it.
Swift also differs from C by requiring explicit nullability. Swift lets you describe to the type system whether a single value is “empty” or not, using the Optional
type. You can say whether you have a view controller, or something that might be a view controller, and might be .none
. The type system can then check your work in all places, ensuring that the value is never missing when you expect it to be there.
Doubly Empty
When optionals and arrays interplay, however, you have two ways of expressing emptiness: nil
and the empty array.
This can cause confusion, for example, when checking if an optional array is either nil or empty. For example, you might expect optional chaining to be effective, since it’s so useful in other parts of Swift. optionalArray?.isEmpty
, however, returns an Optional<Bool>
, which really distills down to the essence of a confused type. The type system will reject expressions that evaluate to optional bools in if
statements.
optionalArray == []
will compile, but returns false
for the case when the array is nil, which isn’t the expected behavior. You’re left with a few other choices that are all correct, but unwieldy:
if optionalArray == nil || optionalArray == [] {
if let array = optionalArray, array.isEmpty {
if (optionalArray ?? []).isEmpty {
if optionalArray?.isEmpty != false {
if optionalArray?.isEmpty ?? false {
The easiest way to get around this is to remember to never store optional arrays as properties. I’m pretty strict about this rule, making sure I don’t mix different types of emptiness. I also apply it other other types that can be “empty” as well — dictionaries, strings, booleans, sets, and sometimes numbers. Having to check two forms of emptiness is something I never want to do.
While its easy to follow this rule, for example, in the properties of a class, it’s not really possible to follow it in all cases. For example, getting an array property from an optional object will result in an optional array.
let wheels = optionalCar?.wheels // expression has type [Wheel]?
Getting a value out of a dictionary is the same.
let wheels = dictionary["wheels"] as? [Wheel]
You have to remember to tack on ?? []
to each of these expressions.
We just came from a world where you couldn’t separate the concept of a nullable view controller from a view controller to the type system. Gaining that ability simplified lots of expressions, reduced crashes, and generally clarified our code.
If the Array
type couldn’t be empty, then optional arrays would represent empty arrays, and non-optional arrays would always have at least one item in them. One of the two forms of emptiness would be impossible, and any code that tried to use the other form would simply wouldn’t compile.
Modeling
Non-empty arrays are also useful for modeling. Sometimes it’s useful to express to the type system that a given array can never be empty. For example, perhaps your User
class is modeled as having many email addresses, but shouldn’t be valid if the user has no email addresses. Expressing that to the type system would be awesome, but we currently can’t do that. Other examples:
- A
Country
must have at least oneCity
. - An
Album
must have at least oneSong
. - A
Building
can’t exist without at least oneFloor
.
The examples abound.
If the Array
type couldn’t be empty, these relationships and their constraints would all be expressible to the type system, and you’d be prevented from removing the last item from arrays.
Expressions
With the advent of the Optional
type, many expressions became simplified. When you know that a type can’t ever be null, you can skip certain checks and treat it in a more straightforward way. It’s the same with non-empty arrays. Currently, methods on the Collection
protocol, like first
, last
, max
and min
return optionals, purely to handle the case that the array is empty.
There have been multiple cases where I knew an array was modeled such that it should never be empty, and when calling methods like first
, I lamented the fact that I’d still have to guard that case even though I knew it was never possible and I just wanted to express that to the type system.
If the Array
type couldn’t be empty, these methods could return non-optional values, and expressions that use them would be simplified. Empty arrays would use optional chaining to access these methods, and they would their results would be optional, just like today.
Appending
If arrays worked like this, appending to non-empty (now regular) arrays would work as normal. But appending to emptiable arrays would be a mess.
var emptiableArray = //...
emptiableArray == nil
? emptiableArray = [newItem]
: emptiableArray?.append(newItem)
This is annoying, but the good news is that as of Swift 3.1, we can specialize specific types of generic extensions. That is, we can add a method to optionals that store concrete array types. (Before, you could only add extensions to specializations that used protocols.)
extension Optional<Array<Element>> {
func append(_ element: Element) {
switch self {
case .some(array):
array.append(element)
case .none:
self = [element]
}
}
}
And now we transparently have the behavior that we had before.
Without Loss Of Generality
To take this a step further, what if the type of an array baked in how long it was? For example, adding an element to an Array<of: 4, element: String>
would return an Array<of: 5, element: String>
. This concept is called dependent types and exists in some experimental languages with more advanced type systems, like Coq, Agda, and Idris. Oisín discusses how we can achieve a similar effect in Swift.
While these are super interesting, they’re also a little bit impractical. If you think about it, it means you can no longer store arrays in a class’s properties, unless you know that that the number of elements in the array won’t change. In a lot of cases, you also won’t know at compile time how many objects are going to come through an API or a database connection.
Simplying making the empty/non-empty distinction has a clear and practical use today, and it cleans up a lot of how Swift looks and works.
NonEmptyArray
This blog post is mostly a thought experiment. But it’s also a regular experiment. To that end, I built a non-empty array type. You can find it on GitHub here. It acts just like an array, but it isn’t emptiable. It conforms to Sequence
, Collection
, and has ==
and !=
methods.
Because of some part of the Swift type system that I don’t fully understand but appreciate nonetheless, you can override methods (like first
) from a protocol (like Collection
) but change its type from Element?
to Element
, and Swift will do the right thing at the call-site, and use the more precise type, Element
. This means that NonEmptyArray
correctly returns non-optional values for first
, last
, max
and min
, even though they’re defined on Collection
as being Optional
. There are tests in the repo asserting this.
Having an array that can’t be empty does interesting things. Methods that insert or append are fine, but methods that remove elements are more problematic. I marked the methods as throws
, but after thinking about it more, this may not have been the right call. After all, removing elements from regular Swift arrays can also have deleterious effects, it’s just that they’re allowed to remove one more element than NonEmptyArray
can. Swift arrays will fatalError
when removing from an empty array, so perhaps that’s the right thing to do here as well.
I’m looking forward to breaking in the NonEmptyArray type in a few projects to see if the clean-up from having non-optional methods like first
and friends is worth losing the type sugar that Swift’s built-in array type.