Sending vs Sendable in Swift

With Swift 6, we have an entirely new version of the language that has all kinds of data race protections built-in. Most of these protections were around with Swift 5 in one way or another and in Swift 6 they've refined, updated, improved, and expanded these features, making them mandatory. So in Swift 5 you could get away with certain things where in Swift 6 these are now compiler errors.

Swift 6 also introduces a bunch of new features, one of these is the sending keyword. Sending closely relates to Sendable, but they are pretty different in terms of why they're used, what they can do, and which problems they tend to solve.

In this post, I would like to explore the similarities and differences between Sendable and sending. By the end of this post, you will understand why the Swift team decided to change the closures that you pass to tasks, continuations, and task groups to be sending instead of @Sendable.

If you're not fully up to date on Sendable, I highly recommend that you check out my post on Sendable and @Sendable closures. In this post, it's most relevant for you to understand the @Sendable closures part because we're going to be looking at a comparison between a @Sendable closure and a sending argument.

Understanding the problem that’s solved by sending

In Swift 5, we didn't have the sending keyword. That meant that if we wanted to pass a closure or a value from one place to another safely, we would do that with the sendable annotation. So, for example, Task would have been defined a little bit like this in Swift 5.

public init(
  priority: TaskPriority? = nil,
  operation: @Sendable @escaping () async -> Success
)

This initializer is copied from the Swift repository with some annotations stripped for simplicity.

Notice that the operation argument takes a @Sendable closure.

Taking a @Sendable closure for something like a Task means that that closure should be safe to call from any other tasks or isolation context. In practice, this means that whatever we do and capture inside of that closure must be safe, or in other words, it must be Sendable.

So, a @Sendable closure can essentially only capture Sendable things.

This means that the code below is not safe according to the Swift 5.10 compiler with strict concurrency warnings enabled.

Note that running the example below in Xcode 16 with the Swift 6 compiler in Swift 5 mode will not throw any errors. That's because Task has changed its operation to be sending instead of @Sendable at a language level regardless of language mode.

So, even in Swift 5 language mode, Task takes a sending operation.

// The example below requires the Swift 5 COMPILER to fail
// Using the Swift 5 language mode is not enough
func exampleFunc() {
  let isNotSendable = MyClass()

  Task {
      // Capture of 'isNotSendable' with non-sendable type 'MyClass' in a `@Sendable` closure
    isNotSendable.count += 1
  }
}

If you want to explore this compiler error in a project that uses the Swift 6 compiler, you can define your own function that takes a @Sendable closure instead of a Task:

public func sendableClosure(
  _ closure: @Sendable () -> Void
  ) {
  closure()
}

If you call that instead of Task, you’ll see the compiler error mentioned earlier.

The compiler error is correct. We are taking something that isn't sendable and passing it into a task which in Swift 5 still took a @Sendable closure.

The compiler doesn't like that because the compiler says, "If this is a sendable closure, then it must be safe to call this from multiple isolation contexts, and if we're capturing a non-sendable class, that is not going to work."

This problem is something that you would run into occasionally, especially with @Sendable closures.

Our specific usage here is totally safe though. We're creating an instance of MyClass inside of the function that we're making a task or passing that instance of MyClass into the task.

And then we're never accessing it outside of the task or after we make the task anymore because by the end of exampleFunc this instance is no longer retained outside of the Task closure.

Because of this, there's no way that we're going to be passing isolation boundaries here; No other place than our Task has access to our instance anymore.

That’s where sending comes in…

Understanding sending arguments

In Swift 6, the team added a feature that allows us to tell the compiler that we intend to capture whatever non-sendable state we might receive and don't want to access it elsewhere after capturing it.

This allows us to pass non-sendable objects into a closure that needs to be safe to call across isolation contexts.

In Swift 6, the code below is perfectly valid:

func exampleFunc() async {
  let isNotSendable = MyClass()

  Task {
    isNotSendable.count += 1
  }
}

That’s because Task had its operation changed from being @Sendable to something that looks a bit as follows:

public init(
  priority: TaskPriority? = nil,
  operation: sending @escaping () async -> Success
)

Again, this is a simplified version of the actual initializer. The point is for you to see how they replaced @Sendable with sending.

Because the closure is now sending instead of @sendable, the compiler can check that this instance of MyClass that we're passing into the task is not accessed or used after the task captures it. So while the code above is valid, we can actually write something that is no longer valid.

For example:

func exampleFunc() async {
  let isNotSendable = MyClass()

  // Value of non-Sendable type ... accessed after being transferred; 
  // later accesses could race
  Task {
    isNotSendable.count += 1
  }

  // Access can happen concurrently
  print(isNotSendable.count)
} 

This change to the language allows us to pass non-sendable state into a Task, which is something that you'll sometimes want to do. It also makes sure that we're not doing things that are potentially unsafe, like accessing non-sendable state from multiple isolation contexts, which is what happens in the example above.

If you are defining your own functions that take closures that you want to be safe to call from multiple isolation contexts, you’ll want to mark them as sending.

Defining your own function that takes a sending closure looks as follows:

public func sendingClosure(
  _ closure: sending () -> Void
) {
  closure()
}

The sending keyword is added as a prefix to the closure type, similar to where @escaping would normally go.

In Summary

You probably won't be defining your own sending closures or your own functions that take sending arguments frequently. The Swift team has updated the initializers for tasks, detached tasks, the continuation APIs, and the task group APIs to take sending closures instead of @Sendable closures. Because of this, you'll find that Swift 6 allows you to do certain things that Swift 5 wouldn't allow you to do with strict concurrency enabled.

I think it is really cool to know and understand how sending and @Sendable work.

I highly recommend that you experiment with the examples in this blog post by defining your own sending and @Sendable closures and seeing how each can be called and how you can call them from multiple tasks. It's also worth exploring how and when each options stops working so you're aware of their limitations.

Further reading

Mocking a network connection in your Swift Tests

Unit tests should be as free of external dependencies as possible. This means that you want to have full control over everything that happens in your tests.

For example, if you're working with a database, you want the database to be empty or in some predefined state before your test starts. You operate on the database during your test and after your test the database can be thrown away.

By making your tests not depend on external state, you make sure that your tests are repeatable, can run in parallel and don't depend on one test running before another test.

Historically, something like the network is particularly hard to use in tests because what if your test runs but you don't have a network connection, or what if your test runs during a time where the server that you're talking to has an outage? Your tests would now fail even though there's nothing wrong with your code. So you want to decouple your tests from the network so that your tests become repeatable, independent and run without relying on some external server.

In this post, I'm going to explore two different options with you.

One option is to simply mock out the networking layer entirely. The other option uses something called URLProtocol which allows us to take full control over the requests and responses inside of URLSession, which means we can actually make our tests work without a network connection and without removing URLSession from our tests.

Defining the code that we want to test

In order to properly figure out how we're going to test our code, we should probably define the objects that we would like to test. In this case, I would like to test a pretty simple view model and networking pair.

So let's take a look at the view model first. Here's the code that I would like to test for my view model.

@Observable
class FeedViewModel {
  var feedState: FeedState = .notLoaded
  private let network: NetworkClient

  init(network: NetworkClient) {
    self.network = network
  }

  func fetchPosts() async {
    feedState = .loading
    do {
      let posts = try await network.fetchPosts()
      feedState = .loaded(posts)
    } catch {
      feedState = .error(error)
    }
  }

  func createPost(withContents contents: String) async throws -> Post {
    return try await network.createPost(withContents: contents)
  }
}

In essence, the tests that I would like to write here would confirm that calling fetchPost would actually update my list of posts as new posts become available.

Planning the tests

I would probably call fetchPost to make sure that the feed state becomes a value that I expect, then I would call it again and return different posts from the network, making sure that my feed state updates accordingly. I would probably also want to test that if any error would be thrown during the fetching phase, that my feed state will become the corresponding error type.

So to boil that down to a list, here's the test I would write:

  • Make sure that I can fetch posts
  • Make sure that posts get updated if the network returns new posts
  • Make sure that errors are handled correctly

I also have the create post function, which is a little bit shorter. It doesn't change the feed state.

What I would test there is that if I create a post with certain contents, a post with the provided contents is actually what is returned from this function.

I've already implemented the networking layer for this view model, so here's what that looks like.

class NetworkClient {
  let urlSession: URLSession
  let baseURL: URL = URL(string: "https://practicalios.dev/")!

  init(urlSession: URLSession) {
    self.urlSession = urlSession
  }

  func fetchPosts() async throws -> [Post] {
    let url = baseURL.appending(path: "posts")
    let (data, _) = try await urlSession.data(from: url)

    return try JSONDecoder().decode([Post].self, from: data)
  }

  func createPost(withContents contents: String) async throws -> Post {
    let url = baseURL.appending(path: "create-post")
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    let body = ["contents": contents]
    request.httpBody = try JSONEncoder().encode(body)

    let (data, _) = try await urlSession.data(for: request)

    return try JSONDecoder().decode(Post.self, from: data)
  }
}

In an ideal world, I would be able to test that calling fetchPosts on my network client is actually going to construct the correct URL and that it will use that URL to make a call to URLSession. Similarly for createPost, I would want to make sure that the HTTP body that I construct is valid and contains the data that I intend to send to the server.

There are essentially two things that we could want to test here:

  1. The view model, making sure that it calls the correct functions of the network.
  2. The networking client, making sure that it makes the correct calls to the server.

Replacing your networking layer with a mock for testing

A common way to test code that relies on a network is to simply remove the networking portion of it altogether. Instead of depending on concrete networking objects, we would depend on protocols.

Abstracting our dependencies with protocols

Here's what that looks like if we apply this to our view model.

protocol Networking {
  func fetchPosts() async throws -> [Post]
  func createPost(withContents contents: String) async throws -> Post
}

@Observable
class FeedViewModel {
  var feedState: FeedState = .notLoaded
  private let network: any Networking

  init(network: any Networking) {
    self.network = network
  }

  // functions are unchanged
}

The key thing that changed here is that instead of depending on a network client, we depend on the Networking protocol. The Networking protocol defines which functions we can call and what the return types for those functions will be.

Since the functions that we've defined are already defined on NetworkClient, we can update our NetworkClient to conform to Networking.

class NetworkClient: Networking {
  // No changes to the implementation
}

In our application code, we can pretty much use this network client passage to our feed view model and nothing would really change. This is a really low-key way to introduce testability into our codebase for the feed view model.

Mocking the network in a test

Now let's go ahead and write a test that sets up our feed view model so that we can start testing it.

class MockNetworkClient: Networking {
  func fetchPosts() async throws -> [Post] {
    return []
  }

  func createPost(withContents contents: String) async throws -> Post {
    return Post(id: UUID(), contents: contents)
  }
}

struct FeedViewModelTests {
  @Test func testFetchPosts() async throws {
    let viewModel = FeedViewModel(network: MockNetworkClient())

    // we can now start testing the view model
  }
}

Now that we have a setup that we can test, it's time to take another look at our testing goals for the view model. These testing goals are what's going to drive our decisions for what we'll put in our MockNetworkClient.

Writing our tests

These are the tests that I wanted to write for my post fetching logic:

  • Make sure that I can fetch posts
  • Make sure that posts get updated if the network returns new posts
  • Make sure that errors are handled correctly

Let’s start adding them one-by-one.

In order to test whether I can fetch posts, my mock network should probably return some posts:

class MockNetworkClient: Networking {
  func fetchPosts() async throws -> [Post] {
    return [
      Post(id: UUID(), contents: "This is the first post"),
      Post(id: UUID(), contents: "This is post number two"),
      Post(id: UUID(), contents: "This is post number three")
    ]
  }

  // ...
}

With this in place, we can test our view model to see if calling fetchPosts will actually use this list of posts and update the feed state correctly.

@Test func testFetchPosts() async throws {
  let viewModel = FeedViewModel(network: MockNetworkClient())

  await viewModel.fetchPosts()

  guard case .loaded(let posts) = viewModel.feedState else {
    Issue.record("Feed state is not set to .loaded")
    return
  }

  #expect(posts.count == 3)
}

The second test would have us call fetchPosts twice to make sure that we update the list of posts in the view model.

In order for us to control our tests fully, we should probably have a way to tell the mock network what list of posts it should return when we call fetchPost. Let’s add a property to the mock that allows us to specify a list of posts to return from within our tests:

class MockNetworkClient: Networking {
  var postsToReturn: [Post] = []

  func fetchPosts() async throws -> [Post] {
    return postsToReturn
  }

  func createPost(withContents contents: String) async throws -> Post {
    return Post(id: UUID(), contents: contents)
  }
}

And now we can write our second test as follows:

@Test func fetchPostsShouldUpdateWithNewResponses() async throws {
  let client = MockNetworkClient()
  client.postsToReturn = [
    Post(id: UUID(), contents: "This is the first post"),
    Post(id: UUID(), contents: "This is post number two"),
    Post(id: UUID(), contents: "This is post number three")
  ]

  let viewModel = FeedViewModel(network: client)
  await viewModel.fetchPosts()

  guard case .loaded(let posts) = viewModel.feedState else {
    Issue.record("Feed state is not set to .loaded")
    return
  }

  #expect(posts.count == 3)

  client.postsToReturn = [
    Post(id: UUID(), contents: "This is a new post")
  ]

  await viewModel.fetchPosts()

  guard case .loaded(let posts) = viewModel.feedState else {
    Issue.record("Feed state is not set to .loaded")
    return
  }

  #expect(posts.count == 1)
}

The test is now more verbose but we are in full control over the responses that our mock network will provide.

Our third test for fetching posts is to make sure that errors are handled correctly. This means that we should apply another update to our mock. The goal is to allow us to define whether our call to fetchPosts should return a list of posts or throw an error. We can use Result for this:

class MockNetworkClient: Networking {
  var fetchPostsResult: Result<[Post], Error> = .success([])

  func fetchPosts() async throws -> [Post] {
    return try fetchPostsResult.get()
  }

  func createPost(withContents contents: String) async throws -> Post {
    return Post(id: UUID(), contents: contents)
  }
}

Now we can make our fetch posts calls succeed or fail as needed in the tests. Our tests would now need to be updated so that instead of just passing a list of posts to return, we're going to provide success with the list. Here's what that would look like for our first test (I’m sure you can update the longer test based on this example).

@Test func testFetchPosts() async throws {
  let client = MockNetworkClient()
  client.fetchPostsResult = .success([
    Post(id: UUID(), contents: "This is the first post"),
    Post(id: UUID(), contents: "This is post number two"),
    Post(id: UUID(), contents: "This is post number three")
  ])

  let viewModel = FeedViewModel(network: client)

  await viewModel.fetchPosts()

  guard case .loaded(let posts) = viewModel.feedState else {
    Issue.record("Feed state is not set to .loaded")
    return
  }

  #expect(posts.count == 3)
}

Data that we can provide a success or failure for our tests. We can actually go on ahead and tell our tests to throw a specific failure.

@Test func fetchPostsShouldUpdateWithErrors() async throws {
  let client = MockNetworkClient()
  let expectedError = NSError(domain: "Test", code: 1, userInfo: nil)
  client.fetchPostsResult = .failure(expectedError)

  let viewModel = FeedViewModel(network: client)
  await viewModel.fetchPosts()

  guard case .error(let error) = viewModel.feedState else {
    Issue.record("Feed state is not set to .error")
    return
  }

  #expect(error as NSError == expectedError)
}

We now have three tests that test our view model.

What's interesting about these tests is that they all depend on a mock network. This means that we're not relying on a network connection. But this also doesn't mean that our view model and network client are going to work correctly.

We haven't tested that our actual networking implementation is going to construct the exact requests that we expect it to create. In order to do this we can leverage something called URLProtocol.

Mocking responses with URLProtocol

Knowing that our view model works correctly is really good. However, we also want to make sure that the actual glue between our app and the server works correctly. That means that we should be testing our network client as well as the view model.

We know that we shouldn't be relying on the network in our unit tests. So how do we eliminate the actual network from our networking client?

One approach could be to create a protocol for URLSession and stuff everything out that way. It's an option, but it's not one that I like. I much prefer to use something called URLProtocol.

When we use URLProtocol to mock out our network, we can tell URLSession that we should be using our URLProtocol when it's trying to make a network request.

This allows us to take full control of the response that we are returning and it means that we can make sure that our code works without needing the network. Let's take a look at an example of this.

Before we implement everything that we need for our test, let's take a look at what it looks like to define an object that inherits from URLProtocol. I'm implementing a couple of basic methods that I will need, but there are other methods available on an object that inherits from URLProtocol.

I highly recommend you take a look at Apple's documentation if you're interested in learning about that.

Setting up ur URLProtocol subclass

For the tests that we are interested implementing, this is the skeleton class that I'll be working from:

class NetworkClientURLProtocol: URLProtocol {
  override class func canInit(with request: URLRequest) -> Bool {
    return true
  }

  override class func canonicalRequest(for request: URLRequest) -> URLRequest {
    return request
  }

  override func startLoading() {
    // we can perform our fake request here
  }
}

In the startLoading function, we're supposed to execute our fake network call and inform the client (which is a property that we inherit from URLProtocol) that we finished loading our data.

So the first thing that we need to do is implement a way for a user of our fake network to provide a response for a given URL. Again, there are many ways to go about this. I'm just going to use the most basic version that I can come up with to make sure that we don't get bogged down by details that will vary from project to project.

struct MockResponse {
  let statusCode: Int
  let body: Data
}

class NetworkClientURLProtocol: URLProtocol {
  // ...

  static var responses: [URL: MockResponse] = [:]
  static var validators: [URL: (URLRequest) -> Bool] = [:]
  static let queue = DispatchQueue(label: "NetworkClientURLProtocol")

  static func register(
    response: MockResponse, requestValidator: @escaping (URLRequest) -> Bool, for url: URL
  ) {
    queue.sync {
      responses[url] = response
      validators[url] = requestValidator
    }
  }

  // ...
}

By adding this code to my NetworkClientURLProtocol, I can register responses and a closure to validate URLRequest. This allows me to test whether a given URL results in the expected URLRequest being constructed by the networking layer. This is particularly useful when you’re testing POST requests.

Note that we need to make our responses and validators objects static. That's because we can't access the exact instance of our URL protocol that we're going to use before the request is made. So we need to register them statically and then later on in our start loading function we'll pull out the relevant response invalidator. We need to make sure that we synchronize this through a queue so we have multiple tests running in parallel. We might run into issues with overlap.

Before we implement the test, let’s complete our implementation of startLoading:

class NetworkClientURLProtocol: URLProtocol {
  // ...

  override func startLoading() {
    // ensure that we're good to...
    guard let client = self.client,
      let requestURL = self.request.url,
      let validator = validators[requestURL],
      let response = responses[requestURL]
    else { 
      Issue.record("Attempted to perform a URL Request that doesn't have a validator and/or response")
      return 
    }

        // validate that the request is as expected
    #expect(validator(self.request))

    // construct our response object
    guard let httpResponse = HTTPURLResponse(
      url: requestURL, 
      statusCode: response.statusCode, httpVersion: nil,
      headerFields: nil
    ) else {
      Issue.record("Not able to create an HTTPURLResponse")
      return 
    }

    // receive response from the fake network
    client.urlProtocol(self, didReceive: httpResponse, cacheStoragePolicy: .notAllowed)
    // inform the URLSession that we've "loaded" data
    client.urlProtocol(self, didLoad: response.body)
    // complete the request
    client.urlProtocolDidFinishLoading(self)
  }
}

The code contains comments on what we’re doing. While you might not have seen this kind of code before, it should be relatively self-explanatory.

Implementing a test that uses our URLProtocol subclass

Now that we’ve got startLoading implemented, let’s try and use this NetworkClientURLProtocol in a test…

class FetchPostsProtocol: NetworkClientURLProtocol { }

struct NetworkClientTests {
  func makeClient(with protocolClass: NetworkClientURLProtocol.Type) -> NetworkClient {
    let configuration = URLSessionConfiguration.default
    configuration.protocolClasses = [protocolClass]
    let session = URLSession(configuration: configuration)
    return NetworkClient(urlSession: session)
  }

  @Test func testFetchPosts() async throws {
    let networkClient = makeClient(with: FetchPostsProtocol.self)

    let returnData = try JSONEncoder().encode([
      Post(id: UUID(), contents: "This is the first post"),
      Post(id: UUID(), contents: "This is post number two"),
      Post(id: UUID(), contents: "This is post number three"),
    ])

    let fetchPostsURL = URL(string: "https://practicalios.dev/posts")!

    FetchPostsProtocol.register(
      response: MockResponse(statusCode: 200, body: returnData),
      requestValidator: { request in
        return request.url == fetchPostsURL
      },
      for: fetchPostsURL
    )

    let posts = try await networkClient.fetchPosts()
    #expect(posts.count > 0)
  }
}

The first thing I'm doing in this code is creating a new subclass of my NetworkClientProtocol. The reason I'm doing that is because I might have multiple tests running at the same time.

For that reason, I want each of my Swift test functions to get its own class. This might be me being a little bit paranoid about things overlapping in terms of when they are called, but I find that this creates a nice separation between every test that you have and the actual URLProtocol implementation that you're using to perform your assertions.

The goal of this test is to make sure that when I ask my network client to go fetch posts, it actually performs a request to the correct URL. And given a successful response that contains data in a format that’s expected from the server’s response, we're able to decode the response data into a list of posts.

We're essentially replacing the server in this example, which allows us to take full control over verifying that we're making the correct request and also have full control over whatever the server would return for that request.

Testing a POST request with URLProtocol

Now let’s see how we can write a test that makes sure that we’re sending the correct request when we’re trying to create a post.

struct NetworkClientTests {
  // ...

  @Test func testCreatePost() async throws {
    let networkClient = makeClient(with: CreatePostProtocol.self)

    // set up expected data
    let content = "This is a new post"
    let expectedPost = Post(id: UUID(), contents: content)
    let returnData = try JSONEncoder().encode(expectedPost)
    let createPostURL = URL(string: "https://practicalios.dev/create-post")!

    // register handlers
    CreatePostProtocol.register(
      response: MockResponse(statusCode: 200, body: returnData),
      requestValidator: { request in
        // validate basic setup
        guard 
          let httpBody = request.streamedBody,
          request.url == createPostURL,
          request.httpMethod == "POST" else {
            Issue.record("Request is not a POST request or doesn't have a body")
            return false
        }

        // ensure body is correct
        do {
          let decoder = JSONDecoder()
          let body = try decoder.decode([String: String].self, from: httpBody)
          return body == ["contents": content]
        } catch {
          Issue.record("Request body is not a valid JSON object")
          return false
        }
      },
      for: createPostURL
    )

    // perform network call and validate response
    let post = try await networkClient.createPost(withContents: content)
    #expect(post == expectedPost)
  }
}

There's quite a lot of code here, but overall it follows a pretty similar step to before. There's one thing that I want to call your attention to, and that is the line where I extract the HTTP body from my request inside of the validator. Instead of accessing httpBody, I'm accessing streamedBody. This is not a property that normally exists on URLRequest, so let's talk about why I need that for a moment.

When you create a URLRequest and execute that with URLSession, the httpBody that you assign is converted to a streaming body.

So when you access httpBody inside of the validator closure that I have, it's going to be nil.

Instead of accessing that, we need to access the streaming body, gather the data, and return alll data.

Here's the implementation of the streamedBody property that I added in an extension to URLRequest:

extension URLRequest {
  var streamedBody: Data? {
    guard let bodyStream = httpBodyStream else { return nil }
    let bufferSize = 1024
    let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
    var data = Data()
    bodyStream.open()
    while bodyStream.hasBytesAvailable {
      let bytesRead = bodyStream.read(buffer, maxLength: bufferSize)
      data.append(buffer, count: bytesRead)
    }
    bodyStream.close()
    return data
  }
}

With all this in place, I'm able to now check that my network client constructs a fully correct network request that is being sent to the server and that if the server responds with a post like I expect, I'm actually able to handle that.

So at this point, I have tests for my view model (where I mock out the entire networking layer to make sure that the view model works correctly) and I have tests for my networking client to make sure that it performs the correct requests at the correct times.

In Summary

Testing code that has dependencies is always a little bit tricky. When you have a dependency you'll want to mock it out, stub it out, remove it or otherwise hide it from the code that you're testing. That way you can purely test whether the code that you're interested in testing acts as expected.

In this post we looked at a view model and networking object where the view model depends on the network. We mocked out the networking object to make sure that we could test our view model in isolation.

After that we also wanted to write some tests for the networking object itself. To do that, we used a URLProtocol object. That way we could remove the dependency on the server entirely and fully run our tests in isolation. We can now test that our networking client makes the correct requests and handles responses correctly as well.

This means that we now have end-to-end testing for a view model and networking client in place.

I don’t often leverage URLProtocol in my unit tests; it’s mainly in complex POST requests or flows that I’m interested in testing my networking layer this deeply. For simple requests I tend to run my app with Proxyman attached and I’ll verify that my requests are correct manually.

Testing completion handler based code in Swift Testing

Swift's new modern testing framework is entirely driven by asynchronous code. This means that all of our test functions are async and that we have to make sure that we perform all of our assertions “synchronously”.

This also means that completion handler-based code is not as straightforward to test as code that leverages structured concurrency.

In this post, we’ll explore two approaches that can be useful when you’re testing code that uses callbacks or completion handlers in Swift Testing.

First, we’ll look at the built-in confirmation method from the Swift Testing framework and why it might not be what you need. After that, we’ll look at leveraging continuations in your unit tests to test completion handler based code.

Testing async code with Swift Testing’s confirmations

I will start this section by stating that the main reason that I’m covering confirmation is that it’s present in the framework, and Apple suggests it as an option for testing async code. As you’ll learn in this section, confirmation is an API that’s mostly useful in specific scenarios that, in my experience, don’t happen all that often.

With that said, let’s see what confirmation can do for us.

Sometimes you'll write code that runs asynchronously and produces events over time.

For example, you might have a bit of code that performs work in various steps, and during that work, certain progress events should be sent down an AsyncStream.

As usual with unit testing, we're not going to really care about the exact details of our event delivery mechanism.

In fact, I'm going to show you how this is done with a closure instead of an async for loop. In the end, the details here do not matter. The main thing that we're interested in right now is that we have a process that runs and this process has some mechanism to inform us of events while this process is happening.

Here are some of the rules that we want to test:

  • Our object has an async method called createFile that kicks of a process that involves several steps. Once this method completes, the process is finished too.
  • The object also has a property onStepCompleted that we can assign a closure to. This closure is called for every completed step of our process.

The onStepCompleted closure will receive one argument; the completed step. This will be a value of type FileCreationStep:

enum FileCreationStep {
  case fileRegistered, uploadStarted, uploadCompleted
}

Without confirmation, we can write our unit test for this as follows:

@Test("File creation should go through all three steps before completing")
func fileCreation() async throws {
  var completedSteps: [FileCreationStep] = []
  let manager = RemoteFileManager(onStepCompleted: { step in
    completedSteps.append(step)
  })

  try await manager.createFile()
  #expect(completedSteps == [.fileRegistered, .uploadStarted, .uploadCompleted])
}

We can also refactor this code and leverage Apple’s confirmation approach to make our test look as follows:

@Test("File creation should go through all three steps before completing")
func fileCreation() async throws {
  try await confirmation(expectedCount: 3) { confirm in 
    var expectedSteps: [FileCreationStep] = [.fileRegistered, .uploadStarted, .uploadCompleted]

    let manager = RemoteFileManager(onStepCompleted: { step in
      #expect(expectedSteps.removeFirst() == step)
      confirm()
    })

    try await manager.createFile()
  }
}

As I’ve said in the introduction of this section; confirmation's benefits are not clear to me. But let’s go over what this code does…

We call confirmation and we provide an expected number of times we want a confirmation event to occur.

Note that we call the confirmation with try await.

This means that our test will not complete until the call to our confirmation completes.

We also pass a closure to our confirmation call. This closure receives a confirm object that we can call for every event that we receive to signal an event has occurred.

At the end of my confirmation closure I call try await manager.createFile(). This kicks off the process and in my onStepCompleted closure I verify that I’ve received the right step, and I signal that we’ve received our event by calling confirm.

Here’s what’s interesting about confirmation though…

We must call the confirm object the expected number of times before our closure returns.

This means that it’s not usable when you want to test code that’s fully completion handler based since that would mean that the closure returns before you can call your confirmation the expected number of times.

Here’s an example:

@Test("File creation should go through all three steps before completing")
func fileCreationCompletionHandler() async throws {
  await confirmation { confirm in 
    let expectedSteps: [FileCreationStep] = [.fileRegistered, .uploadStarted, .uploadCompleted]
    var receivedSteps: [FileCreationStep] = []

    let manager = RemoteFileManager(onStepCompleted: { step in
      receivedSteps.append(step)
    })

    manager.createFile {
      #expect(receivedSteps == expectedSteps)
      confirm()
    }
  }
}

Notice that I’m still awaiting my call to confirmation. Instead of 3 I pass no expected count. This means that our confirm should only be called once.

Inside of the closure, I’m running my completion handler based call to createFile and in its completion handler I check that we’ve received all expected steps and then I call confirm() to signal that we’ve performed our completion handler based work.

Sadly, this test will not work.

The closure returns before the completion handler that I’ve passed to createFile has been called. This means that we don’t call confirm before the confirmation’s closure returns, and that results in a failing test.

So, let’s take a look at how we can change this so that we can test our completion handler based code in Swift Testing.

Testing completion handlers with continuations

Swift concurrency comes with a feature called continuations. If you are not familiar with them, I'd highly recommend that you read my post where I go into how you can use continuations. For the remainder of this section, I'm going to assume that you know continuations basics. I will just look at how they work in the context of Swift testing.

The problem that we're trying to solve is essentially that we do not want our test function to return until our completion handler based code has fully executed. In the previous section, we saw how using a confirmation doesn't quite work because the confirmation closure returns before the file managers create file finishes its work and calls its completion handler.

Instead of a confirmation, we can have our test wait for a continuation. Inside of the continuation, we can call our completion handler based APIs and then resume the continuation when our callback is called and we know that we've done all the work that we need to do. Let's see what that looks like in a test.

@Test("File creation should go through all three steps before completing")
func fileCreationCompletionHandler() async throws {
  await withCheckedContinuation { continuation in
    let expectedSteps: [FileCreationStep] = [.fileRegistered, .uploadStarted, .uploadCompleted]
    var receivedSteps: [FileCreationStep] = []

    let manager = RemoteFileManager(onStepCompleted: { step in
      receivedSteps.append(step)
    })

    manager.createFile {
      #expect(receivedSteps == expectedSteps)
      continuation.resume(returning: ())
    }
  }
}

This test looks very similar to the test that you saw before, but instead of waiting for a confirmation, we're now calling the withCheckedContinuation function. Inside of the closure that we passed to that function, we perform the exact same work that we performed before.

However, in the createFile function’s completion handler, we resume the continuation only after we've made sure that the received steps from our onStepCompleted closure match with the steps to be expected.

So we're still testing the exact same thing, but this time our test is actually going to work. That's because the continuation will suspend our test until we resume the continuation.

When you're testing completion handler based code, I usually find that I will reach for this instead of reaching for a confirmation because a confirmation does not work for code that does not have something to await.

In Summary

In this post, we explored the differences between continuations and confirmations for testing asynchronous code.

You've learned that Apple's recommended approach for testing closure based asynchronous code is with confirmations. However, in this post, we saw that we have to call our confirm object before the confirmation closure returns, so that means that we need to have something asynchronous that we await for, which isn't always the case.

Then I showed you that if you want to test a more traditional completion handler based API, which is probably what you're going to be doing, you should be using continuations because these allow our tests to suspend.

We can resume a continuation when the asynchronous work that we were waiting for is completed and we’ve asserted the results of our asynchronous work are what we’d like them to be using the #expect or #require macros.

Testing requirements with #require in Swift Testing

In a previous post, I wrote about using the #expect macro to ensure that certain assertions you want to make about your code are true. We looked at testing boolean conditions as well as errors.

In this post, I would like to take a look at a macro that goes hand-in-hand with #expect and that is the #require macro.

The #require macro is used to ensure that certain conditions in your test are met, and to abort your test if these conditions are not met. The key difference between #expect and #require is that #expect will not cause a failed assertion to stop the test.

#require is much stricter. If we find one assertion to be untrue inside of the #require macro, we end the test because we don't think it makes sense to test any further.

In this post, we'll take a look at several applications of the #require macro. For example, we'll use #require to ensure that an optional value can be unwrapped. We'll also see how you can use #require to ensure that a specific error is or is not thrown. And of course, we'll also look at boolean conditions inside of #require.

Let's start by looking at Optional.

Unwrapping optionals with #require

Sometimes in our code we will have optional values. They're pretty much unavoidable in Swift and they're actually a really useful tool. In your test, it is quite likely that you'll want to make sure that a certain value exists before proceeding with your test. One way to do this would be to use the #expect macro and ensure that some property or value is not nil.

However, sometimes you'll want to take your optional value and use it as input for something else or you want to do further testing with that object. In that case, it makes sense to abort your test entirely if the optional happens to be nil.

We can use the #require macro for this, here’s how:

@Test func userIsReturned() async throws {
  let userStore = UserInfoStore()
  let user = User(name: "John")

  userStore.addUser(user: user)

  let returnedUser = try #require(userStore.getUser(withName: "John"), "User store should return the user that was added")
  #expect(returnedUser == user, "User store should return the user that was added")
}

The magic here is on the line where we create our let returnedUser. We use the #require macro and we call it with the try keyword.

That's because if the #require macro fails to unwrap the optional that is returned by getUser, the macro will throw an error and so our test will actually fail. This is quite useful when you really don't want to continue your test if whatever you're trying to require isn't there.

So in this case I want to compare the return user with the one that I've tried to store. I cannot do that if the user isn't there. So I want my test to not just fail when the optional that's returned by getUser is nil, I want this test case to end.

Now let’s imagine that I also want to end my test if the returned user and the stored user aren’t the same…

Checking boolean conditions with #require

In the previous section I used the following to line to make sure that my getUser function returned the correct user:

#expect(returnedUser == user, "User store should return the user that was added")

Notice how I'm using #expect to compare my returned user to my stored user.

This expectation will allow my test to continue running even if the expectation fails. This would allow me to perform multiple assertions on an object. For example, if I were to check whether the user name, the user's ID, and a bunch of other properties match, I would use #expect so that I can perform all assertions and see exactly which ones failed.

In this case I would want my test to fail and end if I didn’t get the right user back.

So I'm comparing the two users like before and I’ve replaced my #expect with #require. Here's what that looks like in a full test.

@Test func userIsReturned() async throws {
  let userStore = UserInfoStore()
  let user = User(name: "John")

  userStore.addUser(user: user)

  let returnedUser = try #require(userStore.getUser(withName: "John"), "User store should return the user that was added")
  try #require(returnedUser == user, "User store should return the user that was added")
  print("this won't run if I got the wrong user")
}

Notice that I had to prefix my #require with the try keyword, just like I had for getting my returned user on the line before.

The reason for that is if I didn't get the right user back and it doesn't match with the user that I just stored, my test will throw an error and end with a failure.

Overall, the APIs for #require and #expect are pretty similar, with the key difference being that #require needs the try keyword and your test ends if a requirement isn't met.

Now that we've seen how we can use this to unwrap optionals and check boolean conditions, the next step is to see how we can use it to check for certain errors being thrown.

Checking errors with #require

If you know how to check for errors with the #expect macro, you basically know how to it do with the #require macro too.

The key difference being once again if a requirement is not met your test case will stop.

If you want to learn more about checking for errors, I urge you to take a look at my blog post on the #expect macro. I don't want to duplicate everything that's in there in this post, so for an in-depth overview, you can take a look at that post.

In this post, I would just like to give you a brief rundown of what it looks like to check for errors with the #require macro.

So first let's see how we can assert that certain function throws an expected error with the #require macro.

I will be using the same example that I used in the previous post. We're going to check that giving an incorrect input to an object will actually throw the error that I want to receive.

@Test func errorIsThrownForIncorrectInput() async throws {
  let input = -1

  try #require(throws: ValidationError.valueTooSmall(margin: 1), "Values between 0 and 100 should be okay") {
    try checkInput(input)
  }
}

In this specific example, it might not make a ton of sense to use #require over #expect. However, if I were to have more code after this assertion and it wouldn't make sense to continue my test if the wrong error was thrown, then it makes total sense for me to use #require because I want to abandon the test because there's no point in continuing on.

Similar to the #expect macro, we can pass a specific error (like I did in the example above) or an error type (like ValidationError.self). If we want to assert that no error is thrown, we could pass Never.self as the error. type to make sure that our function call does not throw.

Similar to the #expect macro, you can use the #require macro to check whether a certain expression throws an error based on a more complicated evaluation.

For all the different overloads that exist on #require, I would like to redirect you to the #expect macro post because they are exactly the same for #require and #expect. The key difference is what happens when the assertion fails: #expect will allow your test to continue, but it will fail with an error on the line where your assertion failed. With #require, your test case will simply end on the line where something that you didn't expect actually happened.

In Summary

Overall, I quite like that Swift testing allows us to have a loose checking for assertions in the #expect macro, where we can validate that certain things are or are not correct without failing the entire test. That would allow you to make a whole bunch of assertions and see which ones fail, fixing one problem at a time (running your test again, fixing the next problem that shows up) is tedious.

The #require macro is really nice when you pretty much rely on something to be returned or something to be true before you can proceed.

For example, unwrapping an optional if you want to use whatever you're trying to unwrap to run further code and perform further assertions. It makes no sense to continue your test because you know that every single assertion that comes after it will fail, so I really like using #require for those kinds of situations and #expect for the ones where I can continue my test to collect more information about the results.

Asserting state with #expect in Swift Testing

I don't think I've ever heard of a testing library that doesn't have some mechanism to test assertions. An assertion in the context of testing is essentially an assumption that you have about your code that you want to ensure is correct.

For example, if I were to write a function that's supposed to add one to any given number, then I would want to assert that if I put 10 into that function I get 11 out of it. A testing library that would not be able to do that is not worth much. And so it should be no surprise at all that Swift testing has a way for us to perform assertions.

Swift testing uses the #expect macro for that.

In this post, we're going to take a look at the #expect macro. We'll get started by using it for a simple Boolean assertion and then work our way up to more complex assertions that involve errors.

Testing simple boolean conditions with #expect

The most common way that you're probably going to be using #expect is to make sure that certain conditions are evaluated to betrue. For example, I might want to test that the function below actually returns 5 whenever I call it.

func returnFive() -> Int {
  return 0
}

Of course this code is a little bit silly, it doesn't really do that much, but you could imagine that a more complicated piece of code would need to be tested more thoroughly.

Since I haven't actually implemented my returnFive function yet, it just returns 0. What I can do now is write a test as shown below.

@Test func returnFiveWorks() async throws {
  let functionOutput = Incrementer().returnFive()
  #expect(5 == functionOutput)
}

This test is going to test that when I call my function, we get number 5 back. Notice the line where it says #expect(5 == functionOutput).

That is an assertion.

I am trying to assert that 5 equals the output of my function by using the #expect macro.

When our function returns 5, my expression (5 == functionOutput) evaluated to true and the test will pass. When the expression is false, the test will fail with an error that looks a bit like this:

Expectation failed: 5 == (functionOutput → 0)

This error will show up as an error on the line of code where the expectation failed. That means that we can easily see what went wrong.

We can provide more context to our test failures by adding a comment. For example:

@Test func returnFiveWorks() async throws {
  let functionOutput = Incrementer().returnFive()
  #expect(5 == functionOutput, "returnFive() should always return 5")
}

If we update our tests to look a little bit more like this, if the test fails we will see an output that is a bit more elaborate (as you can see below).

Expectation failed: 5 == (functionOutput → 0)
returnFive() should always return 5

I always like to write a comment in my expectations because this will provide a little bit more context about what I expected to happen, making debugging my code easier in the long run.

Generally speaking, you're either going to be passing one or two arguments to the expect macro:

  1. The first argument is always going to be a Boolean value
  2. A comment that will be shown upon test failure

So in the test you saw earlier, I had my comparison between 5 and the function output inside of my expectation macro as follows:

5 == functionOutput

If I were to change my code to look like this where I put the comparison outside of the macro, the output of my failing test is going to look a little bit different. Here's what it will look like:

@Test func returnFiveWorks() async throws {
  let functionOutput = Incrementer().returnFive()
  let didReturnFive = 5 == functionOutput
  #expect(didReturnFive, "returnFive() should always return 5")
}

// produces the following failure message:
// Expectation failed: didReturnFive
// returnFive() should always return 5

Notice how I'm not getting any feedback right now about what might have gone wrong. I simply get a message that says "Expectation failed: didReturnFive" and no context as to what exactly might have gone wrong.

I always recommend trying to put your expressions inside the expect macro because that is simply going to make your test output a lot more useful because it will inspect variables that you inserted into your expect macro and it will say "you expected 5 but you've got 0".

In this case I only know that I did not get 5, which is going to be a lot harder to debug.

We can even have multiple variables that we're using inside of expect and have the testing framework tell us about those as well.

So imagine I have a function where I input a number and the amount that I want to increment the number by. And I expect the function to perform the math increment the input by the amount given. I could write a test that looks like this.

@Test func incrementWorks() async throws {
  let input = 1
  let incrementBy = 2
  let functionOutput = Incrementer().increment(input: input, by: incrementBy)
  #expect(functionOutput == input + incrementBy, "increment(input:by:) should add the two numbers together")
}

This test defines an input variable and the amount that I want to increment the first variable by.

It passes them both to an increment function and then does an assertion that checks whether the function output equals the input plus the increment amount. If this test fails, I get an output that looks as follows:

Expectation failed: (functionOutput → 4) == (input + incrementBy → 3)
increment(input:by:) should add the two numbers together

Notice how I quite conveniently see that my function returned 4, and that is not equal to input + increment (which is 3). It's really like this level of detail in my failure messages.

It’s especially useful when you pair this with the test arguments that I covered in my post on parameterized testing. You can easily see a clear report on what your inputs were, what the output was, and what may have gone wrong for each different input value.

In addition to boolean conditions like we’ve seen so far, you might want to write tests that check whether or not your function threw an error. So let's take a look at testing for errors using expect next.

Testing for errors with #expect

Sometimes, the goal of a unit test isn't necessarily to check that the function produces the expected output, but that the function produces the expected error or that the function simply doesn't throw an error. We can use the expect macro to assert this.

For example, I might have a function that throws an error if my input is either smaller than zero or larger than 50. Here's what that test could look like with the expect macro:

@Test func errorIsThrownForIncorrectInput() async throws {
  let input = -1
  #expect(throws: ValidationError.valueTooSmall, "Values less than 0 should throw an error") {
    try checkInput(input)
  }
}

The syntax for the expect macro when you're using it for errors is slightly different than you might expect based on what the Boolean version looked like. This macro comes in various flavors, and I prefer the one you just saw for my general purpose error tests.

The first argument that we pass is the error that we expect to be thrown. The second argument that we pass is the comment that we want to print whenever something goes wrong. The third argument is a closure. In this closure we run the code that we want to check thrown errors for.

So for example in this case I'm calling try checkInput which means that I expect that code to throw the error that I specified as the first argument in my #expect.

If everything works as expected and checkInput throws an error, my test will pass as long as that error matches ValidationError.valueTooSmall.

Now let's say that I accidentally throw a different error for this function the output will look a little bit like this

Expectation failed: expected error "valueTooSmall" of type ValidationError, but "valueTooLarge" of type ValidationError was thrown instead
Values less than 0 should throw an error

Notice how the message explains exactly which error we received (valueTooLarge) and the error that we expected (valueTooSmall). It's quite convenient that the #expect macro will actually tell us what we received and what we expected, making it easy to figure out what could have gone wrong.

Adding a little comment just like we did with the Boolean version makes it easier to reason about what we expected to happen or what could be happening.

If the test does not throw an error at all, the output would look as shown below

ExpectMacro.swift:42:3: Expectation failed: an error was expected but none was thrown
Values less than 0 should throw an error

This error pretty clearly tells us that no error was thrown while we did expect an error to be thrown.

But what if we want to ensure that a certain function call does not throw an error?

For one, you can just call that function in your test:

@Test func noErrorIsThrown() async throws {
  try checkInput(5)
}

This is great, but when you're working with parameterized tests you'd have to write two logic paths in your test. It's not a huge deal but we can do better.

You can check for Never being thrown to check that no error is thrown in your test. Here's how that looks:

@Test func noErrorIsThrown() async throws {
  let input = 5
  #expect(throws: Never.self, "Values between 0 and 100 should be okay") {
    try checkInput(input)
  }
}

There could also be situations where you don't really care about the exact error being thrown, but just that an error of a specific type was thrown. For example, I might not care that my "value too small" or "value too large" error was thrown, but I do care that the type of error that got thrown was a validation error. I can write my test like this to check for that.

@Test func errorIsThrownForIncorrectInput() async throws {
  let input = -1
  #expect(throws: ValidationError.self, "Values less than 0 should throw an error") {
    try checkInput(input)
  }
}

Instead of specifying the exact case on validation error that I expect to be thrown, I simply pass ValidationError.self. This will allow my test to pass when any validation error is thrown. If for whatever reason I throw a different kind of error, the test would fail.

There's a third version of expect in relation to errors that we could use. This one would first allow us to specify a comment like we can in any expect. We can then pass a closure that we want to execute (e.g. calling try checkInput) and a second closure that receives whatever error we received. We can perform some checks on that and then we can return whether or not that was what we expected.

For example, if you have a bit more complicated setup where you're throwing an error with an associated value you might want to inspect the associated value as well. Here's what that could look like.

@Test func errorIsThrownForIncorrectInput() async throws {
  let input = -1
  #expect {
    try checkInput(input)
  } throws: { error in 
    guard let validationError = error as? ValidationError else {
      return false
    }

    switch validationError {
    case .valueTooSmall(let margin) where margin == 1:
      return true
    default:
      return false
    }
  }
}

In this case, our validation logic for the error is pretty basic, but we could expand this in the real world. This is really useful when you have a complicated error or complicated logic to determine whether or not the error was exactly what you expected.

Personally, I find that in most cases I have pretty straightforward error checking, so I’m generally using the very first version of expect that you saw in this section. But I've definitely dropped down to this one when I wanted to inspect more complicated conditions to determine whether or not I got what I expected from my error.

What you need is, of course, going to depend on your own specific situation, but know that there are three versions of expect that you can use when checking for errors, and that they all have sort of their own downsides that you might want to take into account.

In Summary

Usually, I evaluate testing libraries by how powerful or expressive their assertion APIs are. Swift Testing has done a really good job of providing us with a pretty basic but powerful enough API in the #expect macro. There's also the #require macro that we'll talk about more in a separate post, but the #expect macro on its own is already a great way to start writing unit tests. It provides a lot of context about what you're doing because it's a macro and it will expand into a lot more information behind the scenes. The API that we write is pretty clear, pretty concise, and it's powerful for your testing needs.

Make sure to check out this category of Swift testing on my website because I had a lot of different posts with Swift testing, and I plan to expand this category over time. If there's anything you want me to talk about in terms of Swift testing, make sure you find me on social media, I’d love to hear from you.

Improving test coverage with parameterized tests in Swift testing

When you subscribe to the practice of test-driven development or just writing tests in general you'll typically find that you're going to be writing lots and lots of tests for pretty much everything in your codebase.

This includes testing that varying inputs on the same function or on the same object result in expected behavior. For example, if you have a function that takes user input and you want to make sure that you validate that a user has not entered a number greater than 100 or smaller than 0, you're going to want to test this function with values like -10, 0, 15, 90, 100, and 200 for example.

Writing a test for each input by hand will be quite repetitive and you're going to do pretty much the exact same things over and over. You'll have the same setup code, the same assertions, and the same teardown for every function. The difference is that for some inputs you might expect an error to be thrown, and for other inputs you might expect your function to return a value.

The behavior you’re testing is the same every single time.

If you prefer learning through video, this one's for you:

With Swift testing we can avoid repetition by through parameterized tests.

This means that we can run our tests multiple times with any number of predefined arguments. For example, you could pass all the values I just mentioned along with the error (if any) that you expect your function to throw.

This makes it quite easy for you to add more and more tests and in turn improve your test coverage and improve your confidence that the code does exactly what you want it to. This is a really good way to make sure that you're not accidentally adding bad code to your app because your unit tests simply weren’t extensive enough.

A plain test in Swift testing looks a little bit like this:

@Test("Verify that 5 is valid input")
func testCorrectValue() throws {
  #expect(try Validator.validate(input: 5), "Expected 5 to be valid")
}

The code above shows a very simple test, it passes the number 5 to a function and we expect that function to return true because 5 is a valid value.

In the code below we've added a second test that makes sure that entering -10 will throw an error.

@Test("Verify that -10 is invalid input")
func testTooSmall() throws {
  #expect(throws: ValidationError.valueTooSmall) {
    try Validator.validate(input: -10)
  }
}

As you can see the code is very repetitive and looks pretty much the same.

The only two differences are the input value and the error that is being thrown; no error versus a valueTooSmall error.

Here's how we can parameterize this test:

@Test(
  "Verify input validator rejects values smaller than 0 and larger than 100",
  arguments: [
    (input: -10, expectedError: ValidationError.valueTooSmall),
    (input: 0, expectedError: nil),
    (input: 15, expectedError: nil),
    (input: 90, expectedError: nil),
    (input: 100, expectedError: nil),
    (input: 200, expectedError: ValidationError.valueTooLarge),
  ]
)
func testRejectsOutOfBoundsValues(input: Int, expectedError: ValidationError?) throws {
  if let expectedError {
    #expect(throws: expectedError) {
      try Validator.validate(input: input)
    }
  } else {
    #expect(try Validator.validate(input: input), "Expected \(input) to be valid")
  }
}

We now have a list of values added to our test macro’s arguments. These values are passed to our test as function arguments which means that we can quite easily verify that all of these inputs yield the correct output.

Notice that my list of inputs is a list of tuples. The tuples contain both the input value as well as the expected error (or nil if I don’t expect an error to be thrown). Each value in my tuple becomes an argument to my test function. So if my tuples contain two values, my test should have two arguments.

Inside of the test itself I can write logic to have a slightly different expectation depending on my expected results.

This approach is really powerful because it allows me to easily determine that everything works as expected. I can add loads of input values without changing my test code, and this means that I have no excuse to not have an extensive test suite for my validator.

If any of the input values result in a failing test, Swift Testing will show me exactly which values resulted in a test failure which means that I’ll know exactly where to look for my bug.

In Summary

I think that parameterized tests are probably the feature of Swift testing that I am most excited about.

A lot of the syntax changes around Swift testing are very nice but they don't really give me that much new power. Parameterized testing on the other hand are a superpower.

Writing repetitive tests is a frustration that I've had with XCTest for a long time, and I've usually managed to work around it, but having proper support for it in the testing framework is truly invaluable.

Swift Testing basics explained

Swift testing is Apple's framework for running unit tests in a modern and more elegant way than it was with XCTest, which came before it. This post is the first one in a series of posts that will help you start using Swift Testing in your projects.

In this post, we'll take a look at the following topics:

  • Adding a Swift Testing to an existing project
  • Writing your first Swift test
  • Understanding Swift Testing syntax

Let's go ahead and dive right in and see what it takes to add a new Swift test to an existing project.

Adding a Swift Testing to an existing project

Adding a new Swift Testing based test to an existing project is surprisingly straightforward. If you already have a test target, all you need to do is add a new Swift file, import the testing framework, and start writing your tests.

In the past, if you would make a new test file, the skeleton for what you’d put in that file looks a bit like this:

import XCTest

final class ExampleXCTest: XCTestCase {
  override func setUpWithError() throws {

  }

  override func tearDownWithError() throws {

  }

  func testExample() throws {
    XCTAssertTrue(true, "This test will always pass")
  }
}

If you’ve worked with unit testing before, this should look familiar to you. It’s a very plain and simple example of what an XCTest based test can look like. All our tests are written inside of subclasses of XCTestCase, they can contain setup and teardown methods, and we write our tests in functions prefixed with the word “test”.

With Swift testing, all you need to do is add a new file, import the testing framework, and start writing unit tests.

You don't need to configure any build settings, you don't have to configure any project settings - all you have to do is add a new file and import the Testing framework, which is really convenient and allows you to experiment with Swift testing in existing projects even if the project already uses XCTest.

It's good to know that Swift Testing works with packages, executables, libraries, and any other project where you’re using Swift as you might expect.

Here's what the same skeleton looks like when we’re using for Swift Testing.

import Testing

@Test func swiftTestingExample() {
    // do setup
    #expect(true, "This test will always pass")
    // do teardown
}

We don’t need to wrap our test in a class, we don’t need a setup or teardown method, and we don’t need to prefix our test with the word “test”.

Notice that the test that I just showed is essentially an @Test macro applied to a function.

The @Test macro tells the testing framework that the function that's wrapped in the macro is a function that contains a test. We can also put these test functions inside of structs or classes if we want, but for simplicity I chose to show it as a function only which works perfectly well.

When you place your tests inside of an enclosing object, you still need to apply @Test to the functions that you want to run as your tests.

Let's say you choose to add your tests to a class. You can have setup and teardown logic in the initializer for your class because Swift testing will make a new instance of your class for every single test that it runs, meaning that you don't have to allow for one instance of the class to run all of your tests.

You know that you're going to always have a fresh instance for every single test, so you can set up in your initializer and tear down in a deinit.

If you're working with a struct, you can do the same thing, and this really makes Swift testing a very flexible framework because you get to pick and choose the correct type of object that you would like to use.

When in doubt, you're free to just use the kind of object that you prefer to use. If at any point in time you find that you do need something that only a class or struct could provide, you can always switch and use that instead.

Personally, I prefer classes because of their deinit where I can put any shared cleanup logic.

In the next section, I'd like to take a bit of a deeper look at structuring your tests and the kinds of things that we can do inside of a test, so let's dig into writing your first Swift test.

Writing your first Swift test

You've just seen your first test already. It was a free-floating function annotated with the @Test macro. Whenever you write a Swift test function, you're going to apply the test macro to it. This is different from XCTest where we had to prefix all of our test functions with the word "test".

Writing tests with the @Test macro is a lot more convenient because it allows us to have cleaner function names, which I really like.

Let’s grab the test from earlier and put that inside of a class. This will allow us to move shared setup and teardown logic to their appropriate locations.

class MyTestSuite {
  init() {
    // do setup
    print("doing setup")
  }

  deinit {
    // do teardown
    print("doing teardown")
  }

  @Test func testWillPass() {
    print("running passing test")
    #expect(true, "This test will always pass")
  }

  @Test func testWillFail() {
    print("running failing test")
    #expect(1 == 2, "This test will always fail")
  }
}

The code above shows two tests in a single test suite class. In Swift testing, we call enclosing classes and structs suites, and they can have names (which we'll explore in another post). For now, know that this test suite is called "MyTestSuite" (just like the class name).

If we run this test, we see the doing setup line print first, then we see that we're running the passing test, followed by the teardown. We're going to see another setup, another failing test, and then we'll see another teardown.

What's interesting is that Swift testing will actually run these tests in parallel as much as possible, so you might actually see two setups printed after each other or maybe a setup and a running test interleave depending on how fast everything runs. This is because Swift testing makes a separate instance of your test suite for every test function you have.

Having separate instances allows us to do setup in the initializer and teardown in the de-initializer.

If we expand this example here to something that's a little bit more like what you would write in the real world, here's what it could look like to test a simple view model that's supposed to fetch data for us.

class TestMyViewModel {
  let viewModel = ExercisesViewModel()

  @Test func testFetchExercises() async throws {
    let exercises = try await viewModel.fetchExercises()
    #expect(exercises.count > 0, "Exercises should be fetched")
  }
}

Because we're making new instances of my view model, I don't really have to put the initialization of the exercises view model in an initializer. I can just write let viewModel = ExercisesViewModel() to create my ExercisesViewModel instance right when the class is created. And I can use it in my test and know that it's going to be cleaned up after the test runs.

This is really nice.

What is important to keep in mind though is that the fact that Swift testing uses separate instances for each of my tests means that I cannot rely on any ordering or whatever of my tests, so every test has to run in complete isolation which is a best practice for unit testing anyway.

Inside of my test fetch exercises function, I can just take my let exercises and verify that it has more than zero items. If there are zero items, the test will fail because the expectation for my #expect macro evaluates to false.

I'd like to zoom in a little bit more on the syntax that I'm using here because the #expect macro is the second macro we’re looking at in addition to the @Test macro, so let's just take a really brief look at what kinds of macros we have available to us in the Swift testing framework.

Exploring the basics of Swift testing syntax

You've already seen some tests, so you're somewhat familiar with the syntax. You can recognize a Swift test by looking for the @Test macro. The @Test macro is used to identify individual tests, which means that we can give our functions any name that we want.

You have also seen the #expect macro. The #expect macro allows us to write our assertions in the form of expectations that are going to give us a boolean value (true or false) and we can add a label that shows us what should be presented in case of a failing test.

Before we take a deeper look at #expect, let's take a closer look at the @Test macro first. The @Test macro is used to signal that a certain function represents a test in our test suite.

We can pass some arguments to our @Test, one of these arguments is be a display name (to make a more human-readable version of our test). We can also pass test traits (which I'll cover in another post), and arguments (which I'll also cover in another post).

Arguments are the most interesting one in my opinion because they would allow you to actually run a test function multiple times with different input arguments. But like I said, that’s a topic for another day…

Let's stay on focus.

The display name that we can pass to a test macro can be used a little bit like this.

@Test("Test fetching exercises") 
func testFetchExercises() async throws {
  let exercises = try await viewModel.fetchExercises()
  #expect(exercises.count > 0, "Exercises should be fetched")
}

Now whenever this test runs, it will be labeled as the human-readable test "Fetching exercises" vs the function name. For a short test like this, that's probably not really needed, but for longer tests, it will definitely be useful to be able to give more human-readable names to your tests. I would advise that you use the display name argument on your tests wherever relevant.

The second building block of Swift testing that I'd like to look at now is the macro for expecting a certain state to be true or false. The #expect macro can take several kinds of arguments. It could take a statement that may or may not throw, or it could take a statement that will return a Boolean value. You've already seen the Bool version in action.

Sometimes you'll write tests where you want to ensure that calling a certain function with an incorrect input will throw a specific error. The expect macro can also handle that.

We can give it a specific error type that we expect to be thrown, a human readable failure message, and the expression to perform.

This expression is what we expect to throw the error that was defined as the first argument.

Here’s an example of using #expect to test for thrown errors.

@Test("Validate that an error is thrown when exercises are missing") func throwErrorOnMissingExercises() async {
  await #expect(
    throws: FetchExercisesError.noExercisesFound, 
    "An error should be thrown when no exercises are found", 
    performing: { try await viewModel.fetchExercises() })
}

I think these are the most useful things to know about the #expect macro because with just knowing how to leverage Bool expectations and knowing how to expect thrown errors, you're already able to write a very powerful tests.

In future posts, I will dig deeper into different macros and into setting up more complicated tests, but I think this should get you going with Swift testing very nicely.

In Summary

In this post, you've learned how you can get started with the Swift testing framework. You've seen that adding a new Swift test to an existing project is as simple as making a new file, importing the Swift testing framework, and writing your tests using the @Test macro. The fact that it's so easy to add Swift testing to an existing project makes me think that everybody should go and try it out as soon as possible.

Writing unit tests with Swift testing feels a lot quicker and a lot more elegant than it ever did with XCTest. You've also seen the basics of writing unit tests with Swift testing. I talked a little bit about the @Test macro and the #expect macro and how they can be used to both create more readable tests and to do more than just comparing booleans.

As I've mentioned several times, I will be writing more posts about Swift testing, so in those posts, we'll dig deeply into more advanced and different features of the testing framework. But for now, I think this is a great introduction that hopefully gets you excited for a new era in testing your Swift code.

Testing completion handler APIs with Swift Testing

The Swift testing framework is an incredibly useful tool that allows us to write more expressive tests with convenient and modern APIs.

This is my first post about Swift Testing, and I’m mainly writing it because I wanted to write about something that I encountered not too long ago when I tried to use Swift testing on a code base where I had both async code as well as older completion handler based code.

The async code was very easy to test due to how Swift Testing is designed, and I will be writing more about that in the future.

The completion handler base code was a little bit harder to test, mainly because I was converting my code from XCTest with test expectations to whatever the equivalent would be in Swift testing.

Understanding the problem

When I started learning Swift testing, I actually looked at Apple's migration document and I found that there is an something that’s supposed to be analogous to the expectation object, which is the confirmation object. The examples from Apple have one little caveat in there.

The Swift Testing example looks a little bit like this:

// After
struct FoodTruckTests {
  @Test func truckEvents() async {
    await confirmation("…") { soldFood in
      FoodTruck.shared.eventHandler = { event in
        if case .soldFood = event {
          soldFood()
        }
      }
      await Customer().buy(.soup)
    }
    ...
  }
  ...
}

Now, as you can see in the code above, the example that Apple has shows that we have a function and a call to the confirmation function in there, which is how we’re supposed to test our async code.

They call their old completion handler based API and in the event handler closure they call their confirmation closure (called soldFood in the example).

After calling setting the event handler they await Customer().buy(.soup).

And this is really where Apple wants us to pay close attention because in the migration document, they mention that we want to catch an event that happens during some asynchronous process.

The await that they have as the final line of that confirmation closure is really the key part of how we should be using confirmation.

When I tried to migrate my completion handler based code that I tested with XCTestExpectation, I didn't have anything to await. My original testing code looked a little bit like this:

func test_valueChangedClosure() {
  let expect = expectation(description: "Expected synchronizer to complete")

  let synchronizer = Synchronizer()
  synchronizer.onComplete = {
    XCTAssert(synchronizer.newsItems.count == 2)
    expect.fulfill()
  }

  synchronizer.synchronize()
  waitForExpectations(timeout: 1)
}

Based on the migration guide and skimming the examples I though that the following code would be the Swift Testing equivalent:

@Test func valueChangedClosure() async {    
  await confirmation("Synchronizer completes") { @MainActor confirm in
    synchronizer.onComplete = {
      #expect(synchronizer.newsItems.count == 2)
      confirm()
    }

    synchronizer.synchronize()
  }
}

My code ended up looking quite similar to Apple’s code but the key difference is the last line in my confirmation. I’m not awaiting anything.

The result when running this is always a failing test. The test is not waiting for me to call the confirm closure at all. That await right at the end in Apple’s sample code is pretty much needed for this API to be usable as a replacement of your expectations.

What Apple says in the migration guide when you carefully read is actually that all of the confirmations have to be called before your closure returns:

Confirmations function similarly to the expectations API of XCTest,
however, they don’t block or suspend the caller while waiting for a
condition to be fulfilled. Instead, the requirement is expected to be confirmed (the equivalent of fulfilling an expectation) before confirmation() returns

So whenever that confirmation closure returns, Swift Testing expects that we have confirmed all of our confirmations. In a traditional completion handler-based setup, this won't be the case because you're not awaiting anything because you don't have anything to await.

This was quite tricky to figure out.

Write a test for completion handler code

The solution here is to not use a confirmation object here because what I thought would happen, is that the confirmation would act a little bit like a continuation in the sense that the Swift test would wait for me to call that confirmation.

This is not the case.

So what I've really found is that the best way to test your completion handler-based APIs is to use continuations.

You can use a continuation to wrap your call to the completion handler-based API and then in the completion handler, do all of your assertions and resume your continuation. This will then resume your test and it will complete your test.

Here’s what that looks like as an example:

@Test func valueChangedClosure() async {
    await withCheckedContinuation { continuation in
        synchronizer.onComplete = {
            #expect(synchronizer.newsItems.count == 2)
            continuation.resume()
        }

        synchronizer.synchronize()
    }
}

This approach works very well for what I needed, and it allows me to suspend the test while my callback based code is running.

It's the simplest approach I could come up with, which is usually a good sign. But if you have any other approaches that you prefer, I would love to hear about them, especially when it relates to testing completion handler APIs. I know this is not a full-on replacement for everything that we can do with expectations, but for the completion handler case, I think it's a pretty good replacement.

When not to use continuations for testing completion handler code

The approach of testing outlined above assumes that our code is somewhat free of certain bugs where the completion handler is never called. Our continuation doesn't do anything to prevent our test from hanging forever which could (let's be honest, will) be an issue for certain scenarios.

There are code snippets out there that will get you the ability to handle timeouts, like the one found in this gist that was shared with me by Alejandro Ramirez.

I haven't done extensive testing with this snippet yet but a couple of initial tests look good to me.

What is dependency injection in Swift?

Code has dependencies. It’s something that I consider universally true in one way or another. Sometimes these dependencies are third party dependencies while other times you’ll have objects that depend on other objects or functionality to function. Even when you write a function that should be called with a simple input like a number, that’s a dependency.

We often don’t really consider the small things the be dependencies and this post will not focus on that at all. In an earlier post, I’ve written about using closures as dependencies, also known as protocol witnesses.

In this post I’d like to focus on explaining dependency injection for Swift. You’ll learn what dependency injection is, what types of dependency injection we have, and you’ll learn a bit about the pros and cons of the different approaches.

If you prefer learning through video, take a look here:

Understanding the basics of dependency injection

Dependency Injection (DI) is a design pattern that allows you to decouple components in your codebase by injecting dependencies from the outside, rather than hardcoding them within classes or structs.

For example, you might have a view model that needs an object to load user data from some data source. This could be the filesystem, the networking or some other place where data is stored.

Providing this data source object to your view model is dependency injection. There are several ways in which we can inject, and there are different ways to abstract these dependencies.

It’s fairly common for an object to not depend on a concrete implementation but to depend on a protocol instead:

protocol DataProviding {
  func retrieveUserData() async throws -> UserData
}

class LocalDataProvider: DataProviding {
  func retrieveUserData() async throws -> UserData {
    // read and return UserData
  }
}

class UserProfileViewModel {
  let dataProvider: DataProviding

  // this is dependency injection
  init(dataProvider: DataProviding) {
      self.dataProvider = dataProvider
  }
}

This code probably is something you’ve written at some point. And you might be surprised to find out that simply passing an instance of an object that conforms to DataProviding is considered dependency injection. It’s just one of several approaches you can take but in its simplest form, dependency injection is actually relatively simple.

Using dependency injection will make your code more modular, more reusable, more testable, and just overal easier to work with. You can make sure that every object you define in your code is responsible for a single thing which means that reasoning about parts of your codebase becomes a lot simpler than when you have lots of complex and duplicated logic that’s scattered all over the place.

Let’s take a closer look at initializer injection which is the form of dependency injection that’s used in the code above.

Initializer injection

Initializer injection is a form of dependency injection where you explicitly pass an object’s dependencies to its initializer. In the example you saw earlier, I used initializer injection to allow my UserProfileViewModel to receive an instance of an object that conforms to DataProviding as a dependency.

Passing dependencies around like this is most likely the simplest form of passing dependencies around. It doesn’t require any setup, there’s no third party solutions needed, and it’s all very explicit. For every object you’re able to see exactly what that object will depend on.

More importantly, it’s also a very safe way of injecting dependencies; you can’t create an instance of UserViewModel without creating and providing your data provider as well.

A downside of this approach of dependency injection is that an object might have dependencies that it doesn’t actually need. This is especially true in the view layer of your app.

Consider the example below:

struct MyApp: App {
  let dataProvider = LocalDataProvider()

  var body: some Scene {
    WindowGroup {
      MainScreen()
    }
  }
}

struct MainScreen: View {
  let dataProvider: DataProviding
  var body: some View {
    NavigationStack {
      // ... some views

      UserProfileView(viewModel: UserProfileViewModel(dataProvider: dataProvider))
    }
  }
}

In this example, we have an app that has a couple of views and one of our views needs a ProfileDataViewModel. This view model can be created by the view that sits before it (the MainView) but that does mean that the MainView must have the dependencies that are needed in order to create the ProfileDataViewModel. The result is that we’re creating views that have dependencies that they don’t technically need but we’re required to provide them because some view deeper in the view hierarchy does need that dependency.

In larger apps this might mean that you’re passing dependencies across several layers before they reach the view where they’re actually needed.

There are several approaches to fixing this. We could, for example, pass around an object in our app that is able to produce view models and other dependencies. This object would depend on all of our “core” objects and is capable of producing objects that need these “core” objects.

An object that’s able to do this is referred to as a factory.

For example, here’s what a view model factory could look like:

struct ViewModelFactory {
  private let dataProvider: DataProviding

  func makeUserProfileViewModel() -> UserProfileViewModel {
    return UserProfileViewModel(dataProvider: dataProvider)
  }

  // ...
}

Instead of passing individual dependencies around throughout our app, we could now pass our view model factory around as a means of fabricating dependencies for our views without making our views depend on objects they definitely don’t need.

We’re still passing a factory around all over the place which you may or may not like.

As an alternative approach, we can work around this with several tools like the SwiftUI Environment or a tool like Resolver. While these two tools are very different (and the details are out of scope for this post), they’re both a type of service locator.

So let’s go ahead and take a look at how service locators are used next.

Service locators

The service locator pattern is a design pattern that can be used for dependency injection. The way a service locator works is that almost like a dictionary that contains all of our dependencies.

Working with a service locator typically is a two-step process:

  1. Register your dependency on the locator
  2. Extract your dependency from the locator

In SwiftUI, this will usually mean that you first register your dependency in the environment and then take it out in a view. For example, you can look at the code below and see exactly how this is done.

extension EnvironmentValues {
  @Entry var dataProvider = LocalDataProvider()
}

struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      MainScreen()
      .environment(\.dataProvider, LocalDataProvider())
    }
  }
}

struct MainScreen: View {
  @Environment(\.dataProvider) var dataProvider

  var body: some View {
    NavigationStack {
      // ... some views

      UserProfileView(viewModel: UserProfileViewModel(dataProvider: dataProvider))
    }
  }
}

In this code sample, I register my view model and a data provider object on the environment in my app struct. Doing this allows me to retrieve this object from the environment wherever I want it, so I don't have to pass it from the app struct through potentially several layers of views. This example is simplified so the beneifts aren't huge. In a real app, you'd have more view layers, and you'd pass dependencies around a lot more.

With the approach above, I can put objects in the environment, build my view hierarchy and then extract whatever I need at the level where I need it. This greatly simplifies the amount of code that I have to write to get a dependency to where it needs to be and I won't have any views that have dependencies that they don't technically need (like I do with initializer injection).

The downside is that this approach does not really give me any compile-time safety.

What I mean by that is that if I forget to register one of my dependencies in the environment, I will not know about this until I try to extract that dependency at runtime. This is a pattern that will exist for any kind of service load configuration use, whether it's a SwiftUI environment or a third-party library like Resolver.

Another downside is that my dependencies are now a lot more implicit. This means that even though a view depends on a certain object and I can see that in the list of properties, I can create that object without putting anything in its environment and therefore getting crashes when I try to grab dependencies from the environment. This is fine in smaller apps because you're more likely to hit all the required patterns while testing, but in larger apps, this can be somewhat problematic. Again, we're lacking any kind of compile-time safety, and that's something that I personally miss a lot. I like my compiler to help me write safe code.

That said, there is a time and place for service locators, especially for things that either have a good default value or that are optional or that we inject into the app root and basically our entire app depends on it. So if we would forget, we'd see crashes as soon as we launch our app.

The fact that the environment or a dependency locator is a lot more implicit also means that we're never quite sure exactly where we inject things in the environment. If the only place we inject from is the abstract or the root of our application, it's pretty manageable to see what we do and don't inject. If we also make new objects and inject them in the middle of our view hierarchy, it becomes a lot trickier to reason about exactly where a dependency is created and injected. And more importantly, it also doesn't really make it obvious if at any point we overwrite a dependency or if we're injecting a fresh one.

This is something to keep in mind if you choose to make heavy use of a service locator like the SwiftUI environment.

In Summary

In short, dependency injection is a complicated term for a relatively simple concept.

We want to get dependencies into our objects, and we need some mechanism to do this. iOS historically doesn't do a lot of third-party frameworks or libraries for dependency injection, so most commonly you'll either use initializer injection or the SwiftUI environment.

There are third-party libraries that do dependency injection in Swift, but you most likely don’t need them.

Whether you use initializer injection or the service locator pattern, it's somewhat of a mix between a preference and a trade-off between compile-time safety and convenience.

I didn't cover things like protocol witnesses in this post because that is a topic that uses initializer injection typically, and it's just a different kind of object that you inject. If you want to learn more about protocol witnesses, I do recommend that you take a look at my blog post where I talk about using closures as dependencies.

I hope you enjoyed this post. I hope it taught you a lot about dependency injection. And do not hesitate to reach out to me if you have any questions or comments on this post.

Getting started with Mesh Gradients on iOS 18

With iOS 18, we have the possibility to create mesh gradients. Mesh gradients are a really nice way to create very cool UI effects. In this post, we're going to explore exactly what mesh gradients are, how we can use them, and how we can even animate them to look really cool.

We’ll start off looking at how we can create a mesh gradient. We're going to take a look at how it more or less works, and then we'll also look at what we can animate and how we can play with mesh gradients. At the end of the post, I'll talk a little bit about where I think it makes a lot of sense to use mesh gradients and where maybe it could be a little bit much.

Let's dig in, shall we?

Creating a mesh gradient

A mesh gradient is a gradient that doesn't just go from one color to another like a standard linear or radial gradient would. It goes through several colors in a kind of cloud formation looking way. Let's look at an example of a mesh gradient down below.

Example of a mesh gradient with nine colors

In this picture, we can see a gradient that uses nine colors to create an interesting effect where we go from that purple color in the top-left to a different color in the middle and then another color in the top-right and in the bottom-left, basically allowing us to have different colors inside of the gradient and they all mesh into each other like the name mesh gradient suggests.

We can create these gradients in SwiftUI by using the new mesh gradient object.

A mesh gradient needs a couple of things: a width and a height, which basically tell the mesh gradient the number of colors that we'll have on the horizontal and vertical axis.

For example, we could create mesh gradient that uses four colors instead of nine. That mesh would have a width of two and a height of two. This results in a mesh that has two rows with two columns each. We could also make it a gradient that has a width of 2 and a height of 3, which means that we're going to have two columns over three rows, so a total of six colors.

With a gradient like that, each color is placed at the edges of the mesh.

The placement of our colors is controlled by passing them to the MeshGradient's initializer.

Before I explain further, I think it's a good idea to take a look at the code that's needed to create a mesh gradient.

MeshGradient(
    width: 2,
    height: 2,
    points: [
        .init(x: 0, y: 0), .init(x: 1, y: 0),
        .init(x: 0, y: 1), .init(x: 1, y: 1),
    ] ,
    colors: [
        .red, .orange,
        .purple, .blue
    ]
)

In this code, you can see that the way that we create this gradient is by giving that width and height that we just talked about and then a list of points. This list of points tells the mesh gradient exactly where inside of the mesh each color exists.

So the first color in this case exists in the top left (0, 0), and then we move to the next point which is going to be top right which is (1, 0). What's interesting to note is that we specify these positions by columns and rows. So we start off top left and then the top right and then the next one becomes bottom left.

The positions we pass are always values between zero and one, and they represent a relative position within the mesh. No matter how large or small your gradient gets, the gradient is going to know exactly where everything should be based on our relative positions.

The fourth and last argument that you can see as well in the code snippet is colors. colors is a list of all the colors that we want to use. So if we have a width of 2 and a height of 2, then we specify 4 colors. The order of the colors the same as how our positioning works. So in this case, red, orange, purple, blue means:

  • Red is going to be top left
  • Orange is going to be top right
  • Purple is going to be bottom left
  • Blue is going to be bottom right

Even if we place the first color at something like, for example, the bottom right position the system is still going to calculate the gradients as if red is positioned (roughly) in the top left. Feel free to play around with this a little bit yourself to see what I mean.

In addition to providing whole values like 0 and 1, we can also provide decimal values like 0.5. The result is not always exactly aesthetically pleasing because if we, for eg., give our first color (the red color) an x value of 0.5, the result looks a little bit like this.

A gradient that was cut off

You can see that suddenly the gradient sorts of cuts off, which means that there's a straight line drawn from the bottom left position all the way up to the position wherever our red color is going to be, and we don't really cover the area that's remaining. So we're now left with a huge gap, and that's not necessarily desirable.

Typically, what you'll do is you'll have your gradients always sit at the corners. If we did want to play around with a color in the middle, we could actually make a gradient grid of 3x3 and then play around with that middle value. The gradient you saw at the start of this post could be adjusted a little bit and end up looking like this:

A gradient with offset mesh points

This effect was achieved by moving around all of the gradient points except those at the corners. The result is an even more interesting visual effect than the one you saw earlier.

That brings me to animating a mesh gradient. Let's go ahead and take a look at that next.

Animating a mesh gradient

We can make our mesh gradients animate using several techniques. I will be using a timer-based animation here because that is what I like to use most. If you prefer to use something else, you're completely free to do that, of course.

Let's go ahead and look at the code that creates our animation. You can see it down below. Overall, this code doesn't really do anything fancy. It just moves our gradient in a random direction within a scope around the base position. The effect is pretty cool. It kind of looks like there's a spotlight going around the canvas. I really like it. It's really not that hard to achieve, and that's really the power of the mesh gradient API.

struct ContentView: View {
    @State var positions: [SIMD2<Float>] = [
        .init(x: 0, y: 0), .init(x: 0.2, y: 0), .init(x: 1, y: 0),
        .init(x: 0, y: 0.7), .init(x: 0.1, y: 0.5), .init(x: 1, y: 0.2),
        .init(x: 0, y: 1), .init(x: 0.9, y: 1), .init(x: 1, y: 1)
    ]

    let timer = Timer.publish(every: 1/6, on: .current, in: .common).autoconnect()

    var body: some View {
        MeshGradient(
            width: 3,
            height: 3,
            points: positions,
            colors: [
                .purple, .red, .yellow,
                .blue, .green, .orange,
                .indigo, .teal, .cyan
            ]
        )
        .frame(width: 300, height: 200)
        .onReceive(timer, perform: { _ in
            positions[1] = randomizePosition(
                currentPosition: positions[1],
                xRange: (min: 0.2, max: 0.9),
                yRange: (min: 0, max: 0)
            )

            positions[3] = randomizePosition(
                currentPosition: positions[3],
                xRange: (min: 0, max: 0),
                yRange: (min: 0.2, max: 0.8)
            )

            positions[4] = randomizePosition(
                currentPosition: positions[4],
                xRange: (min: 0.3, max: 0.8),
                yRange: (min: 0.3, max: 0.8)
            )

            positions[5] = randomizePosition(
                currentPosition: positions[5],
                xRange: (min: 1, max: 1),
                yRange: (min: 0.1, max: 0.9)
            )

            positions[7] = randomizePosition(
                currentPosition: positions[7],
                xRange: (min: 0.1, max: 0.9),
                yRange: (min: 1, max: 1)
            )
        })
    }

    func randomizePosition(
        currentPosition: SIMD2<Float>,
        xRange: (min: Float, max: Float),
        yRange: (min: Float, max: Float)
    ) -> SIMD2<Float> {
        var updateDistance: Float = 0.01

        let newX = if Bool.random() {
            min(currentPosition.x + updateDistance, xRange.max)
        } else {
            max(currentPosition.x - updateDistance, xRange.min)
        }

        let newY = if Bool.random() {
            min(currentPosition.y + updateDistance, yRange.max)
        } else {
            max(currentPosition.y - updateDistance, yRange.min)
        }

        return .init(x: newX, y: newY)
    }
}

In addition to the code, I think it's interesting to take a look at the result, which is shown down below.

Animated gradient

Both the effect and the code are pretty simple examples of what we can do. There are a lot of other ways to achieve similar, the same or better results. So I hope this just provides a starting point for you, so that you know what you can do and to inspire you on how you could get started animating your mesh gradients.

There's not much else to say about mesh gradients and animating them.

To explore meshes and animations more, you could also play around with the points array and give it bezier points instead of plain points. That gets you more freedom and allows you to change how the mesh gradient interpolates how colors should blend. It is really hard to do this well, so I'm not going to dig into that too much.

I think if you're comfortable with bezier points, you're going to be able to use this. If you're not comfortable with that like me, it's going to be a lot harder. So yeah, not going to cover that one. Now that you've seen how to animate a mesh gradient, let's talk a little bit about where and when it makes sense to use them.

Where and when to use mesh gradients

Like any UI effect, mesh gradients can be applied tastefully and properly, or they can be applied in a very overbearing way, which basically makes them look bad in your UI. I think what's important to realize is that mesh gradients do take up a lot of visual space from the user. So it doesn't affect that you're going for, they make total sense. I also think that kind of makes sense as a background kind of view.

A really interesting effect that I've seen is to apply a little bit of a frosted overlay over your mesh gradient, which you can do by using the code below.

MeshGradient(
    width: 3,
    height: 3,
    points: positions,
    colors: [
        .purple, .red, .yellow,
        .blue, .green, .orange,
        .indigo, .teal, .cyan
    ]
)
.frame(width: 300, height: 200)
.overlay(.ultraThinMaterial)

If you do that, your gradient will be a bit more muted as shown in the picture down below.

Frosted glass effect gradient

If you apply the effect like that, what happens is that the mesh gradient becomes a lot more subtle and it really adds to your UI when used as a background view.

I would also make sure that you don't use colors that are too far apart. If the colors are somewhat similar, it creates this nice unified view, which is a lot more attractive to look at than something that is very out extreme like I did in my example.

Of course, it depends on what you're going for. But if you're going for something more subtle, that's what you want to do.

In Summary

To summarize what you've learned, in this post we took a look at mesh gradients. We looked at how they are written. You've seen how you can pass a list of points and a list of colors to create a mesh gradient quickly. You've also seen that mesh gradients allow you to move the positions of colors around and how making too extreme of an adjustment can result in strange looks (which maybe are exactly what you're looking for, probably not).

You've also seen how you can animate your mesh gradients. We wrap up the post by looking at how you can make sure that you apply mesh gradients in a tasteful manner instead of just going all in with them and going entirely overboard with something that's going to be way too much.

I think MeshGradient is going to be able to make some really cool UI effects. And I'm actually looking forward to apps implementing this because I would love to see how they make good use of this new API on iOS 18.