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.

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.

Animating SF Symbols on iOS 18

Over the years, Apple has been putting tons of work into the SF Symbols catalog. With SF Symbols, we’re able to leverage built-in iconography that will look familiar to users while also fitting into the Apple ecosystem very nicely. The fact that there’s thousands of symbols to choose from makes it a highly flexible and powerful catalog of icons that, in my opinion, should be every designer and developer’s first choice when they’re looking for visual components to add to their apps.

Initially, SF Symbols were pretty much static. We could configure them with a color and thickness but that was about it. Now that we’re a few years down the line, Apple has added multiple ways to animate SF Symbols.

In this post, I’d like to take a look at the current state of SF Symbol animations and explore some of the available animation options and styles that are available to us today.

If you prefer to learn through video instead of text, check out this video on my YouTube channel

Basic SF Symbol animations

Overall, the ways in which we can animate SF Symbols are plenty. It’s honestly quite likely that this post end up missing some fun technique that you’ve discovered and enjoy using.

The reason for this is that I’ve found that it’s not immediately obvious just how powerful SF Symbol animations can be.

At the very core, it’s really not that complex to animate an SF Symbol. For example, we could quite easily create the animation below with just a few lines of code:

Wiggle animation example

The code for creating an effect like that looks a bit like this:

Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
    .symbolEffect(.wiggle, options: .repeat(.continuous))

What’s fun is that some symbols lend themselves to certain animations better than other. A wiggle is usually a fine way to draw attention to a symbol.

Notice how in the animation above you can distinguish between three layers that exist. The AirPods case, the inner “radio waves”, and the outer “radio waves”. SF Symbols lets us apply animations that change individual layers one by one. For example, to indicate a “searching” or “charging” animation you could want to have both radio waves be empty, then fill the inner ones, then the outer ones, and then have them be empty again.

A bit like this:

Animated waves on Airpods case

We can achieve that by changing the symbol effect that we’ve applied:

Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
    .symbolEffect(.variableColor, options: .repeat(.continuous))

That’s pretty cool, right?

There’s a whole bunch of symbol effects available for you to try so I highly recommend to apply the symbolEffect view modifier to see which effects exist, and to see how they play with specific symbols. As you’ll see, some effects (like variableColor will work well with certain layered SF Symbols but not with others).

The variableColor effect is an effect that has a list of sub effects. In the example above, all layers get filled and then we reset back to a base state. This is equivalent to the following code:

Image(systemName: "airpodspro.chargingcase.wireless.radiowaves.left.and.right.fill")
    .symbolEffect(.variableColor.cumulative, options: .repeat(.continuous))

If you switch cumulative to iterative in the example above, the effect looks like this:

Iterative animation on Airpods case

I highly recommend that you take a look at the available options and play with them to see how much you can really do with SF Symbol animations.

In the code above I used options to set my animation up to be repeating. You can choose to repeat continuously like I did, or you can repeat a set number of times.

It’s also possible to set the repeat behavior to be periodic. That way, your SF Symbol can show its animation once every couple of seconds as a nice way to draw the user’s attention towards the symbol without being obnoxious:

A periodic wiggle effect on a bell

The code for this animation looks like this:

Image(systemName: "bell.circle")
    .symbolEffect(.wiggle, options: .repeat(.periodic(delay: 2)))

It’s pretty cool that we’re able to write animations this powerful with very little work. SF Symbols do a lot of the heavy lifting of building good looking animations for us.

It’s also possible to link a symbol effect to a specific value in your view so that the animation starts as soon as the linked value changes.

Here’s what the code to do that looks like:

Image(systemName: "bell.circle")
    .symbolEffect(.wiggle, options: .repeat(.periodic(2, delay: 2)), value: notificationsEnabled)

Button("Toggle Notifications") {
    notificationsEnabled.toggle()
}

Every time we click the button to change the value of notificationsEnabled we start our symbol effect which wiggles the bell twice before stopping our animation.

We can also link our effect to a boolean value that determines whether or not our effect is active at all:

Image(systemName: "bell.circle")
    .symbolEffect(.wiggle, options: .repeat(.periodic(delay: 2)), isActive: notificationsEnabled)

The code above is slightly different because it uses isActive instead of value to determine whether the animation is active. We’ve also gone back to a constantly repeating animation that will only be active whenever the notificationsEnabled property is true. As soon as it’s set to false, the animation will end.

It’s worth exploring which animations are available, and how you can mix and match different options and configurations in order to come up with some pretty cool animations.

Next, let’s take a look at symbol transitions.

SF Symbol Transitions

Sometimes, you might want to use an SF Symbol to represent a state-dependent piece of UI.

For example, you might present a notification bell to your user if they’ve enabled notifications but you might want to cross out the notification bell if the user turns off notifications.

The code to achieve that could look a bit like this:

Image(systemName: notificationsEnabled ? "bell" : "bell.slash")

Button("Toggle Notifications") {
    withAnimation {
        notificationsEnabled.toggle()
    }
}

When run, the result looks a bit as follows:

Jarring example of swapping symbols

It’s not great and luckily, we can do better. SF Symbols can now nicely animate between different variants of the same symbols in most cases.

For example, SF Symbols can animate our bell example like this if we apply the right configuration:

A nicely animated symbol transition

All that we need to do is provide a contentTransition for our symbol:

Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
    .contentTransition(.symbolEffect(.replace))

Pretty cool, right? The .replace transition will always try to perform the most appropriate transition to move from one symbol to the next. In this case, that’s by seamlessly adding or removing our slash.

If we combine this with a different rendering mode, the effect looks even better:

A symbol transition that animates colors and changes symbols

In the example above I’ve used a hierarchical rendering mode to automatically gain appropriate opacity levels for my symbol’s layers.

Image(systemName: notificationsEnabled ? "bell" : "bell.slash")
    .symbolRenderingMode(.hierarchical)
    .contentTransition(.symbolEffect(.replace))

Again, I encourage you to play around with different settings and options to see what you can come up with.

SF Symbols are a very powerful tool in your iOS development toolbox and I highly recommend that you spend some time exploring different options the next time you’re working on a design for your app’s UI. Adding the right animations at the right times can really make your app stand out in a good way.

Solving “Value of non-Sendable type accessed after being transferred; later accesses could race;”

Once you start migrating to the Swift 6 language mode, you'll most likely turn on strict concurrency first. Once you've done this there will be several warings and errors that you'll encounter and these errors can be confusing at times.

I'll start by saying that having a solid understanding of actors, sendable, and data races is a huge advantage when you want to adopt the Swift 6 language mode. Pretty much all of the warnings you'll get in strict concurrency mode will tell you about potential issues related to running code concurrently. For an in-depth understanding of actors, sendability and data races I highly recommend that you take a look at my Swift Concurrency course which will get you access to a series of videos, exercises, and my Practical Swift Concurrency book with a single purchase.

WIth that out of the way, let's take a look at the following warning that you might encounter in your project:

Value of non-Sendable type 'MyType' accessed after being transferred; later accesses could race;

For example, the following code produces such an error:

var myArray = [Int]()

Task {
  // Value of non-Sendable type '@isolated(any) @async @callee_guaranteed @substituted <τ_0_0> () -> @out τ_0_0 for <()>' accessed after being transferred; later accesses could race;
  myArray.append(1)
}

myArray.append(2)

Xcode offers a little guidance as to what that error is telling us:

Access can happen concurrently

In other words, the compiler is telling us that we're accessing myArray after we've "transferred" that property to our Task. You can see how we're appending to the array both inside of the task as well as outside of it.

Swift is telling us that we're potentially causing data races here because our append on myArray after the task might actually collide with the append inside of the task. When this happens, we have a data race and our code would crash.

The fix here would be to explicitly make a copy for our task when it's created:

Task { [myArray] in
  var myArray = myArray
  myArray.append(1)
}

This gets rid of our data race potential but it's also not really achieving our goal of appending to the array from inside of the task.

The fix here could be one of several approaches:

  1. You can wrapp your array in an actor to ensure proper isolation and synchronization
  2. You can rework your approach entirely
  3. Global actors could be useful here depending on the structure of your code

Ultimately, most strict concurrency related issues don't have a single solution that works. It's always going to require a case-by-case analysis of why a certain error appears, and from there you should figure out a solution.

In this case, we're taking a mutable object that we're mutating from within a task as well as right after where we've defined the task. The compiler is warning us that that will most likely cause a data race and you'll need to determine which solution works for you. My first attempt at fixing this would be to wrap the mutable state in an actor to make sure we achieve proper isolation and prevent future data races.

Setting the Swift Language mode for an SPM Package

When you create a new Swift Package in Xcode 16, the Package.swift contents will look a bit like this:

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

import PackageDescription

let package = Package(
    name: "AppCore",
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "AppCore",
            targets: ["AppCore"]),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
            name: "AppCore"
        )
    ]
)

Notice how the package's Swift tools version is set to 6.0. If you want your project to reference iOS18 for example, you're going to need to have you Swift tools version set to 6.0. A side effect of that is that your package will now build in the Swift 6 language mode. That means that you're going to get Swift's full suite of sendability and concurrency checks in your package, and that the compiler will flag any issues as errors.

You might not be ready to use Swift 6.0 in your new packages yet. In those cases you can either set the Swift tools version back to 5.10 if you're not using any features from the 6.0 toolchain anyway or you can set your package's language mode to Swift 5 while keeping the 6.0 toolchain:

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

import PackageDescription

let package = Package(
    name: "AppCore",
    platforms: [.iOS(.v18)],
    // ... the rest of the package description
    swiftLanguageModes: [.v5]
)

It's also possible to assign the swift language mode for specific targets in your package instead. Here's what that looks like:

targets: [
  // Targets are the basic building blocks of a package, defining a module or a test suite.
  // Targets can depend on other targets in this package and products from dependencies.
  .target(
    name: "AppCore",
    swiftSettings: [.swiftLanguageMode(.v5)]
  )
]

By using the Swift 5 language mode you can continue to write your code as usual until you're ready to start migrating to Swift 6. For example, you might want to start by enabling strict concurrency checks.

Solving “Task-isolated value of type ‘() async -> Void’ passed as a strongly transferred parameter”

Once you start migrating to the Swift 6 language mode, you'll most likely turn on strict concurrency first. Once you've done this there will be several warings and errors that you'll encounter and these errors can be confusing at times.

I'll start by saying that having a solid understanding of actors, sendable, and data races is a huge advantage when you want to adopt the Swift 6 language mode. Pretty much all of the warnings you'll get in strict concurrency mode will tell you about potential issues related to running code concurrently. For an in-depth understanding of actors, sendability and data races I highly recommend that you take a look at my Swift Concurrency course which will get you access to a series of videos, exercises, and my Practical Swift Concurrency book with a single purchase.

WIth that out of the way, let's take a look at the following warning that you might encounter in your project:

Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter

When I first encountered the error above, I was puzzled. The code that made this happen wasn't all that strange and I had no idea what could be wrong here.

Let's look at an example of the code that would make this error show up:

var myArray = [1, 2, 3]

await withTaskGroup(of: Void.self) { group in
  for _ in 0..<10 {
    // Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter; later accesses could race;
    group.addTask { 
      myArray.append(Int.random(in: 0..<10))
    }
  }
}

The problem above can also occur when you create an unstructured task with Task or a detached task with Task.detached. The error and the reason for the error appearing are the same for all cases, but what exactly is wrong in the code above?

Unfortunately, the compiler isn't of much help here so we'll need to figure this one out on our own...

In every case that I've seen for this specific error, the task that we create (whether it's a child task, unstructured task or a detached task) captures a non-sendable object. To learn more about sendable, take a look at my post that explains Sendable and @Sendable closures.

So while the compiler error is extremely hard to read and understand, the reason for it appearing is actually relatively simple. We've got a strong capture to something that's not Sendable inside of a task that might run concurrently with other work. The result is a possible data race.

The fix can sometimes be relatively simple if you're able to make the captured type sendable or an actor. In the case of the code above that would be tricky; myArray is an array of Int which means that we're already as sendable as we could be. But because the array is mutable, there's a chance that we'll race.

There are multiple possible fixes in this case. One of them is to mutate the array outside of the child tasks by having child tasks produce numbers and then iterating over the task group:

var myArray = [1, 2, 3]

await withTaskGroup(of: Int.self) { group in
  for _ in 0..<10 {
      group.addTask {
          // Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter; later accesses could race;
          return (myArray.first ?? 2) * 2
      }
  }

    for await value in group {
        myArray.append(value)
    }
}

Unfortunately, the above still produces an error...

The reason for that is that myArray is still being accessed from within a child task. So that means that while a child task is reading, our async for loop could be writing and then we have a data race.

To fix that we need to make a copy of myArray in the child task's capture list like this:

group.addTask { [myArray] in
  return (myArray.first ?? 2) * 2
}

With that change in place, the code compiles and runs correctly.

Unfortunately, Task-isolated value of type '() async -> Void' passed as a strongly transferred parameter is a very tough to read error with no single fix. What this error tells you though, is that you're accessing or capturing a value that's not sendable or safe to be accessed concurrently. Fixes for this could be:

  1. To make the captured object an actor
  2. To make the captured object sendable
  3. To make a copy of the object
  4. To capture properties on the object outside of your task
  5. To rethink your approach completely (this is rarely needed)

As with many other strict concurrency related issues, solving this error will depend on your ability to analyze the problem, and your understanding of actors and sendable. These are topics that you should try and understand as good as you can before you attempt to migrate to Swift 6.