Sharing Swift code
We might not realize it, but most of us share code with other people every single day. Whether we’re discussing a new way to implement something on the company Slack, opening a Pull Request to have a co-worker review our code, working on something together with someone else, or talking about the state of a feature in a meeting — whenever we work in some form of team, we’re usually sharing code with others.
However, sharing code in a way that’s clear and effective can be quite difficult — and it’s easy for misunderstandings to happen, for code reviews to drag out over a long period of time, and for bugs to start occurring because an API was used “the wrong way”.
So in this week’s article — which is my 100th in a row (🎉!) — let’s take a look at what goes into sharing code in a nice way, and some techniques that we can employ to make any code that we share easier to understand, whether we’re showing something to a colleague or publishing something publicly.
Context is king
Often when sharing code to illustrate an idea or an alternative implementation, we want to boil things down to their essentials. After all, we don’t want to spend too much time on code that’s just there to prove a point — we’d rather spend that time writing actual production code.
A very common way to do just that is to use types called Foo
and Bar
— and variables named a
, b
, and c
— when writing sample code. For example, let’s say that we want to show one of our co-workers how to use Swift’s first class function capabilities to pass a function as a closure when filtering an array. We might send that person something like this:
struct Foo {
var bar: Bar
}
struct Bar {
var int: Int
}
func foobar(_ foo: Foo) -> Bool {
return foo.bar.int > 0
}
let a = [
Foo(bar: Bar(int: 0)),
Foo(bar: Bar(int: 2))
]
let b = a.filter(foobar)
However, even if you know exactly how both first class functions and the filter
API work, the above code can still be quite hard to read and understand — because it lacks something that most code heavily relies on — context.
If we instead contextualize the above code, by simply replacing all of the ”Foo”s and ”Bar”s with names that give more meaning to the code we’re sharing, things usually become much easier to understand. Here’s the exact same code as above, but written within the context of checking if a document has been opened by inspecting its metadata:
struct Document {
var metadata: Metadata
}
struct Metadata {
var openCount: Int
}
func hasDocumentBeenOpened(_ document: Document) -> Bool {
return document.metadata.openCount > 0
}
let allDocuments = [
Document(metadata: Metadata(openCount: 0)),
Document(metadata: Metadata(openCount: 2))
]
let openedDocuments = allDocuments.filter(hasDocumentBeenOpened)
Even though both of the above two examples are functionally the same, looking at them side by side, it almost looks like the first one is a “ciphered version” of the second one. It might seem like unnecessary extra work to have to come up with a context for each code sample that we share with someone — but doing so usually makes it so much easier to get our message across, which is particularly important if we’re teaching someone a new technique, or showing how a new API we built is meant to be used.
Focusing in
So how much context is “enough”, and is it possible to end up sharing too much context? Let’s take a look at another example, in which we’ve implemented a new purchase flow in an e-commerce app, and want to share some sample code with our co-workers that shows how to use this new feature.
An initial idea might be to show the entire context in which our new PurchaseViewController
will be used in, such as sharing a full implementation of a ProductViewController
that presents and interacts with our new feature:
class ProductViewController: UIViewController, PurchaseViewControllerDelegate {
private let product: Product
private lazy var productView = ProductView()
init(product: Product) {
self.product = product
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = productView
}
override func viewDidLoad() {
super.viewDidLoad()
productView.titleLabel.text = product.name
productView.subtitleLabel.text = product.category
let purchaseButton = UIButton()
purchaseButton.setTitle("Purchase".localized, for: .normal)
purchaseButton.addTarget(self,
action: #selector(openPurchaseFlow),
for: .touchUpInside
)
productView.accessoryView = purchaseButton
}
@objc private func openPurchaseFlow() {
let purchaseVC = PurchaseViewController(product: product)
purchaseVC.delegate = self
present(purchaseVC, animated: true)
}
func purchaseViewControllerDidFinish(_ viewController: PurchaseViewController) {
viewController.dismiss(animated: true)
showSuccessAlert()
}
}
There’s nothing really wrong with the above code, but it’s kind of hard to see what the point we’re trying to make is, simply by quickly reading through it. After all, all we really want to show is how to use our new PurchaseViewController
, not how to implement an entire product screen. So rather than showing a complete full implementation, let’s instead focus in on what’s really important in this case, and only show the code that directly deals with our new API.
One way of doing so, is to simply share two extensions on ProductViewController
that adds the new functionality that we want to demonstrate — like this:
// Showing the purchase flow
private extension ProductViewController {
@objc func openPurchaseFlow() {
let purchaseVC = PurchaseViewController(product: product)
purchaseVC.delegate = self
present(purchaseVC, animated: true)
}
}
// Responding to when the flow was completed
extension ProductViewController: PurchaseViewControllerDelegate {
func purchaseViewControllerDidFinish(_ viewController: PurchaseViewController) {
showSuccessAlert()
viewController.dismiss(animated: true)
}
}
Sharing code in smaller pieces, rather than as one big wall of text, usually makes it much easier for the reader to follow along. This is especially true when including sample code in some form of presentation, when the audience usually has very limited time to read and understand the code — and needs to do so while the presenter is talking. Just like when structuring code to make it easier to maintain, splitting things up into easier-to-digest chunks really becomes key when sharing code as well.
What, how, and why?
When showing a new technique, submitting a change for code review, or when debating whether or not to introduce a certain abstraction into a code base — there really are three questions that our code needs to answer:
- What is our code attempting to do?
- How does our code work?
- Why is the code being introduced?
Arguably the most important of those three questions is the last one — if a piece of code doesn’t have a clear purpose, why is it being discussed in the first place? While coding as an “academic exercise” can be fun, it’s usually hard to fully understand a technique, pattern or language feature until it’s used in a way that makes practical sense.
Take Swift’s key paths feature for example. Even though I was incredibly excited about that feature, it took me quite a while to write about it — since I hadn’t yet encountered a solid, practical use case for it. That’s my general rule of thumb — I don’t write about something (no matter how cool it is from a theoretical standpoint) until I’ve used it in a real project, so that I’ve been able to see the practical pros and cons of it.
As an example, one way to share how to use key paths much earlier, would’ve been to do something like this:
let keyPath: KeyPath<Car, String> = \.brand
let car = Car(brand: "Volkswagen")
let brand = car[keyPath: keyPath]
print(brand) // Prints Volkswagen
While the above sample code answers what key paths are and how to use them — it doesn’t answer the why. Since the above can simply be replaced by accessing the car.brand
property directly, it doesn’t really show a compelling use case.
Instead, here’s a sample taken from the article I did end up writing — “The power of key paths in Swift” — which shows the key paths feature in action, using a very practical use case:
// Using key paths to implement a convenience API on Sequence
extension Sequence {
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { a, b in
return a[keyPath: keyPath] < b[keyPath: keyPath]
}
}
}
// We can now easily sort an array simply by passing a key path
playlist.songs.sorted(by: \.name)
playlist.songs.sorted(by: \.dateAdded)
playlist.songs.sorted(by: \.ratings.worldWide)
The above may or may not “convince you” to start using key paths (which isn’t my goal by the way), but it does show why the feature might be useful in a real context — which usually makes it much easier to relate to the code being shared.
Documenting intent
Even though the purpose of a given piece of code might be completely clear to everyone involved when it’s being introduced, chances are that over time that knowledge will be lost. The developer who wrote the code might have transferred to a different team or left the company entirely, and everyone who originally reviewed the code or has since worked on it might simply not remember what the original intent was.
One common solution to this problem is documentation. Simply write a comment or an external document (depending on complexity) that outlines exactly what the intent behind a certain algorithm, class, or feature was — and once that code becomes legacy, whoever digs up that relic will have a nice instruction manual to go along with it, right?
However, while documentation is great, there are two major problems with relying solely on documentation to capture the intent of something. First, the text we write can easily become outdated — especially over time — as requirements change, the code is refactored, and new ideas and concepts are introduced. Second, it’s hard to know before hand what kind of questions that whoever ends up maintaining our code in the future will have about it. We can try our best to cover what we think is ambiguous or difficult to grasp, but it’s hard to make any sort of guarantees.
One way to address the above two problems is to supplement our documentation with a solid suite of tests. Doing so will both document and constantly validate our intent, not only once — but every time our test suite is run.
Let’s take a look at another example. Here we’re working on an Animator
class, and we’re using a static constant to define a minimum animation duration. Over time, while tweaking this value based on current designs, we’ve discovered that if we use a value less than 0.2
seconds we start seeing glitches in our UI — which could be caused by things beyond our control. So to warn both others, and future versions of ourselves, never to go below that threshold — we add a comment as part of our documentation:
class Animator {
// The minimum allowed duration for an animation. If the
// user supplies a lower value, Animator will adjust it to
// be this value. Do not use a value lower than 0.2 as it
// causes glitches!
static let minimumDuration: TimeInterval = 0.25
init(duration: TimeInterval,
closure: @escaping (UIView) -> Void) {
...
}
}
While the above is a great first step towards documenting our intent — let’s also add a test to make sure that minimumDuration
will stay above 0.2
, even if someone misses the comment that we made, or if it’s ever lost in case someone moves the code around or refactors it:
class AnimatorTests: XCTestCase {
func testMinimumDurationisAboveGlitchThreshold() {
XCTAssertGreaterThanOrEqual(
Animator.minimumDuration, 0.2,
"Do not use a value lower than 0.2 as it causes glitches"
)
}
}
By both documenting our intent with actual text, and codifying the requirements of our code using a test, we usually stand a much better chance of avoiding past mistakes in the future — and it can be a great way of showing how something is supposed to work.
Conclusion
Whether we’re sharing code publicly, or internally with the people we work with, taking a bit of extra care to make sure that the code we share is as clear as possible, is most often worth the effort. While we can never guarantee that the right context will be captured, or that the intent of our code will stand the test of time — by focusing in on what we’re trying to say with the code that we’re sharing, we can drastically increase our chance of success.
I hope this article has given you a bit of insight into how I come up with sample code for the articles on this site, and that these tips will be useful to you — whether or not you are, or are planning to, share any of your code publicly — through articles, open source, or something similar.
I also want to take this moment to thank you so much for reading this site — whether this is the first article you’ve read, or the 100th. Writing for all of you each week is something that I thoroughly enjoy, and hope that I’ll be able to keep doing for a long time going forward.
Here’s to another 100 articles, and thanks for reading! 🎉🚀