SwiftUI Tip: Enumerating a View State

Due to its declarative and reactive nature, SwiftUI works pretty well with this technique. Suppose we are building a screen that fetches a Github profile. We begin by describing the possible stages of this fetch process:

enum State: Equatable {
    case `default`
    case loading
    case fetched(profile: GithubProfileViewModel)
    case failure(error: HttpError)

In the view model, we declare a variable holding the state:

final class GithubProfileFetchViewModel: ObservableObject {

    private(set) var state = State.default

    func fetchProfile(using username: String) async {
        // This method will update the state in the different stages
        // of the fetch process. Notice the state is published.

    // ...

Then in the View, we use this state when declaring our body:

struct GithubProfileFetchView: View {

    private var viewModel = GithubProfileFetchViewModel()

    // ...

    var body: some View {
        VStack {
            // ...
            switch viewModel.state {
            case .`default`:
            case .fetched(let profileViewModel):
                ScrollView {
                    GithubProfileView(viewModel: profileViewModel)
            case .loading:
            case .failure(let error):
                if error == .requestFailed(statusCode: 404) {
                } else {

            // ...
        .task(id: shouldStartFetch) {
            await fetchProfile()
    // ...

The usage of switch makes the code really readable. Whenever the state changes, SwiftUI presents a different view for us. Since SwiftUI is declarative, we don’t need to setup bindings, and manually change what we display. This process is all done automatically for us.

Note that this technique works with other architectures too, and with other property wrappers (e.g @State, @ObservableObject, @EnvironmentObject). You can check the full view model or view code.

You might also want to read this article by Apple.

When does a SwiftUI Environment get retained?

The answer depends on how we use SwiftUI. For an app entirely written using it, one might argue that it gets released whenever the app finishes. But what about an UIKit app that uses some SwiftUI views?

To answer this question, let’s explore some scenarios involving environment objects. They have reference semantics, and we can track their instances using the memory graph.

If you wish to check the sample project, here’s the repository. It has different git tags for each scenario explored below.

Scenario 1: holding a View instance

Let’s begin our exploration by instantiating our RootView with some environment objects. We won’t attach it to the UI, but only store it in a variable in our view controller:

class MainViewController: UIViewController {
    var rootView: AnyView!
    override func viewDidLoad() {
        rootView = AnyView(

SwiftUI needs a way to bind these environment objects to a specific view. They will stay around as long as we hold the RootView instance. If we build the memory graph, here’s what it shows:

Scenario 1: holding a RootView instance.

Notice the memory graph doesn’t show instances of SwiftUI views. These are structs, and have value semantics, not living in the heap.

Scenario 2: Holding a reference to a UIHostingController instance

This is similar to the first scenario. If we hold a reference to a UIHostingController, the environment will still be around, as the controller holds the value of its root view. Notice this happens even after the SwiftUI views get removed from the view hierarchy:

@objc private func releaseRootView() {
    guard let rootViewHostingController,
          let hostingView = rootViewHostingController.view else {
    // Scenario 2: Keep holding a reference to the hosting controller.
    //self.rootViewHostingController = nil

The memory graph continues showing our two instances in memory:

Scenario 2: Referencing a UIHostingController

Scenario 3: Having a UIView with a retain cycle

In this scenario, we explore UIViews used within a SwiftUI view tree. UIViews have reference semantics, and if an instance leaks in memory, the SwiftUI environment will continue alive:

final class LeakingUIView: UIView {
    private var retainingClosure: (() -> ())!
    override func layoutSubviews() {
        retainingClosure = {
            // Scenario 3: Having a retain cycle in a UIView.
            self.backgroundColor = .red

struct LeakingView: UIViewRepresentable {
    func makeUIView(context: Context) -> LeakingUIView {
    func updateUIView(_ uiView: LeakingUIView, context: Context) {}

When we release our hosting controller instance, we notice the environment is still being held due to references associated with the leaking view in memory.

Here’s what the memory graph shows:

  1. LeakingView has a reference a UITraitCollection
  2. UITraitCollection has a reference to a SwiftUIEnvironmentWrapper
  3. SwiftUIEnvironmentWrapper has references to its environment objects (EnvA, EnvB)

Scenario 4: Having a retain cycle between a UIView and an environment object

If we hold a reference to a UIView in one of the environment objects, and this UIView was inside a SwiftUI tree, we have a reference cycle:

struct SubView: UIViewRepresentable {
    private var envA: EnvA
    func makeUIView(context: Context) -> SubUIView {
        let subView = SubUIView()
        // Scenario 4: Retain cycle between environemnt objects and view references.
        envA.someView = subView
        return subView
    func updateUIView(_ uiView: SubUIView, context: Context) {}

After removing the SwiftUI views from the screen, here’s what the memory graph shows:

Scenario 4: a retain cycle between SubUIView and EnvA
  1. SubUIView has a reference to a UITraitCollection
  2. UITraitCollection has a reference to a SwiftUIEnvironmentWrapper
  3. SwiftUIEnvironmentWrapper has a reference to EnvA
  4. EnvA has a reference to SubUIView, and we can go back to the point 1 (a cycle)


Always make sure to:

  1. Dispose of any SwiftUI View values not used anymore
  2. Dispose of any UIHostingController references not used anymore
  3. Watch out for memory leaks in:
    • UIViews used within SwiftUI
    • references between your UIViews and your environment objects
    • UIViewControllers presenting the UIHostingControllers
    • the environment objects themselves

Environment objects can get complex depending on what your views need to accomplish. They might use a lot of computational resources, so it’s necessary to watch out for live instances when the underlying views get deallocated.

SPM command-line interface

In the preceding articles, we’ve been using Xcode as a tool for creating, developing, and fetching Swift packages. At the end of the day, the program doing the heavy job is SPM. Xcode interfaces with it, providing a nice GUI.

In this article, we’ll take a closer look at SPM itself, an independent program belonging to the Swift project. We’ll explore its main commands by building and releasing a trivial swift executable.

Main commands

If we type man swift in the terminal, it’ll display the manual page for Swift:

man page for the Swift command.

As we can see, the commands for controlling SPM are:

  • build
  • test
  • run
  • package and its subcommands

If we type swift package in the terminal, it lists the following sub-commands (notice how many they are, as opposed to the three commands Xcode provides):

The subcommands of swift package

Creating an executable

Besides libraries, a Swift Package can produce executables too. This is how the Swift project creates its command-line programs (e.g: docc, swift-driver). To create an executable package, we need to type the following in the terminal (I’m using ZSH):

mkdir simple-executable
cd simple-executable
swift package init --name "Simple Executable" --type executable

# If you wish to see other options available, type `swift package --help`

This will initialize a package with an executable target:

swift package init output.

Here’s what the manifest looks like:

// swift-tools-version:5.5

import PackageDescription

let package = Package(
    name: "Simple Executable",
    dependencies: [],
    targets: [
            name: "Simple Executable",
            dependencies: []),
            name: "Simple ExecutableTests",
            dependencies: ["Simple Executable"]),


To build our package, we use the swift build command:

Building the package.

Use the swift package clean command to clear the build caches.


Running the executable is simple, simply type swift run:

Running the executable.

Resolving & updating dependencies

Let’s first include a dependency in our manifest:

let package = Package(
    // ...
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
    targets: [
            name: "Simple Executable",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser")
    // ...

With this dependency now declared, we can use the following commands:

  • swift package resolve to resolve the dependencies
  • swift package show-dependencies to show the currently resolved dependencies
  • swift package update to update the dependencies
  • swift package reset to reset the cache and build directories
Commands for handling dependencies.


Unfortunately, the swift package command doesn’t explain how we can build an executable for release. You can find this information in the GitHub repository. Here’s how it works:

  1. Use swift build -c release to build the executable using the release configuration
  2. The built executable lives inside the .build/release folder. We can invoke it directly from there: type .build/release/Simple\ Package to see what happens.
Building for release and running the executable.

Invoking it from ZSH

We can include the executable in one of the folders used in the $PATH variable. This allows ZSH to find and run the executable we created. Type the following in your terminal:

  1. echo $PATH, this will display the folders separated by colon characters (:)
  2. Rename the executable to an invokable name:
    1. mv .build/release/Simple\ Executable ./say-hello
    2. If /usr/local/bin doesn’t exist, create it with this command: sudo mkdir /usr/local/bin
    3. sudo mv ./say-hello /usr/local/bin
  3. Now we can simply invoke our command by typing say-hello
Invoking the executable using the terminal.


SPM is a powerful program capable of managing dependencies and building libraries and executables. It is a key component of the Swift project, supporting the usage of the language in different operating systems.

In this article, we saw how we can create a trivial package that builds an executable. We also explored the main SPM commands, and how we can invoke our executables directly from the terminal’s $PATH variable.

Semantic versioning

In the previous article, we talked about how we can publish a swift package, and how we version it using semantic git tags. But what does semantic mean? In this article, we’ll explore this subject in more detail.

The basics

A library’s public API is all the symbols (e.g: types, variables, and functions) used by its clients via import. When we depend on a library, we also depend on its public API to accomplish a task. This dependency makes part of our code coupled to it.

Imagine we’re using a library from a popular Swift package. What would happen if the maintainers released a new version with huge changes to its public API? Our code would break. To avoid this issue, we could rely on a single version. But this means we would be locked in it, incapable of updating the package without updating our code. To make things worse, imagine each package owner has a different way of handling versions. These scenarios are what we call dependency hell.

Semantic versioning is a specification aimed at putting an end to dependency hell. It divides versions into three numbers: major, minor, and patch (e.g. 1.5.8). Here’s what each number mean:

  • Major means the public API has changed, requiring our code to be updated
  • Minor means new code was added to the public API, the additions don’t break existing integrations (backward compatible)
  • Patch means bugs were fixed without changing the public API

Selecting package versions

When adding a Swift package, the recommended approach is to select a version using the up to next major dependency rule:

Use the Up to Next Major version.

It means our project we’ll receive updates that don’t break our code. Select the Up to next minor rule if you only wish to receive bug fixes.

When declaring dependencies on the package, we can also specify those same version rules:

// ...
dependencies: [
        url: "https://github.com/apple/swift-algorithms.git",
        .upToNextMajor(from: "1.0.0")
// ...


Besides defining how to organize our versions, the specification also comes with some important guidelines:

Development versions (below 1.0.0)

Every version below 1.0.0 is considered unstable and under development. The public API isn’t defined yet and can change drastically.

The initial version (1.0.0)

Once the public API is defined, it’s time to release the initial version of the package. The public API is considered stable and set in stone.

Never modify a released version

If you wish to correct something, release a new version on top of the one with errors.

Use a development branch

You don’t need to release every single change you make. We can release multiple changes using a single version. Having a development branch allows developers to decide when to release a batch of changes as a single major, minor, or patch.


We can also pre-release versions. This is useful for testing features as they get added. They follow the same versioning rules but have a specific format in the end:


A pre-release doesn’t need to be stable. Use it only to test a set of features while they are being developed. Pre-releases have less precedence over official versions:

1.2.5 > 1.2.5-beta1

Usage with SPM

On the package side, to specify a pre-release, we use the Version type:

// ...
dependencies: [
        url: "https://github.com/apple/swift-algorithms.git",
        from: Version(1, 2, 5, prereleaseIdentifiers: ["alpha", "1"]))
// ...

The above code specifies a range of versions: 1.2.5-alpha-1 ..< 2.0.0.

On the project side (using Xcode 13.2.1), we can include the pre-release identifiers directly:

1.2.0-beta.1 ..< 2.0.0

If we include the pre-release identifiers, SPM will fetch the versions automatically when we update our packages.

Keep a changelog

It’s important to document every version being added. The documentation is added to a file called CHANGELOG.md. It contains the history of your project, with relevant information on what was added, removed, or changed. Here’s an example from the Swift-Algorithms package.

For more information, visit this link.

Closing thoughts

If you wish to correctly use dependency managers, make sure to understand what semantic versioning is. We’ve explored what problems it solves, and also some of its main guidelines. We also understood what a CHANGELOG file is, and how we can use it.

In the next article, we’ll talk about building executables with SPM. We’ll also take a look at its command-line interface.

Publishing a Swift Package

This article is the third post in a series about SPM. If you don’t know what a Swift Package is, I recommend you to take a look at the first and second articles. Today we’ll explore how we can publish and version a package.

Centralized vs. Decentralized

Dependency managers can have two different strategies for organizing and fetching their packages. They can be centralized or decentralized.

A centralized dependency manager has a central place where all packages are located. It then uses this place to fetch a dependency. The advantage of this approach is that it offers easy discoverability (e.g: a website we can use to search for dependencies).

A decentralized dependency manager doesn’t have a specific place containing all registered packages. It allows dependencies to be published independently, requiring us to inform their URL to fetch them. The advantage of this approach is that it doesn’t rely on a single source, which makes it more flexible and resilient to failures.

Git usage

SPM is decentralized, and makes heavy use of Git. Xcode offers an option to initialize a local Git repository when we create a package:

You can initialize the git repository when creating a package.

It also adds an initial commit for us:

The initial commit Xcode added for us.

The default folder structure even comes with the README.md and .gitignore files (notice we also have the .git folder for our local repository):

The listed files under our package folder.

Creating a remote repository

To publish a package, all we need to do is create a remote Git repository and push our local package commits to it. SPM will then be able to fetch the package. We’ll be using Github, but we can publish a package to other Git servers as well.

When creating the repository, make sure you mark it as public and that it’s empty (doesn’t include a README.md or license):

Creating a repository on Github.

Once the remote repository is created, we need to configure the remote inside our package:

Configuring a remote.

After we push our local commits, we can use the repository URL to fetch the package:

Fetching our package.

Private packages

It’s pretty common to develop and use private packages. They belong to a person or organization, and are shared internally with multiple apps or packages. Their publication process is the same, but the repository is private.

To fetch private packages, we need to configure how we authenticate to the Git server we’re using. SPM will then be able to access our private repositories. There are two common protocols we might use: HTTPS and SSH.

To setup authentication, we first need to configure a Github account in the preferences panel (preferences -> Accounts). Xcode will require a personal access token. To get one, follow the steps in this link.

Accounts panel: preferences -> accounts


Authenticating with HTTPS is simple. Once we have the account in place, we need to ensure it will clone the repositories using this protocol:

A Github account using HTTPS.


SSH is more complicated to be configured. This link explains it in detail. When creating the keys, make sure you don’t use the ssh-ed25519 encryption algorithm, because it’s unsupported (we need to use RSA instead):

Don’t use the ed25519 algorithm.
Use RSA instead. The instructions are in this link.

Once we have SSH setup, we need to configure our Xcode account to use our private key. Notice that we also need to clone using SSH:

Configuring the account to use ssh.


Once we publish our package, we need to ensure its clients receive updates. SPM uses git tags to mark the versions of a package, and we use semantic versioning to organize them. We release a new version when we want the clients to receive a change. It might be a bug fix or performance improvement, for example.

The workflow for releasing a new version is pretty simple:

# To list our versions:
git tag

# To create a local version:
git tag -a major.minor.patch -m "tag description"

# To publish a version:
git push origin major.minor.patch

# To remove a version already published:
git push origin --delete major.minor.patch
git tag -d major.minor.patch

To summarize

  • SPM makes heavy use of Git
  • We can initialize a Git repository when creating a package
  • To publish a package, push its local changes to a remote repository
  • To fetch private repositories, configure the Xcode account with either HTTPS or SSH
  • Use git tags to manage the versions of a package

Versioning packages is an important topic that deserves attention. In the upcoming article, we’ll look into it in more detail.

The structure of a Swift Package

In the previous article, we talked about what SPM is and how we can use it. We briefly talked about packages, but we are missing a key point: how to create them. In this article, we’ll explore packages in more detail. If you don’t know what a package is, I recommend you first check the previous blog post before continuing.

Creating a package

We can use Xcode to create packages (in this article I’ll be using version 13.2.1). With Xcode running, Let’s go to File -> New -> Package:

Selecting the package creation menu.

Selecting this option will open the following window:

Package creation screen in Xcode.

Once we are good with the location and name of our package, we can click the create button, and SPM will create our Swift package with a default folder structure.

Folder structure

The folder structure of our package.

When created, a package comes with a default folder structure. Each file and folder has a purpose:

  • README.md: Describes the package to humans
  • Package.swift: A manifest file defining what the package is
  • Sources: Folder containing the source files of our package
  • Tests: Folder containing the unit test suites covering the code from the Sources folder

The manifest file

Every Swift package has a manifest (named Package.swift) identifying it. A manifest contains important information about a package:

  • Its name
  • The platforms it supports
  • The targets it consists of
  • Its dependencies
  • The products it distributes (libraries)

SPM uses this information to manage our packages. The following code comes from our Package.swift file:

// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Simple-Package",
    products: [
            name: "Simple-Package",
            targets: ["Simple-Package"]),
    dependencies: [
        // .package(url: /* package url */, from: "1.0.0"),
    targets: [
            name: "Simple-Package",
            dependencies: []),
            name: "Simple-PackageTests",
            dependencies: ["Simple-Package"]),

Let’s understand the most important attributes of a package:


A package is divided into targets. They are the building blocks of a package. Each target has a specific purpose and defines its module:

targets: [
    .target(name: "Simple-Package", dependencies: []),
    // More targets can be defined here.

Targets have their own folder under Sources, containing their source code. They can also depend on external code or on other targets as well. This allows us to modularize our code if needed.

Test targets

We can write and run automated tests for the code contained in a target. All we have to do is define a test target with the corresponding folder and test suites (notice how it uses our Simple-Package as a dependency):

targets: [
    // ...
    .testTarget(name: "Simple-PackageTests", dependencies: ["Simple-Package"])


The module defined by a target isn’t directly accessible to the clients of a package. It is initially internal. The only way to distribute the public API of a target is to define a product of the library type. It vends the module defined by its target, allowing the clients to use its public API via import:

products: [
    .library(name: "Simple-Package", targets: ["Simple-Package"]),


Packages can also depend on other packages. When we declare dependencies, we specify which versions we want:

dependencies: [
    .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),

Right after we edit our manifest, SPM will resolve our dependencies, the same process it does on a regular Xcode project:

The resolved dependencies (swift-algorithms depends on swift-numerics)

We can then use any products from swift-algorithms in our targets:

/// ...
    name: "Simple Package", 
    dependencies: [
        .product(name: "Algorithms", package: "swift-algorithms")
/// ...


The workflow for packages using Xcode is pretty similar to the one for regular projects (e.g iOS or MacOS).

Adding files

To add swift files to a target, simply place them inside its folder. This file will then belong to the target’s module, which means we can add new code to it and access the other code already defined in there.

Adding a file to the `Simple Package` target.

Building and testing

Building is pretty simple. Just select the product from the list and build it. I’ve added some targets and products to exemplify:

Selecting a product to be built.

Here’s how the manifest looks like:

// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Simple Package",
    products: [
            name: "Simple Package",
            targets: ["Simple Package"]),
            name: "Library1",
            targets: ["Target1"]),
            name: "Library2",
            targets: ["Target2"]),
            name: "Library3",
            targets: ["Target3"]),
    dependencies: [
        .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
    targets: [
            name: "Simple Package",
            dependencies: [
                .product(name: "Algorithms", package: "swift-algorithms")
            name: "Target1",
            dependencies: []),
            name: "Target2",
            dependencies: []),
            name: "Target3",
            dependencies: []),
            name: "Simple PackageTests",
            dependencies: ["Simple Package"]),

We also have to define the folders for each target:

The new folder structure in Sources.


We’ve learned how to build packages and what they look like in terms of structure. We explored what manifests are and how we can use them to define a package. We also looked at the key components a package has:

  • Dependencies
  • Targets
  • Products

The package we built doesn’t have any source code, just meaningless files. I encourage you to look at real-world examples, like the Time package we used in the previous post or the Algorithms package we used in this one.

In the next article, we’ll take a look at how we can publish and version our packages.

The basics of Swift Package Manager

Most of the code we write depends on external libraries. Libraries are pieces of code that can be used by many applications. The OS already comes with essential libraries we can use as building blocks, and all we have to do is import their modules. This is what we do when we need libraries like Foundation, SwiftUI, or UIKit. But what happens when we need to use a library that doesn’t come with the OS?

In this case, we can manually download the library and embed it in our project. This process gets complicated if we depend on multiple libraries, though. A library might also depend on other libraries, which means we would need to manually manage each sub dependency. Another problem we might face is dealing with versions. How do we know when to update a specific dependency?

Those problems are what make dependency managers exist. They automate those processes, making our lives simpler and allowing us to focus on writing code. SPM is Apple’s dependency manager and is part of the Swift project. It integrates with the Swift build system to provide us the full experience of downloading, compiling, linking, and updating dependencies. Xcode also integrates with it, exposing controls to use this tool.


SPM works by managing packages. A package is a flexible container of source code that can distribute libraries to clients. In this article, we’ll be using a simple open-source package called Time. More specifically, we’ll explore how we can use SPM to use this library in our code.

The examples in this article use Xcode 13.2.1. If you wish to follow along, create a new iOS project using Swift (the UI framework doesn’t matter).

Declaring Dependencies

To use a package in our iOS app, we first need to declare it as a dependency in the Package Dependencies screen:

  1. In the navigation area, select the project
  2. At the left bar, select the project again (not the target)
  3. At the top tab bar, select the Package Dependencies tab
The Package Dependencies screen on Xcode

Now we need to add the Time package:

  1. Copy the repository URL: https://github.com/davedelong/time.git
  2. Select the + button, under Packages
  3. Paste the copied URL in the search bar at the top
  4. In the dependency rule option, select “Up to Next Major Version”
Adding Time as a package

Xcode will then fetch this package and ask us which library we want to use (one package can distribute multiple libraries). This package is simple, it only distributes one:

Adding the Time product

Once we add this package to our project, Xcode will display it in the Package Dependencies list, in the navigation area:

The list of resolved packages in the navigation area

Package Resolution

After we declare the dependencies in our project, SPM will resolve them:

  1. Look at the URL and version of each package
  2. Fetch each dependency with the correct version
  3. Repeat this process for each sub dependency of a package if it has any
  4. Configure the packages with the project, allowing us to import their distributed libraries

After the package resolution phase, SPM generates a JSON file called Package.resolved. It contains which packages were resolved and what their versions are. SPM uses it to figure out whether a dependency needs to be updated or not. Here’s what this file looks like:

  "object": {
    "pins": [
        "package": "Time",
        "repositoryURL": "https://github.com/davedelong/time.git",
        "state": {
          "branch": null,
          "revision": "be6cbbbb97aa4570e3b51bd56f98ca3cf62aa3cb",
          "version": "0.9.2"
  "version": 1

Package.resolved is located in the swiftpm folder, inside your xcodeproj file:


It’s a good idea to put the swiftpm folder under version control. This way we can ensure every team member is using the right package versions.

Packages Location

The resolved packages are located in the derived data folder. A lot of times developers have problems with Xcode, and deleting this folder sometimes solves those issues. Just be aware that when you delete it, you’ll also delete the cached dependencies, forcing a new resolution of packages.

The Three SPM Actions

Once we have the packages in place, we can use three different actions for dealing with them:

Actions for dealing with SPM

Reset Package Caches

Use this action to erase the cached packages of our project from the derived data folder. SPM will automatically resolve the dependencies again.

Resolve Package Versions

Use this action to force a new resolution of dependencies. If your package.resolved file was updated by another team member, this action will ensure your local packages match the versions in that file.

Update to Latest Package Versions

The action name says it all. It will check if any dependency has available updates and will update them according to the dependency rule (e.g Up to next major). Semantic versioning is used to organize the package versions.

Be careful

Packages can come from third-party developers. Before using these packages, ensure the following:

  1. You understand what the package does and what sub dependencies it uses
  2. It comes from a trusted source
  3. If it’s an open-source package, ensure it’s maintained by the community (check the latest changes, number of stars, and so on)
  4. The package license allows your application to use it


In this article, we’ve explored how SPM works. Here’s what we learned:

  • What SPM and packages are
  • How to add packages to an Xcode project
  • What package resolution is
  • Where Xcode stores the resolved packages
  • How to manage the packages in Xcode
  • What to take into account when adding a package

In the upcoming article, we’ll explore how we can create a Swift package and what it looks like in terms of structure.

My thoughts on TDD

A natural workflow for software developers is to code and then conduct some manual tests to make sure things are working correctly. While this approach works, it also has some issues:

  • It’s error-prone (the tests are manually conducted)
  • We normally only test a small portion of the app (the part under development)

To be fair, manually testing only the part under development is not the problem. The problem is that we normally don’t conduct all previous tests for the other features. We might introduce a breaking change without noticing it.

Beyond manual tests, there are other tools we can use to ensure the correctness of our code. One of them is unit tests. They ensure each component in our code performs correctly in isolation, increasing the overall quality of the software we write.

TDD is a workflow that builds on top of automated tests in general. I don’t think it works in every scenario, but when used with unit tests this technique shines:

  • It gives us instant and reliable feedback on the changes we make
  • It disciplines us to write tests covering the features we develop
  • We are informed if we break an existing functionality

I’ve used TDD in the second and third assignments of the CS193P course. These assignments ask the students to build two card games for iOS. I began the development by the model (which contains the game logic), and I didn’t need to write the UI layer to only then manually test the business logic. Having the tests in place also gave me confidence that it worked. Here is a commit that shows this workflow in action (Each feature has some related tests ensuring it works).

TDD Cons

It’s hard to use this technique if:

  • We don’t have a good understanding of the project
  • We have a tight schedule
  • We are writing UI or integration tests (those take time to write and run and might give different results)

Many of TDD’s benefits come from the fact that it enforces us to write automated tests. If we get into the habit of always writing tests for the changes we make, we are also in a good shape.