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:

@MainActor
final class GithubProfileFetchViewModel: ObservableObject {

    @Published
    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 {

    @StateObject
    private var viewModel = GithubProfileFetchViewModel()

    // ...

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

            // ...
        }
        .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() {
        super.viewDidLoad()
        
        rootView = AnyView(
            RootView()
                .environmentObject(EnvA())
                .environmentObject(EnvB())
        )
    }
}

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 {
        return
    }
        
    rootViewHostingController.removeFromParent()
    hostingView.removeFromSuperview()
        
    // 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() {
        super.layoutSubviews()
        
        retainingClosure = {
            // Scenario 3: Having a retain cycle in a UIView.
            self.backgroundColor = .red
        }
        retainingClosure()
    }
}

struct LeakingView: UIViewRepresentable {
    func makeUIView(context: Context) -> LeakingUIView {
        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 {
    @EnvironmentObject
    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)

Conclusion

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: [
        .executableTarget(
            name: "Simple Executable",
            dependencies: []),
        .testTarget(
            name: "Simple ExecutableTests",
            dependencies: ["Simple Executable"]),
    ]
)

Building

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

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: [
        .executableTarget(
            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.

Releasing

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.

Conclusion

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: [
    .package(
        url: "https://github.com/apple/swift-algorithms.git",
        .upToNextMajor(from: "1.0.0")
    )
],
// ...

Guidelines

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.

Pre-releases

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:

1.2.5-beta1
1.2.5-1
1.2.5-alpha-633

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: [
    .package(
        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

HTTPS

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

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.

Versioning

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.