While moving several projects to Swift 6, I realized I was using migration habits that guaranteed success. These habits are important not only during the actual migration but also beforehand, even if you’re not yet ready to migrate.

That’s right: you can already prepare for the migration you’ll do in the future. It’s all about how you approach new code you write and how to avoid additional technical debt. The more code you write with Swift 6 today, the less you’ll have to migrate by the time you’re going for it.

With this sneak peek into a few of the habits in mind, it’s time to dive into each specific habit.

1. Don’t panic, it’s all about iterations

You’re motivated and positive, you start the migration, and maybe you even think that you’ll finish it before the end of the day. A few code changes in, you realize it’s quite a challenge to migrate all those existing types to Swift Concurrency. New warnings and errors show up and you’re constantly going back and forth between ChatGPT, this course, and Stack Overflow to better understand what you’re doing. Before you know it’s time to panic—but you don’t!

It’s essential to remind yourself about iterations. It’s fine to enable and disable stricter concurrency checking on a single day. I’ve often migrated pieces of code as follows:

  1. Change the build settings to enable strict concurrency checking (more on this in a future lesson)
  2. Fix a few warnings within the time I had for that day
  3. Revert the build setting back to minimal concurrency checking

Doing so allowed me to use 30 minutes per day to migrate the entire package to Swift 6 gradually. By breaking it down into smaller steps, I allowed myself not to panic and make progress slowly. Allow yourself the time it takes for larger projects to migrate, and don’t expect it to be done within a few days.

2. Sendable by default

You or your colleagues are likely writing new code while a migration might be happening. If those colleagues aren’t prepared for the future direction, it might be that you’re fixing things here while introducing new code-to-be-migrated there.

Therefore, it’s essential to instruct your team to prepare new code for a world where you’re using Swift 6. Consider making new types Sendable by default, as it’s way easier to do so when you’re in the context. Revisiting old code during a migration is more challenging, so if there’s any opportunity to prevent doing so, you should take it. Applying Sendable conformance by default is a perfect example of this. Similarly, you should also consider using Swift Concurrency features like async/await and actors for new code you write. This brings us to the next habit:

3. Use Swift 6 & Concurrency for new projects, packages, and code

If you’re creating a new project, package, or Swift file, consider writing it in Swift 6 language mode with Swift Concurrency. This way, you reduce technical debt and prevent the scope of the migration from growing.

You can temporarily enable Swift 6 language mode for new code files added to an existing project or package. This way, you’ll use the context awareness to write modern code more efficiently.

4. Resist the urge to refactor

This is a common challenge during development. You’re enhancing an existing piece of code, and you realize it’s using outdated APIs or architectures. Before you know it, you’re hours deep into a large refactor.

Don’t get me wrong: I think it’s essential to address those refactorings. Yet, I believe it’s better to scope these into new GitHub/Jira tickets and pick them up when the time is right. While migrating to Swift Concurrency, your focus should be on just that. Of course, sometimes it’s a refactor that allows you to migrate properly, but it should be a required change that you really can’t avoid.

5. Focus on as minimal changes as possible

Closely related to the previous one: focusing on minimal changes. I’d even suggest opening a pull request for highly focused changes and getting those merged. The concurrency rabbit hole can suddenly lead you into a situation where multiple changes to files are required to fix a single warning. Large pull requests are more difficult to review and less likely to get merged quickly. If you’ve updated a single class to concurrency, consider opening a PR for it and getting it merged into your feature/swift-6-migration branch. I view these as small checkpoints during the migration and a measure of progress.

6. Don’t just @MainActor all the things

While default actor isolation influences this habit quite a bit, I just want to stress that you shouldn’t just fix warnings or errors by adding @MainActor everywhere. Take time to consider whether the code should actually run on the main actor or whether you should apply other solutions like a custom actor.

Yet, for most app projects, it’s probably worth considering opting into the default actor isolation setting for @MainActor. The majority of app-related code needs to run on the main actor and it can prevent a lot of false-positive warnings or errors.

Summary

These six migration habits prepare you for a successful migration and strategy to prevent extra technical debt. Looking at the migration as a whole can feel intimidating, but breaking it down into smaller steps makes it much more approachable.

With that in mind, it’s time to examine the specific steps required for migrating a project or package to Swift 6.