Understanding and resolving merge conflicts

Git is great, and when it works well it can be a breeze to work with. You push , pull, commit, branch, merge, but then… you get into a merge conflict, In this post, we’ll explore merge conflicts. We’ll look at why they happen, and what we can do to avoid running into merge conflicts in the first place.

Let’s start by understanding why a merge conflict happens.

Understanding why a merge conflict happens

Git is usually pretty good at merging together branches or commits. So why does it get confused sometimes? And what does it mean when a merge conflict occurs?

Let me start by saying that a merge conflict is not your fault. There’s a good chance that you couldn’t have avoided it, and it’s most certainly not something you should feel bad about.

Merge conflicts happen all the time and they are always fixable.

The reason merge conflicts happen is that git sometimes gets conflicting information about changes. For example, maybe your coworker split a huge Swift file into two or more files. You’ve made changes to parts of the code that was now moved into an extension.

The following two code snippets illustrate the before situation, and two “after” situations.

// Before
struct MainView: View {
  var body: some View {
    VStack {
      Text("This is an example")

      Button("Counter ++") {
        // ...
      }
    }
    .padding(16)
  }
}

// After on branch main
struct MainView: View {
  var body: some View {
    VStack {
      Text("This is another example")
      Text("It has multiple lines!")

      Button("Counter ++") {
        // ...
      }
    }
    .padding(16)
  }
}

// After on feature branch
struct MainView: View {
  var body: some View {
    VStack {
      MyTextView()

      CounterButton()
    }
    .padding(16)
  }
}

When git tries to merge this, it gets confused.

Programmer A has deleted some lines, replacing them with new views while programmer B has made changes to those lines. Git needs some assistance to tell it what the appropriate way to merge this is. A merge conflict like this is nobody’s fault because it’s perfectly reasonable for one developer to be refactoring code and for another developer to be working on a part of that code.

Usually you’ll try to avoid two developers working on the same files in a short timespan, but at the same time git makes it so that we can work on the same file on multiple branches so it’s not common for developers to synchronize who works on which files and when. Doing so would be a huge waste of time, so we instead we rely on git to get our merges right in most cases.

Whenever git sees two conflicting changes on the same part of a file, it asks a human for help. So let’s move on to seeing different approaches to resolving merge conflicts.

Resolving merge conflicts

There’s no silver bullet for resolving your merge conflicts. Typically you will choose one of three options when you’re resolving a conflict:

  • Resolve using the incoming change (theirs)
  • Resolve using the current change (mine)
  • Resolve manually

In my experience you’ll usually want to use a manual resolution when fixing merge conflicts. Before I explain how that works, let’s take a Quick Look at how resolving using “mine” and “theirs” works.

A merge conflicts always happens when you try to apply changes from one commit onto another commit. Or, when you try to merge one branch into another branch.

Sometimes git can merge parts of a file while other parts of the file cause conflicts. For example, if my commit changes line 2 of a specific file, and the other commit removes that line. My commit also adds a few lines of code at the end of the file, and the other commit doesn’t.

Git would be smart enough to append the new lines to the file, but it can’t figure out what to do with line 2 of the files since both commits have made changes in a way that git can’t merge.

In this case, we can make a choice to either resolve the conflict for line 2 using my commit (make a change to line 2) or using the other commit (delete the line altogether).

Deciding what needs to be done can sometimes require some work and collaboration.

If your coworker deleted a specific line, it’s worth asking why they did that. Maybe line 2 declares a variable that’s no longer needed or used so your coworker figured they’d delete it. Maybe you didn’t check whether the variable was still needed but you applied a formatting change to get rid of a SwiftLint warning.

In a situation like this, it’s safe to resolve your conflict using “their” change. The line can be removed so you can tell git that the incoming change is what you want.

In other situations things might not be as straightforward and you’ll need to do a manual merge.

For example, let’s say that you split a large file into multiple files while your coworker made some changes to one of the functions that you’ve now moved into a different file.

If this is the case, you can’t tell git to use one of the commits. Instead, you’ll need to manually copy your coworker’s changes into your new file so that everything still works as intended. A manual conflict resolution can sometimes be relatively simple and quick to apply. However, they can also be rather complex.

If you’re not 100% sure about the best way to resolve a conflict I highly recommend that you ask for a second pair of eyes to help you out. Preferably the eyes of the author of the conflicting commit because that will help make sure you don’t accidentally discard anything your coworker did.

During a merge conflict your project won’t build which makes testing your conflict resolution almost impossible. Once you’ve resolved everything, make sure you compile and test your app before you commit the conflict resolution. If things don’t work and you have no idea what you’ve missed it can be useful to just rewind and try again by aborting your merge. You an do this using the following command:

git merge --abort

This will reset you back to where you were before you attempted to merge.

If you approach your merge conflicts with caution and you pay close attention to what you’re doing you’ll find that most merge conflicts can be resolved without too much trouble.

Merge conflicts can be especially tedious when you try to merge branches by rebasing. In the next section we’ll take a look at why that’s the case.

Resolving conflicts while rebasing

When you’re rebasing your branch on a new commit (or branch), you’re replaying every commit on your branch using a new commit as the starting point.

This can sometimes lead to interesting problems during a rebase where it feels like you’re resolving the same merge conflicts over and over again.

In reality, your conflicts can keep popping up because each commit will have its own incompatibilities with your new base commit.

For example, consider the following diagram as our git history:

Git history without rebase

You can see that our main branch has received some commits since we’ve created our feature branch. Since the main branch has changed, we want to rebase our feature branch on main so that we know that our feature branch is fully up to date.

Instead of using a regular merge (which would create a merge commit on feature) we choose to rebase feature on main to make our git history look as follows:

Git history with rebase

We run git rebase main from the command line and git tells us that there’s a conflict in a specific file.

Imagine that this file looked like this when we first created feature:

struct MainView: View {
  var body: some View {
    VStack {
      Text("This is an example")

      Button("Counter ++") {
        // ...
      }
    }
    .padding(16)
  }
}

Then, main received some new code to make the file look like this:

struct MainView: View {
  var body: some View {
    VStack {
      Text("This is another example")
      Text("It has multiple lines!")

      Button("Counter ++") {
        // ...
      }
    }
    .padding(16)
  }
}

But our feature branch has a version of this file that looks as follows:

struct MainView: View {
  var body: some View {
    VStack {
      MyTextView()

      CounterButton()
    }
    .padding(16)
  }
}

We didn’t get to this version of the file on feature in one step. We actually have several commits that made changes to this file so when we replay our commits from feature on the current version of main, each individual commit might have one or more conflicts with the “previous” commit.

Let’s take this step by step. The first commit that has a conflict looks like this on feature:

struct MainView: View {
  var body: some View {
    VStack {
      MyTextView()

      Button("Counter ++") {
        // ...
      }
    }
    .padding(16)
  }
}

struct MyTextView: View {
  var body: some View {
    Text("This is an example")
  }
}

I’m sure you can imagine why this is a conflict. The feature branch has moved Text to a new view while main has changed the text that’s passed to the Text view.

We can resolve this conflict by grabbing the updated text from main, adding it to the new MyTextView and proceed with our rebase.

Now, the next commit that changed this file will also have a conflict to resolve. This time, we need to tell git how to get from our previously fixed commit to this new one. The reason this is confusing git is that the commit we’re attempting to apply can no longer be applied in the same way that it was before; the base for every commit in feature has changed so each commit needs to be rewritten.

We need to resolve this conflict in our code editor, and then we can continue the rebase by running git add . followed by git rebase --continue. This will open your terminal’s text editor (often vim) allowing you to change your commit message if needed. When you’re happy with the commit message you can finish your commit by hitting esc and then writing :wq to write your changes to the commit message.

After that the rebase will continue and the conflict resolution process needs to be repeated for every commit with a conflict to make sure that each commit builds correctly on top of the commit that came before it.

When you’re dealing with a handful of commits this is fine. However if you’re resolving conflicts for a dozen of commits this process can be frustrating. If that’s the case, you can either choose to do a merge instead (and resolve all conflict at once) or to squash (parts of) your feature branch. Squashing commits using rebase is a topic that’s pretty advanced and could be explained in a blog post of its own. So for now, we’ll skip that.

When you’ve decided how you want to proceed, you can abandon your rebase by running git rebase --abort in your terminal to go back to the state your branch was in before you attempted to rebase. After that, you can decide to either do a git merge instead, or you can proceed with squashing commits to make your life a little bit easier.

Git rebase and your remote server

If you’ve resolved all your conflicts using rebasing, you have slightly altered all of the commits that were on your feature branch. If you’ve pushed this branch to a remote git server, git will tell you that your local repository has n commits that are not yet on the remote, and that the remote has a (usually) equal number of commits that you do not yet have.

If the remote has more commits than you do, that’s a problem. You should have pulled first before you did your rebase.

The reason you need to pull first in that scenario is because you need to be able to rebase all commits on the branch before you push the rewritten commits to git since in order to do a push like that, we need to tell git that the commits we’re pushing are correct, and the commits it had remotely should be ignored.

We do this by passing the --force flag to our git push command. So for example git push --force feature.

Note that you should always be super cautious when force pushing. You should only ever do this after a rebase, and if you’re absolutely sure that you’re not accidentally discarding commits from the remote by doing this.

Furthermore, if you’re working on a branch with multiple people a force push can be rather frustrating and problematic to the local branches of your coworkers.

As a general rule, I try to only rebase and force push on branches that I’m working on by myself. As soon as a branch is being worked on my others I switch to using git merge, or I only rebase after checking in with my coworkers to make sure that a force push will not cause problems for them.

When in doubt, always merge. It’s not the cleanest solution due to the merge commits it creates, but at least you know it won’t cause issues for your teammates.

In Summary

Merging branching is a regular part of your day to day work in git. Whether it’s because you’re tying to absorb changes someone made into a branch of your own or it’s because you want to get your own changes in to your main branch, understanding different merging techniques is key.

Regardless of how you intend to merge branches, there’s a possibility to run into a merge conflict. In this post, you’ve learned why merge conflicts can happen, and how you can resolve them.

You’ve also learn why rebases can run into several merge conflicts and why you should always resolve these conflicts one by one. In short, it’s because git replays each commit in your branch on top of the “current” commit for the branch you’re rebasing on.

The key to resolving conflicts is always to keep your cool, take it easy, and work through the conflicts one by one. And when in doubt it’s always a good idea to ask a coworker to be your second pair of eyes.

You also learned about force pushing after rebasing and how that can be problematic if you’re working on your branch with multiple people.

Do you have any techniques you love to employ while resolving conflicts? Let me know on X or Threads!

Expand your learning with my books

Practical Swift Concurrency (the video course) header image

Learn everything you need to know about Swift Concurrency and how you can use it in your projects with Practical Swift Concurrency the video course. It contains:

  • About ten hours worth of videos and exercises
  • Sample projects that use the code shown in the videos.
  • FREE access to the Practical Swift Concurrency book
  • Free updates for future iOS and Swift versions.

The course is available on Teachable for just $89

Enroll now

Git basics for iOS developers

I’ll just say this right off the bat. There’s no such thing as git “for iOS Developers”. However, as iOS Developers we do make use of git. And that means that it makes a lot of sense to understand git, what it is, what it’s not, and most importantly how we can use it effectively and efficiently in our work.

In this post, I’d like to outline some of the key concepts, commands, and principles that you’ll need to know as an iOS Developer that works with git. By the end of this post you will have a pretty good understanding of git’s basics, and you’ll be ready to start digging into more advanced concepts.

Understanding what git is

Git is a so called version control system that was invented in the early 2000s. It was invented by Linus Torvalds who’s also the creator of the Linux operating system. It’s primary goal is to be a faster alternative to older version control systems like SVN and CVS. These older systems all relied on a single source of truth and made features like branching slow and hard to manage. And because everybody relied on a single source of truth, this meant that there was also a single point of failure. In practice this meant that if your server broke, the entire project was broken.

Git is a distributed system. This means that everybody that clones a project clones the entire git repository. Everybody has all code, all branches, all tags, etc. on their machine when they clone a repository.

The upside of this is that if anything goes wrong with any of the copies of the repository it’s always possible to replace that copy because there’s never a single point of failure.

However, in your day to day use it won’t matter much that git is faster and more reliable than what came before it. In your day to day work you’ll most likely be using git as a means to collaborate with your peers, and to make sure you always have a backup with proper history tracking for your project.

A common misconception amongst newer developers is that git is only relevant when a project needs to be shared amongst multiple developers. While it’s very useful for that, I can only recommend that you always use git to manage your personal projects too. Doing this will allow you to experiment with new features in separate branches, rewind your project to a previous point in time, and to tag releases so you always know which version of your code ended up shipping. If you’re not sure what a branch is, don’t worry. I’ll get to explaining that soon.

Using git is always recommended regardless of project size, team size, or project complexity.

In this post, I won’t explain how git works on this inside. My aim is to provide a much higher level overview for now, and to dig into internals in several follow up posts. Git is complicated enough as-is, so there’s really no need to make things more complicated than they need to be in an introductory post.

Now that you know that git is a version control system that allows you to keep track of your code, share it, create branches, tags, and more, let’s take a look at some of they terminology that’s used when working with git.

Key terminology

You have a vague sense about what git is so now I’d like to walk you through a bit of key terminology. This will help you understand explanations for concepts further in this series, and provide you with a first look at the most important git concepts.

Later in this post we’ll also look at some of git’s most important commands which will start putting things in context and give you some pointers to start using git if you aren’t already.

Repository

When you work with git, a project is typically called a repository. Your repository is usually your project folder that contains a .git folder which is created when you initialize your git repository. This folder contains all information about your project, your commits, history, branches, tags, and more. In the next section of this post we’ll go over how to create a new git repository.

Remote (Origin)

A git repository usually doesn’t exist only on your computer (even though it can!). Most repositories are hosted somewhere on a server so that you can easily access it from any computer, and share the repository with your team mates. While it is decentralized and everybody that clones your repository has a full copy of the repository, you’ll often have a single origin that’s used as your source of truth that everybody in your team pushes code to and pulls updates from.

Most projects will use an existing platform like GitHub, GitLab, or Azure as their remote to push and pull code. A project can use multiple remotes if needed but usually your primary / main remote is called “origin”.

Branches

In git, you make use of branches to structure your work. Every project that you place under version control with git will have at least one branch, this branch is typically called main. Every time you make a new commit in your repository you’re essentially associating that commit with a branch. This allows you to create a new branch that’s based off of a given version of your code, work on it, make changes, and eventually switch back to another branch that doesn’t contain the same changes you just made.

In a way, you can think of a branch in git as a chain of commits.

This is incredibly useful when you’re working on new features for your app while you’re also maintaining a shipping version of your app. You can make as many branches as you’d like in git, and you can merge changes back into your main branch when you’re happy with the feature you’ve just built.

Commits

Commits are what I would consider git’s core feature. Every time you make a new commit, you create a snapshot of the work you did in your project so far. After you’ve made a commit you can choose to continue working on your project, make progress towards new features, implement bug fixes, and more. As you make progress you’ll make more and more commits to snapshot your progress.

So why would you make commits?

Well, there are a few key reasons. One of them is that a commit allows you to see the changes that you’ve made from one step to the next. For example, when you’ve completed a big refactor you might not completely remember which files you’ve worked on and what you’ve changed. If you’ve made one or more commits during the refactoring process you can retrace every step that you took during your refactor.

Another reason to make a commit is so you can branch off of that commit to work on different features in isolation. You’ll most commonly do this in teams but I’ve done this in single-person projects too.

Git is all about commits so if there’s one git concept that you’ll want to focus on first if you’re new to git than it’s probably going to be commits.

Merging and rebasing

For now, I’m going talk about merging and rebasing under a single header. They’re both different concepts with very different implications and workflows but they usually serve a similar purpose. Since we’re focussing on introducing topics, I think it’s fair to talk about merge and rebase under a single header.

When we have a series of commits on one branch, and we have another branch with some more commits, we’ll usually want somehow bring the newer commits into our source branch. For example, if I have a main branch that I’ve been committing to, I might have created a feature-specific branch to work from. For example, I might have branched off of the main branch to start working on a design overhaul for my app.

Once my design overhaul is complete I’ll want to update my main branch with the new design so that I can ship this update to my users. I can do this by rebasing or merging. The end result of either operation is that the commits that I made (or the final state of my feature branch) end up being applied to my main branch. Merge and rebase each do this in a slightly different way and I’ll cover each option in more depth in a follow up post.

Git’s most important commands

Alright, I know this is a long post (especially for a blog) but before we can wrap up this introduction to git, I think it’s time we go over a few of git’s key commands. These commands correspond to the key terminology that we just covered, so hopefully the commands along with their explanations help solidify what you’ve just learned.

Because the command line is a universally available interface for git I’ll go ahead and focus my examples only on running commands in the command line. If you prefer working with a more graphical interface feel free to use one that you like. Fork, Tower, and Xcode’s built-in git GUI all work perfectly fine and are all built on top of the commands outlined below.

Initializing a new repository

When you start a new project, you’ll want to create a git repository for your project sooner rather than later. Creating a repository can be done with a single command that creates a .git folder in your project root. As you’ve learned in the previous section, the .git folder is the heart and soul of your repository. It’s what transforms a plain folder on your file system into a repository.

To turn your project folder into a repository, navigate to your project folder (the root of your project, usually the same folder as where your .xcodeproj is located) and type the following command:

git init

This command will run quickly and it will initialize a new repository in the folder you ran the command from.

When creating a new project in Xcode you can check the “create git repository on my mac” checkbox to start your project off as a git repository. This will allow you to skip the git init step.

Creating a repository for your project does not put any files in your project under version control just yet. We can verify this by running the git status command. Doing this for a folder that I just created a new Xcode project in yields the following output:

❯ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    GitSampleProject.xcodeproj/
    GitSampleProject/

nothing added to commit but untracked files present (use "git add" to track)

As you can see, there’s a list of files under the untracked files header.

This tells us that git can see that we have files in our project folder, but git isn’t actively tracking (or ignoring) these files. In this case, git is seeing our xcodeproj folder and the GitSampleProject folder that holds our Swift files. Git won’t pro-actively dig into these folders to list all files that it’s not tracking. Instead, it lists the folder which indicates that nothing in that folder is being tracked.

Let’s take a look at adding files to a git next.

Adding files to git

As you’ve seen, git doesn’t automatically track history for every file in our project folder. To make git track files we need to add them to git using the add command. When you add a file to git, git will allow you to commit versions of that file so that you can track history or go back to a specific version of that file if needed.

The quickest way to add files to git is to use the add command as follows:

git add .

While this approach is quick, it’s not great. In a standard Xcode project there are always some files that you don’t want to add to git. We can be more specific about what we need to be added to git by specifying the files and folders that we want to add:

# adding files
git add Sources/Sample.swift

# adding folders
git add Sources/

For a standard Xcode project we typically want to everything in our project folder with a couple of exceptions. Instead of manually typing and filtering the files and folders that we want to add to git every time we want to make a new commit, we can exclude files and folders from git using a a file called .gitignore. You can add multiple ignore files to your repository but most commonly you’ll have one at the root of your project. You can create your .gitignore file on the command line by typing the following command:

❯ touch .gitignore
❯ open .gitignore

This will open your file in the TextEdit app. A typical iOS project will at least have the following files and folders added to this file:

.DS_Store
xcuserdata/

You can use pattern matching to exclude or include files and folders using wildcards if you’d like. For now, we’ll just use a pretty simple ignore file as an example.

From now on, whenever git sees that you have files and folders in your project that match the patterns from your ignore file it won’t tell you that it’s not tracking those files because it will simply ignore them. This is incredibly useful for files that contain user specific data, or for content that’s generated at build time. For example, if you’re using a tool like Sourcery to generate code in your project every time it builds, you’ll usually exclude these files from git because they’re automatically recreated anyway.

Once you add files to git using git add, they are added to the staging area. This means that if you were to make a commit now, those files are included in your commit. Git doesn’t record a permanent snapshot of your files until you make a commit. And when you make a commit, only changes that are added to the staging area are included in the commit.

To make your initial commit you’ll usually set up your .gitignore file and then run git add . to add everything in your project to the staging area in one go.

To see the current status of files that have changes, files that aren’t being tracked, and files that are in the staging area and ready to be committed we can use git status again. If we run the command for our iOS project after adding some files and creating the .gitignore file we get the following output:

❯ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
    new file:   .gitignore
    new file:   GitSampleProject.xcodeproj/project.pbxproj
    new file:   GitSampleProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata
    new file:   GitSampleProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
    new file:   GitSampleProject/Assets.xcassets/AccentColor.colorset/Contents.json
    new file:   GitSampleProject/Assets.xcassets/AppIcon.appiconset/Contents.json
    new file:   GitSampleProject/Assets.xcassets/Contents.json
    new file:   GitSampleProject/ContentView.swift
    new file:   GitSampleProject/GitSampleProjectApp.swift
    new file:   GitSampleProject/Preview Content/Preview Assets.xcassets/Contents.json

This is exactly what we want. No more untracked files, git has found our ignore file, and we’re ready to tell git to record the first snapshot of our repository by making a commit.

Making your first commit

We can make a new commit by writing git commit -m "<A short description of changes>" you’d replace the text between the < and > with a short message that describes what’s in the snapshot. In the case of your initial commit you’ll often write initial commit. Future commits usually contain a very short sentence that describes what you’ve changed.

Writing a descriptive yet short commit message is an extremely good practice because once your project has been under development for a while you’ll be thanking yourself when your commit messages are more descriptive than just the words “did some work” or something similar.

Back to making our first commit. To make a new commit in my sample repository, I run the following command:

git commit -m "initial commit"

When I run this command, the following output is produced:

[main (root-commit) 5aa14e7] initial commit
 10 files changed, 443 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 GitSampleProject.xcodeproj/project.pbxproj
 create mode 100644 GitSampleProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata
 create mode 100644 GitSampleProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
 create mode 100644 GitSampleProject/Assets.xcassets/AccentColor.colorset/Contents.json
 create mode 100644 GitSampleProject/Assets.xcassets/AppIcon.appiconset/Contents.json
 create mode 100644 GitSampleProject/Assets.xcassets/Contents.json
 create mode 100644 GitSampleProject/ContentView.swift
 create mode 100644 GitSampleProject/GitSampleProjectApp.swift
 create mode 100644 GitSampleProject/Preview Content/Preview Assets.xcassets/Contents.json

This tells me that a new commit was created with a hash of 5aa14e7. This hash is the unique identifier for this commit. Git also tells me the number of files and changes in the commit, and then the files are listed. In this case, all my files are labeled with create mode. When I make changes to a file and I commit those changes that label will change accordingly.

Most git repositories are connected to a remote host like GitHub. In this post I won’t show you how to add a remote to a git repository. This post is already rather long as it is, so we’ll cover git and remote hosts in a separate post.

In Summary

In this post, you’ve learned a lot of basics around git. You now know that git is a so-called version control system. This means that git tracks history of our files, and allows us to work on multiple features and bug fixes at once using branches. You know that a git repository contains a .git folder that holds all information that git needs to operate.

I’ve explained git’s most important terms like commits, branches, merging, and more. We’ve looked at the key concepts here which means that for some of the terminology you’ve seen we could go way deeper and discover lots of interesting details. These are all topics for separate posts.

After introducing the most important terminology in git, we’ve looked at git’s most important commands. You’ve seen how to create a new git repository, how to add and ignore files, and how to make a commit.

Our next post in this series will focus on getting your repository connected to a remote like GitHub.

Making your SwiftData models Codable

In a previous post, I explained how you can make your NSManagedObject subclasses codable. This was a somewhat tedious process that involves a bunch of manual work. Specifically because the most convenient way I've found wasn't all that convenient. It's easy to forget to set your managed object context on your decoder's user info dictionary which would result in failed saves in Core Data.

With SwiftData it's so much easier to define model objects so it makes sense to take a look at making SwiftData models Codable to see if it's better than Core Data. Ultimately, SwiftData is a wrapper around Core Data which means that the @Model macro will at some point generate managed objects, an object model, and more. In this post, we'll see if the @Model macro will also make it easier to use Codable with model objects.

If you prefer learning by video, check out the video for this post on YouTube:

Tip: if you're not too familiar with Codable or custom encoding and decoding of models, check out my post series on the Codable protocol right here.

Defining a simple model

In this post I would like to start us off with a simple model that's small enough to not get confusing while still being representative for a model that you might define in the real world. In my Practical Core Data book I make a lot of use of a Movie object that I use to represent a model that I would load from The Movie Database. For convenience, let's just go ahead and use the a simplified version of that:

@Model class Movie {
  let originalTitle: String
  let releaseDate: Date

  init(originalTitle: String, releaseDate: Date) {
    self.originalTitle = originalTitle
    self.releaseDate = releaseDate
  }
}

The model above is simple enough, it has only two properties and to illustrate the basics of using Codable with SwiftData we really don't need anything more than that. So let's move on and add Codable to our model next.

Marking a SwiftData model as Codable

The easiest way to make any Swift class or struct Codable is to make sure all of the object's properties are Codable and having the compiler generate any and all boilerplate for us. Since both String and Date are Codable and those are the two properties on our model, let's see what happens when we make our SwiftData model Codable:

// Type 'Movie' does not conform to protocol 'Decodable'
// Type 'Movie' does not conform to protocol 'Encodable'
@Model class Movie: Codable {
  let originalTitle: String
  let releaseDate: Date

  init(originalTitle: String, releaseDate: Date) {
    self.originalTitle = originalTitle
    self.releaseDate = releaseDate
  }
}

The compiler is telling us that our model isn't Codable. However, if we remove the @Model macro from our code we are certain that our model is Codable because our code does compiler without the @Model macro.

So what's happening here?

A macro in Swift expands and enriches our code by generating boilerplate or other code for us. We can right click on the @Model macro and choose expand macro to see what the @Model macro expands our code into. You don't have to fully understand or grasp the entire body of code below. The point of showing it is to show you that the @Model macro adds a lot of code, including properties that don't conform to Codable.

@Model class Movie: Codable {
  @_PersistedProperty
  let originalTitle: String
  @_PersistedProperty
  let releaseDate: Date

  init(originalTitle: String, releaseDate: Date) {
    self.originalTitle = originalTitle
    self.releaseDate = releaseDate
  }

  @Transient
  private var _$backingData: any SwiftData.BackingData<Movie> = Movie.createBackingData()

  public var persistentBackingData: any SwiftData.BackingData<Movie> {
    get {
      _$backingData
    }
    set {
      _$backingData = newValue
    }
  }

  static func schemaMetadata() -> [(String, AnyKeyPath, Any?, Any?)] {
    return [
      ("originalTitle", \Movie.originalTitle, nil, nil),
      ("releaseDate", \Movie.releaseDate, nil, nil)
    ]
  }

  required init(backingData: any SwiftData.BackingData<Movie>) {
    self.persistentBackingData = backingData
  }

  @Transient
  private let _$observationRegistrar = Observation.ObservationRegistrar()
}

extension Movie: SwiftData.PersistentModel {
}

extension Movie: Observation.Observable {
}

If we apply Codable to our SwiftData model, the protocol isn't applied to the small model we've defined. Instead, it's applied to the fully expanded macro. This means that we have several properties that don't conform to Codable which makes it impossible for the compiler to (at the time of writing this) correctly infer what it is that we want to do.

We can fix this by writing our own encoding and decoding logic for our model.

Writing your encoding and decoding logic

For a complete overview of writing custom encoding and decoding logic for your models, check out this post.

Let's start off by defining the CodingKeys enum that we'll use for both our encoding and decoding logic:

@Model class Movie: Codable {
  enum CodingKeys: CodingKey {
    case originalTitle, releaseDate
  }

  // ...
}

These coding keys directly follow the property names for our model. We have to define them because we're defining custom encoding and decoding logic.

The decoding init can look as follows:

required init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  self.originalTitle = try container.decode(String.self, forKey: .originalTitle)
  self.releaseDate = try container.decode(Date.self, forKey: .releaseDate)
}

This initializer is pretty straightforward. We grab a container from the decoder, and then we ask the container to decode the properties we're interested in using our coding keys.

The encoding logic would look as follows:

func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  try container.encode(originalTitle, forKey: .originalTitle)
  try container.encode(releaseDate, forKey: .releaseDate)
}

With this initializer and encode(to:) function in place, our model is now fully Codable. Note that if you're only grabbing data from the network and which to decode that data into SwiftData models you can conform to Decodable instead of Codable in order to skip having to write the encode(to:) method.

Let's see how we can actually use our model next.

Decoding JSON into a SwiftData model

For the most part, decoding your JSON data into a SwiftData model will be relatively striaghtforward. The key thing to keep in mind is that you need to register all of your decoded objects in your model context after decoding them. Here's an example of how to do this:

let url = URL(string: "https://path.to.data")!
let (data, _) = try await URLSession.shared.data(from: url)

// this is the actual decoding
let movies = try! JSONDecoder().decode([Movie].self, from: data)

// don't forget to register the decoded objects
for movie in movies {
  context.insert(movie)
}

Making our model Codable and working with it was straightforward enough. To wrap things up, I'd like to explore how this approach works with relationships.

Adding relationships to our model

First, let's update our model object to have a relationship:

@Model class Movie: Codable {
  enum CodingKeys: CodingKey {
    case originalTitle, releaseDate, cast
  }

  let originalTitle: String
  let releaseDate: Date

  @Relationship([], deleteRule: .cascade)
  var cast: [Actor]

  init(originalTitle: String, releaseDate: Date, cast: [Actor]) {
    self.originalTitle = originalTitle
    self.releaseDate = releaseDate
    self.cast = cast
  }

  required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.originalTitle = try container.decode(String.self, forKey: .originalTitle)
    self.releaseDate = try container.decode(Date.self, forKey: .releaseDate)
    self.cast = try container.decode([Actor].self, forKey: .cast)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(originalTitle, forKey: .originalTitle)
    try container.encode(releaseDate, forKey: .releaseDate)
    try container.encode(cast, forKey: .cast)
  }
}

The Movie object here has gained a new property cast which is annotated with SwiftData's @Relationship macro. Note that the decode and encode logic doesn't get fancier than it needs to be. We just decode and encode our cast property like we would any other property.

Let's look at the definition of our Actor model next:

@Model class Actor: Codable {
  enum CodingKeys: CodingKey {
    case name
  }

  let name: String

  @Relationship([], deleteRule: .nullify)
  let movies: [Movie]

  init(name: String, movies: [Movie]) {
    self.name = name
    self.movies = movies
  }

  required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decode(String.self, forKey: .name)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
  }
}

Our Actor defines a relationship back to our Movie model but we don't account for this in our encode and decode logic. The data we're loading from an external source would infinitely recurse from actor to movie and back if actors would also hold lists of their movies in the data we're decoding. Because the source data doesn't contain the inverse that we've defined on our model, we don't decode it. SwiftData will make sure that our movies property is populated because we've defined this property using @Relationship.

When decoding our full API response, we don't need to update the usage code from before. It looks like we don't have to explicitly insert our Actor instances into our model context due to SwiftData's handling of relationships which is quite nice.

With the code as it is in this post, we can encode and decode our SwiftData model objects. No magic needed!

In Summary

All in all I have to say that I'm a little sad that we didn't get Codable support for SwiftData objects for free. It's nice that it's easier to make SwiftData models Codable than it is to make an NSManagedObject conform to Codable but it's not too far off. We still have to make sure that we associate our decoded model with a context. It's just a little bit easier to do this in SwiftData than it is in Core Data.

If you have a different approach to make your SwiftData models Codable, or if you have questions about this post feel free to reach out!

SwiftUI’s Bindable property wrapper explained

WIth the introduction of Xcode 15 beta and its corresponding beta OSses (I would say iOS 17 beta, but of course we also get macOS, iPadOS, and other betas...) Apple has introduced new state mangement tools for SwiftUI. One of these new tools is the @Bindable property wrapper. In an earlier post I explained that @Binding and @Bindable do not solve the same problem, and that they will co-exist in your applications. In this post, I would like to clarify the purpose and the use cases for @Bindable a little bit better so that you can make better decisions when picking your SwiftUI state property wrappers.

If you prefer learning by video, the key lessons from this blog post are also covered in this video:

The key purpose of the @Bindable is to allow developers to create bindings to properties that are part of a model that confoms to the Observable protocol. Typically you will create these models by annotating them with the @Observable macro:

@Observable
class SearchModel {
  var query: String = ""
  var results: [SearchResult] = []

  // ...
}

When you pass this model to a SwiftUI view, you might end up with something like this:

struct SearchView {
  let searchModel: SearchModel

  var body: some View {
    TextField("Search query", text: // ...??)
  }
}

Notice how the searchModel is defined as a plain let. We don't need to use @ObservedObject when a SwiftUI view receives an Observable model from one of its parent views. We also shouldn't be using @State because @State should only be used for model data that is owned by the view. Since we're passed our SearchModel by a parent view, that means we don't own the data source and we shouldn't use @State. Even without adding a property wrapper, the Observable model is able to tell the SwiftUI view when one of its properties has changed. How this works is a topic for a different post; your key takeaway for now is that you don't need to annotate your Observable with any property wrappers to have your view observe it.

Back to SearchView. In the SearchView body we create a TextField and this TextField needs to have a binding to a string value. If we'd be working with an @ObservedObject or if we owned the SearchModel and defined its proeprty as @State we would write $searchModel.query to obtain a binding.

When we attempt to do this for our current searchModel property now, we'd see the following error:

var body: some View {
  // Cannot find '$searchModel' in scope
  TextField("Search query", text: $searchModel.query)
}

Because we don't have a property wrapper to create a projected value for our search model, we can't use the $ prefix to create a binding.

To learn more about property wrappers and projected values, read this post.

In order to fix this, we need to annotate our searchModel with @Bindable:

struct SearchView {
  @Bindable var searchModel: SearchModel

  var body: some View {
    TextField("Search query", text: $searchModel.query)
  }
}

By applying the @Bindable property wrapper to the searchModel property, we gain access to the $searchModel property because the Bindable property wrapper can now provide a projected value in the form of a Binding.

Note that you only need the @Bindable property wrapper if:

  • You didn't create the model with @State (because you can create bindings to @State properties already)
  • You need to pass a binding to a property on your Observable model

Essentially, you will only need to use @Bindable if in your view you write $myModel.property and the compiler tells you it can't find $myModel. That's a good indicator that you're trying to create a binding to something that can't provide a binding out of the box, and thay you'll want to use @Bindable to be able to create bindings to your model.

Hopefully this post helps clear the purpose and usage of @Bindable up a little bit!

What’s the difference between @Binding and @Bindable

With iOS 17, macOS Sonoma and the other OSses from this year's generation, Apple has made a couple of changes to how we work with data in SwiftUI. Mainly, Apple has introduced a Combine-free version of @ObservableObject and @StateObject which takes the shape of the @Observable macro which is part of a new package called Observation.

One interesting addition is the @Bindable property wrapper. This property wrapper co-exists with @Binding in SwiftUI, and they cooperate to allow developers to create bindings to properties of observable classes. So what's the role of each of these property wrappers? What makes them different from each other?

If you prefer learning by video, the key lessons from this blog post are also covered in this video:

To start, let's look at the @Binding property wrapper.

When we need a view to mutate data that is owned by another view, we create a binding. For example, our binding could look like this:

struct MyButton: View {
    @Binding var count: Int

    var body: some View {
        Button(action: {
            count += 1
        }, label: {
            Text("Increment")
        })
    }
}

The example isn' t particularly interesting or clever, but it illustrates how we can write a view that reads and mutates a counter that is owned external to this view.

Data ownership is a big topic in SwiftUI and its property wrappers can really help us understand who owns what. In the case of @Binding all we know is that some other view will provide us with the ability to read a count, and a means to mutate this counter.

Whenever a user taps on my MyButton, the counter increments and the view updates. This includes the view that originally owned and used that counter.

Bindings are used in out of the box components in SwiftUI quite often. For example, TextField takes a binding to a String property that your view owns. This allows the text field to read a value that your view owns, and the text field can also update the text value in response to the user's input.

So how does @Bindable fit in?

If you're famliilar with SwiftUI on iOS 16 and earlier you will know that you can create bindings to @State, @StateObject, @ObservedObject, and a couple more, similar, objects. On iOS 17 we have access to the @Observable macro which doesn't enable us to create bindings in the same way that the ObservableObject does. Instead, if our @Observable object is a class, we can ask our views to make that object bindable.

This means that we can mark a property that holds an Observable class instance with the @Bindable property wrapper, allowing us to create bindings to properties of our class instance. Without @Bindable, we can't do that:

@Observable
class MyCounter {
    var count = 0
}

struct ContentView: View {
    var counter: MyCounter = MyCounter()

    init() {
        print("initt")
    }

    var body: some View {
        VStack {
            Text("The counter is \(counter.count)")
            // Cannot find '$counter' in scope
            MyButton(count: $counter.count)
        }
        .padding()
    }
}

When we make the var counter property @Bindable, we can create a binding to the counter's count property:

@Observable
class MyCounter {
    var count = 0
}

struct ContentView: View {
    @Bindable var counter: MyCounter

    init() {
        print("initt")
    }

    var body: some View {
        VStack {
            Text("The counter is \(counter.count)")
            // This now compiles
            MyButton(count: $counter.count)
        }
        .padding()
    }
}

Note that if your view owns the Observable object, you will usually mark it with @State and create the object instance in your view. When your Observable object is marked as @State you are able to create bindings to the object's properties. This is thanks to your @State property wrapper annotation.

However, if your view does not own the Observable object, it wouldn't be appropriate to use @State. The @Bindable property wrapper was created to solve this situation and allows you to create bindings to the object's properties.

Usage of Bindable is limited to classes that conform to the Observable protocol. The easiest way to create an Observable conforming object is with the @Observable macro.

Conclusion

In this post, you learned that the key difference between @Binding and @Bindable is in what they do. The @Binding property wrapper indicates that some piece of state on your view is owned by another view and you have both read and write access to the underlying data.

The @Bindable property wrapper allows you to create bindings for properties that are owned by Observable classes. As mentioned earlier,@Bindable is limted to classes that conform to Observable and the easiest way to make Observable objects is the @Observable macro.

As you now know, these two property wrappers co-exist to enable powerful data sharing behaviors.

Cheers!

What’s the difference between Macros and property wrappers?

With Swift 5.9 and Xcode 15, we have the ability to leverage Macros in Swift. Macros can either be written with at @ prefix or with a # prefix, depending on where they're being used. If you want to see some examples of Macros in Swift, you can take a look at this repository that sheds some light on both usage and structure of Macros.

When we look at Macros in action, they can look a lot like property wrappers:

@CustomCodable
struct CustomCodableString: Codable {

  @CodableKey(name: "OtherName")
  var propertyWithOtherName: String

  var propertyWithSameName: Bool

  func randomFunction() {

  }
}

The example above comes from the Macro examples repository. With no other context it's hard to determine whether CodableKey is a property wrapper or a Macro.

One way to find out is to option + click on a Macro which should bring up a useful dialog in Xcode that will make it clear that you're looking at a Macro.

Given how similar Macros and property wrappers look, you might be wondering whether Macros replace property wrappers. Or you might think that they're basically the same thing just with different names.

In reality, Macros are quite different from property wrappers. The key difference is when and where they affect your code and your app.

Property wrappers are executed at runtime. This means that any extra logic that you've added in your property wrapper is applied to your wrapped value while your app is running. This is powerful when you need to manipulate or work with wrapped values in a dynamic fashion.

Macros on the other hand are executed at compile time and they allow us to augment our code by rewriting or expanding code. In other words, Macros allow us to add, rewrite, and modify code at compile time.

For example, there's a #URL Macro that we can use in Xcode 15 to get non-optional URL objects that are validated at compile time. There's also an @Relationship Macro in Swift Data that allows us to generate all code that's needed to define a relationship between two models.

Without digging too deep in different kinds of Macros and how they are defined, the difference is that a Macro defined with a # sign are freestanding. This means that they generate code on their own and aren't applied to an object or property. Macros defined with an @ are applied to something and can't exist on their own like a freestanding Macro can.

Exploring Macros in-depth is a topic for another post.

We can even apply Macros to entire objects like when you apply the @Observable or @Model Macros to your model definitions. Applying a Macro to an object definition is very powerful and allows us to add tons of features and functionality to the object that the Macro is applied to.

For example, when we look at the @Model Macro we can see that it takes code defined like this:

@Model
final class Item {
    var timestamp: Date

    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

And transforms it into this:

@Model
final class Item {
    @PersistedProperty
    var timestamp: Date
    {
        get {
            _$observationRegistrar.access(self, keyPath: \.timestamp)
            return self.getValue(for: \.timestamp)
        }

        set {
            _$observationRegistrar.withMutation(of: self, keyPath: \.timestamp) {
                self.setValue(for: \.timestamp, to: newValue)
            }
        }
    }

    init(timestamp: Date) {
        self.timestamp = timestamp
    }

    @Transient
    public var backingData: any BackingData<Item> = CoreDataBackingData(for: Item.self)

    static func schemaMetadata() -> [(String, AnyKeyPath, Any?, Any?)] {
      return [
        ("timestamp", \Item.timestamp, nil, nil)
      ]
    }

    init(backingData: any BackingData<Item>, fromFetch: Bool = false) {
      self.backingData = backingData
      if !fromFetch {
        self.context?.insert(object: self)
      }
    }

    @Transient
    private let _$observationRegistrar = ObservationRegistrar()
}

extension Item : PersistentModel  {}

extension Item : Observable  {}

Notice how much more code that is, and imagine how tedious it would be to write and manage all this code for every Swift Data model or @Observable object you create.

Macros are a real powerhouse, and they will enable us to write shorter, more concise, and less boilerplate-heavy code. I'm excited to see where Macros go, and how they will make their way into more and more places of Swift.

Conclusion

As you learned in this post, the key difference between Macros and property wrappers in Swift is that Macros are evaluated at compile time while property wrappers are useful at runtime. This means that we can use Macros to generate code on our behalf while we compile our app and property wrappers can be used to change behavior and manipulate properties at runtime.

Even though they both share the @ annotation (and Macros can also have the # annotation in some cases), they do not cover the same kinds of features as you now know.

Cheers!

Tips and tricks for exploring a new codebase

As a developer, joining a new project or company is often a daunting and scary task. You have to get aquatinted with not just a whole new team of people, but you also have to familiarize yourself with an entirely new codebase that might use new naming conventions, follows patterns that you’re not familiar with, or even uses tooling that you’ve never seen before.

There are plenty of reasons to be overwhelmed when you’re a new member of any engineering team, and there’s no reason to feel bad about that.

In the past two years, I’ve done a lot of contracting and consulting which means that I’ve had to explore and understand lots of codebases in short amounts of time. Sometimes even having to explore multiple codebases at once whenever I’d start to work for more than one client in a given week or two.

I guess it's fair to say that I’ve had my fair share of confusion and feeling overwhelmed with new codebases.

In this post, I’d like to provide you with some tips and tricks that I use to get myself comfortable with codebases of any size in a reasonable amount of time.

If you prefer to watch this post as a video, check out the video below:

Meet the team

While it might be tempting to get through your introductory calls as soon as possible so you can spend as much time as possible on navigating and exploring a new codebase, I highly recommend letting the code wait for a little while. Meet the team first.

Getting to know the people that wrote the code that you’re working with can truly help to build a better understanding of the codebase as a whole. Ask questions about team dynamics, and ongoing projects, who’s an expert on what? Building empathy around the code you’ll be working with is a very valuable tool.

Knowing which team members know most about specific features, parts of the codebase, tools that are used in a company, and so on also helps you figure out the right person to ask any questions you might have while you explore the codebase.

For example, when I joined Disney almost six years ago I wasn’t all that familiar with Swiftlint. I had heard about it but I had no idea what it did exactly. In the codebase, I saw some comments that looked as follows:

// swiftlint:disable:next cyclomatic_complexity

Of course, I could paste this comment into Google and go down a rabbit hole on what’s happening and I’d probably have learned a lot about Swiftlint but instead, I chose to figure out who knows most about Swiftlint within the team. Surely that person could help me learn a lot about what Swiftlint was used for and how it works.

I asked my team lead and luckily it was my team lead that actually knew lots and lots of things about Swiftlint, how it was set up, which linter rules we used, and so on.

We had a good chat and by the end of it, I knew exactly why we had Swiftlint at Disney Streaming, which rules we had disabled or enabled and why, and why it was okay to disable certain rules sometimes.

Google could have taught me that the comment you saw earlier disabled a specific linter rule to allow one exception to the rule.

My coworker taught me not just what that comment did but also why it did that. And why that was okay. And when I should or shouldn’t disable certain linter rules myself.

Another example is a more recent one.

One of my clients had a pretty large codebase that has had many people working on it over the years. There’s some Objective-C in there, lots of Swift, it has UIKit and SwiftUI, multiple architecture patterns, and much more. It’s a proper legacy codebase.

Instead of figuring everything out on my own, I had conversations with lots of team members. Sometimes they were one-on-one conversations but other times I met with two or three people at once.

Through these conversations, I learned about various architectural patterns that existed in the codebase. Which ones they considered to be good fits, and which ones they were looking to phase out. I learned why certain bits of code were still in Objective-C, and which parts of the Objective-C codebase should be refactored eventually.

I learned that certain team members had spent a lot of time working on specific features, patterns, and services within the app. They would tell me why certain decisions were made, and which choices they were and weren’t particularly happy with.

After meeting the team I knew so much more about the project, the codebase, the people working on the project, and how things move and evolve within the team. This was incredibly helpful information to have once I started to explore the codebase. Through knowing the team I knew so much more about the why of some bits of code. And I knew that some code wasn’t worth exploring too much because it would be gone soon.

On top of that, through knowing the team, I felt more empathic about bits of code that I didn’t like or didn’t understand. I know who was likely to have worked on that code. So instead of getting frustrated about that bit of code, I knew who I could ask to learn more about the confusing section of code.

Break things

In addition to meeting the team behind your new codebase, you’ll want to start exploring the codebase itself sooner rather than later. One of the key things to figure out is how the project is set up. Which code is responsible for what? How does one thing impact the other?

Hopefully, the codebase follows some well-established patterns that help you figure this out. Regardless, I find it useful to try and break things while I explore.

By introducing flaws in the business logic for an app on purpose, you can learn a lot about the codebase. Sometimes it helps you uncover certain “this should never happen” crashes where a team member used a force unwrap or wrote a guard let with a fatalError inside.

Other times things break in more subtle ways where the app doesn’t quite work but no errors are shown. Or maybe the app is very good about handling errors and it indicates that something went wrong / not as expected but the app informs you about this.

When you break the networking layer in your app, you might uncover some hints about how the app handles caching.

By making small changes that most likely break the app you can learn tons. It’s a technique I often use just to see if there are any threads I should start unraveling to learn more and more about the cool details of a codebase.

Of course, you don’t want to go around and start poking at random things. Usually, when I start exploring I’ll choose one or two features that I want to focus on. This is exactly the focus of my next tip.

Focus on a narrow scope

When you join a large enough codebase, the idea of having all of that code in your head at some point sounds impossible. And honestly, it probably is. There’s a good chance that most developers on the team for a large project will have one or two parts of the codebase internalized. They know everything about it. For everything else, they’ll roughly know which patterns the code should follow (because the whole team follows the same patterns) and they might have some sense of how that code interacts with other modules.

Overall though, it’s just not realistic for any team member to know all of the ins and outs of every module or feature in the codebase.

So why would you be attempting to explore the entire codebase all at once?

If you’re hired on a specific team, focus on the code that would be maintained by that team. Start exploring and understanding that code in as much detail as possible, have team members show you how the code works, and see if you can break some of the code.

Sometimes there will be bug tickets or features that you can start looking at to give you a good starting point to begin learning more about a codebase. If that’s the case, you can use your tickets to help you determine your scope. If you’re working on a bug, focus on understanding everything you can about the section of code that seems most likely to be the source of the bug.

And as always, you’ll want to be in touch with the team. Ask them if they can help you find something to focus on initially. When you have a bug ticket to work on, see if somebody on the team can help you kickstart your research; maybe they have some thoughts on where you can start looking first.

And in an ideal world, leverage pair programming to double the speed at which you learn.

Leverage pair programming

One tool that I usually find to be immensely underused is pair programming. In lots of places where I have worked, developers prefer to work alone. Headphones on, deep in the zone. Questions should be initiated on Slack so you’re disturbed as little as possible. Disable notifications if you have to.

There’s absolutely a time and place for deep focused work where you’re not to be disturbed.

However, there’s an enormous benefit in pairing up with a teammate to explore topics and work on features. Especially when you’ve just joined a team, it’s super important you have access to your team members to help you navigate the company, team, and codebase.

When you’re pairing with a teammate during your exploration phase, you can take the wheel. You can start exploring the codebase, asking questions about what you’re seeing as you go. Especially when you have something to work on, this can be extremely useful.

Any question or thought you might have can immediately be bounced off of your programming partner.

Even if you’re not the person taking the wheel, there’s lots of benefit in seeing somebody else navigate the code and project you’ll work on. Pay close attention to certain utilities or tools they use. If you see something you haven’t seen before, ask about it. Maybe those git commands your coworker uses are used by everybody on the team.

Especially when there’s debugging involved it pays dividends to ask for a pairing session. Seeing somebody that’s experienced with a codebase navigate and debug their code will teach you tons about relationships between certain objects for example.

Two people know more than one, and this is especially true while onboarding a new coworker. So next time a new person joins your team, offer them a couple of pair programming sessions. Or if you’re the new joiner see if there’s somebody interested in spending some time with you while working through some problems and exploring the codebase.

Use breakpoints

When I was working on this post I asked the community how they like to explore a codebase and a lot of people mentioned using a symbolic breakpoint on viewDidLoad or viewDidAppear which I found a pretty cool approach to learning more about the different views and view controllers that are used in a project.

A symbolic breakpoint allows you to pause the execution of your program when a certain method is called on code you might not own. For example, you can have a symbolic breakpoint on UIViewController methods which allows you to see whenever a new subclass of UIViewController is added to the navigation hierarchy.

Knowing this kind of stuff is super useful because you’ll be able to learn which view controller(s) belong to which screen quite quickly.

I haven’t used this one a lot myself but I found it an interesting idea so I wanted to include it in this list of tips.

In Summary

When you join a new team, it’s tempting to keep your head down and study your new codebase. In your head, you might think that you’re expected to already know everything about the codebase even though you’re completely new to the project.

You might think that all patterns and practices in the project are industry standard and that you just haven’t worked in places as good as this one before.

All of these kinds of ideas exist in pretty much anybody’s head and they prevent you from properly learning and exploring a new codebase.

In this post, you have learned some tips about why human interaction is extremely important during your exploration phase. You also learned some useful tips for the more technical side of things to help you effectively tackle learning a new codebase.

Good luck on your next adventure into a new codebase!

Understanding unstructured and detached tasks in Swift

When you just start out with learning Swift Concurrency you’ll find that there are several ways to create new tasks. Roughly speaking, you can make one of three tasks in Swift:

  • Unstructured Tasks
  • Detached Tasks
  • Child Tasks

While it's true that a detached task is also unstructured, in this post I will refer to the two tasks we're covering as detached and unstructured.

In this post, I will focus on unstructured and detached tasks. If you’re interested in learning more about child tasks, I highly recommend that you read the following posts:

These posts go in depth on the relationship between parent and child tasks in Swift Concurrency, how cancellation propagates between tasks, and more.

This post assumes that you understand the basics of structured concurrency which you can learn more about in this post. You don’t have to have mastered the topic of structured concurrency, but having some sense of what structured concurrency is all about will help you understand this post much better.

However, if you're interested in a concurrency deep dive you should check out Practical Swift Concurrency.

Creating unstructured tasks with Task.init

The most common way in which you’ll be creating tasks in Swift will be with Task.init which you’ll probably write as follows without spelling out the .init:

Task {
  // perform work
}

An unstructured task is a task that has no parent / child relationship with the place it called from, so it doesn’t participate in structured concurrency. Instead, we create a completely new island of concurrency with its own scopes and lifecycle.

The reason it doesn't participate in structured concurrency is that your newly created task isn't guaranteed to have finished by the time the enclosing task or function has finished; it becomes its own thing entirely.

However, that doesn’t mean an unstructured task is created entirely independent from everything else.

An unstructured task will inherit three pieces of context from where it’s created:

  • The actor we’re currently running on (if any)
  • Task local values
  • Task priority

The first point means that any tasks that we create inside of an actor will participate in actor isolation for that specific actor. For example, we can safely access an actor’s methods and properties from within a task that’s created inside of an actor:

actor SampleActor {
  var someCounter = 0

  func incrementCounter() {
    Task {
      someCounter += 1
    }
  }
}

If we were to mutate someCounter from a context that is not running on this specific actor we’d have to wrap our someCounter += 1 line with a funcyion that we need to await since we can't directly mutate an actor's state from a place that's not isolated to the actor itself.

Note that our task does not have to complete before the incrementCounter() method returns. That shows us that the unstructured task that we created isn’t participating in structured concurrency. If it were, incrementCounter() would not be able to complete before our task completed.

Similarly, if we spawn a new unstructured task from a context that is annotated with @MainActor, the task will run its body on the main actor:

@MainActor
func fetchData() {
  Task {
    // this task runs its body on the main actor
    let data = await fetcher.getData()

    // self.models is updated on the main actor
    self.models = data
  }
}

It’s important to note that the await fetcher.getData() line does not block the main actor. We’re calling getData() from a context that’s running on the main actor but that does not mean that getData() itself will run its body on the main actor. Unless getData() is explicitly associated with the main actor it will always run on a background thread.

However, the task does run its body on the main actor so once we’re no longer waiting for the result of getData(), our task resumes and self.models is updated on the main actor.

Note that while we await something, our task is suspended which allows the main actor to do other work while we wait. We don’t block the main actor by having an await on it. It’s really quite the opposite.

When to use unstructured tasks

You will most commonly create unstructured tasks when you want to call an async annotated function from a place in your code that is not yet async. For example, you might want to fetch some data in a viewDidLoad method, or you might want to start iterating over a couple of async sequences from within a single place.

Another reason to create an unstructured task might be if you want to perform a piece of work independently of the function you’re in. This could be useful when you’re implementing a fire-and-forget style logging function for example. The log might need to be sent off to a server, but as a caller of the log function I’m not interested in waiting for that operation to complete.

func log(_ string: String) {
  print("LOG", string)
  Task {
    await uploadMessage(string)
    print("message uploaded")
  }
}

We could have made the method above async but then we wouldn’t be able to return from that method until the log message was uploaded. By putting the upload in its own unstructured task we allow log(_:) to return while the upload is still ongoing.

Creating detached tasks with Task.detached

Detached tasks are in many ways similar to unstructured tasks. They don’t create a parent / child relationship, they don’t participate in structured concurrency and they create a brand new island of concurrency that we can work with.

The key difference is that a detached task will not inherit anything from the context that it was created in. This means that a detached task will not inherit the current actor, and it will not inherit task local values.

Consider the example you saw earlier:

actor SampleActor {
  var someCounter = 0

  func incrementCounter() {
    Task {
      someCounter += 1
    }
  }
}

Because we used a unstructed task in this example, we were able to interact with our actor’s mutable state without awaiting it.

Now let’s see what happens when we make a detached task instead:

actor SampleActor {
  var someCounter = 0

  func incrementCounter() {
    Task.detached {
      // Actor-isolated property 'someCounter' can not be mutated from a Sendable closure
      // Reference to property 'someCounter' in closure requires explicit use of 'self' to make capture semantics explicit
      someCounter += 1
    }
  }
}

The compiler now sees that we’re no longer on the SampleActor inside of our detached task. This means that we have to interact with the actor by calling its methods and properties with an await.

Similarly, if we create a detached task from an @MainActor annotated method, the detached task will not run its body on the main actor:

@MainActor
func fetchData() {
  Task.detached {
    // this task runs its body on a background thread
    let data = await fetcher.getData()

    // self.models is updated on a background thread
    self.models = data
  }
}

Note that detaching our task has no impact at all on where getData() executed. Since getData() is an async function it will always run on a background thread unless the method was explicitly annotated with an @MainActor annotation. This is true regardless of which actor or thread we call getData() from. It’s not the callsite that decides where a function runs. It’s the function itself.

When to use detached tasks

Using a detached task only makes sense when you’re performing work inside of the task body that you want to run away from any actors no matter what. If you’re awaiting something inside of the detached task to make sure the awaited thing runs in the background, a detached task is not the tool you should be using.

Even if you only have a slow for loop inside of a detached task, or you're encoding a large amount of JSON, it might make more sense to put that work in an async function so you can get the benefits of structured concurrency (the work must complete before we can return from the calling function) as well as the benefits of running in the background (async functions run in the background by default).

So a detached task really only makes sense if the work you’re doing should be away from the main thread, doesn’t involve awaiting a bunch of functions, and the work you’re doing should not participate in structured concurrency.

As a rule of thumb I avoid detached tasks until I find that I really need one. Which is only very sporadically.

In Summary

In this post you learned about the differences between detached tasks and unstructured tasks. You learned that unstructured tasks inherit context while detached tasks do not. You also learned that neither a detached task nor an unstructured task becomes a child task of their context because they don’t participate in structured concurrency.

You learned that unstructured tasks are the preferred way to create new tasks. You saw how unstructured tasks inherit the actor they are created from, and you learned that awaiting something from within a task does not ensure that the awaited thing runs on the same actor as your task.

After that, you learned how detached tasks are unstructured, but they don’t inherit any context from when they are created. In practice this means that they always run their bodies in the background. However, this does not ensure that awaited functions also run in the background. An @MainActor annotated function will always run on the main actor, and any async method that’s not constrained to the main actor will run in the background. This behavior makes detached tasks a tool that should only be used when no other tool solves the problem you’re solving.

Structured concurrency in Swift explained

Swift's async await syntax heavily relies on a concept called Structured Concurrency. Structured concurrency describes the relationship between tasks that perform concurrent work. Specifically, it defines the relationship between parent and child tasks in Swift. Structured Concurrency finds its roots in the fork join model which is a model that stems from the sixties.

In this post, I will explain what structured concurrency means, and how it plays an important role in Swift Concurrency.

Note that this post is not an introduction to using async and await in Swift. IIf you're interested in learning more about async await in Swift, I highly recommend that you take a look at the Swift Concurrency category. These posts all help you learn specific bits and pieces of modern Concurrency in Swift. For example, how you can use task groups, actors, async sequences, and more.

If you're looking for a full introduction to async await in Swift, I recommend you check out my book. In my book I go in depth on all the important parts of Swift Concurrency that you need to know in order to make the most out of modern concurrency features in Swift.

Anyway, back to structured concurrency. We’ll start by looking at the concept from a high level before looking at a few examples of Swift code that illustrates the concepts of structured concurrency nicely.

Understanding the concept of structured concurrency

The concepts behind Swift’s structured concurrency are neither new nor unique. Sure, Swift implements some things in its own unique way but the core idea of structured concurrency can be dated back all the way to the sixties in the form of the fork join model.

The fork join model describes how a program that performs multiple pieces of work in parallel (fork) will wait for all work to complete, receiving the results from each piece of work (join) before continuing to the next piece of work.

We can visualize the fork join model as follows:

Fork Join Model example

In the graphic above you can see that the first task kicks off three other tasks. One of these tasks kicks off some sub-tasks of its own. The original task cannot complete until it has received the results from each of the tasks it spawned. The same applies to the sub-task that kicks of its own sub-tasks.

You can see that the two purple colored tasks must complete before the task labelled as Task 2 can complete. Once Task 2 is completed we can proceed with allowing Task 1 to complete.

Swift Concurrency is heavily based on this model but it expands on some of the details a little bit.

For example, the fork join model does not formally describe a way for a program to ensure correct execution at runtime while Swift does provide these kinds of runtime checks. Swift also provides a detailed description of how error propagation works in a structured concurrency setting.

When any of the child tasks spawned in structured concurrency fails with an error, the parent task can decide to handle that error and allow other child tasks to resume and complete. Alternatively, a parent task can decide to cancel all child tasks and make the error the joined result of all child tasks.

In either scenario, the parent task cannot complete while the child tasks are still running. If there’s one thing you should understand about structured concurrency that would be it. Structured concurrency’s main focus is describing how parent and child tasks relate to each other, and how a parent task can not complete when one or more of its child tasks are still running.

So what does that translate to when we explore structured concurrency in Swift specifically? Let’s find out!

Structured concurrency in action

In its simplest and most basic form structured concurrency in Swift means that you start a task, perform some work, await some async calls, and eventually your task completes. This could look as follows:

func parseFiles() async throws -> [ParsedFile] {
  var parsedFiles = [ParsedFile]()

  for file in list {
    let result = try await parseFile(file)
    parsedFiles.append(result)
  }

  return parsedFiles
}

The execution for our function above is linear. We iterate over a list of files, we await an asynchronous function for each file in the list, and we return a list of parsed files. We only work on a single file at a time and at no point does this function fork out into any parallel work.

We know that at some point our parseFiles() function was called as part of a Task. This task could be part of a group of child tasks, it could be task that was created with SwiftUI’s task view modifier, it could be a task that was created with Task.detached. We really don’t know. And it also doesn’t really matter because regardless of the task that this function was called from, this function will always run the same.

However, we’re not seeing the power of structured concurrency in this example. The real power of structured concurrency comes when we introduce child tasks into the mix. Two ways to create child tasks in Swift Concurrency are to leverage async let or TaskGroup. I have detailed posts on both of these topics so I won’t go in depth on them in this post:

Since async let has the most lightweight syntax of the two, I will illustrate structured concurrency using async let rather than through a TaskGroup. Note that both techniques spawn child tasks which means that they both adhere to the rules from structured concurrency even though there are differences in the problems that TaskGroup and async let solve.

Imagine that we’d like to implement some code that follows the fork join model graphic that I showed you earlier:

Fork Join Model example

We could write a function that spawns three child tasks, and then one of the three child tasks spawns two child tasks of its own.

The following code shows what that looks like with async let. Note that I’ve omitted various details like the implementation of certain classes or functions. The details of these are not relevant for this example. The key information you’re looking for is how we can kick off lots of work while Swift makes sure that all work we kick off is completed before we return from our buildDataStructure function.

func buildDataStructure() async -> DataStructure {
  async let configurationsTask = loadConfigurations()
  async let restoredStateTask = loadState()
  async let userDataTask = fetchUserData()

  let config = await configurationsTask
  let state = await restoredStateTask
  let data = await userDataTask

  return DataStructure(config, state, data)
}

func loadConfigurations() async -> [Configuration] {
  async let localConfigTask = configProvider.local()
  async let remoteConfigTask = configProvider.remote()

  let (localConfig, remoteConfig) = await (localConfigTask, remoteConfigTask)

  return localConfig.apply(remoteConfig)
}

The code above implements the same structure that is outlined in the fork join sample image.

We do everything exactly as we’re supposed to. All tasks we create with async let are awaited before the function that we created them in returns. But what happens when we forget to await one of these tasks?

For example, what if we write the following code?

func buildDataStructure() async -> DataStructure? {
  async let configurationsTask = loadConfigurations()
  async let restoredStateTask = loadState()
  async let userDataTask = fetchUserData()

  return nil
}

The code above will compile perfectly fine. You would see a warning about some unused properties but all in all your code will compile and it will run just fine.

The three async let properties that are created each represent a child task and as you know each child task must complete before their parent task can complete. In this case, that guarantee will be made by the buildDataStructure function. As soon as that function returns it will cancel any running child tasks. Each child task must then wrap up what they’re doing and honor this request for cancellation. Swift will never abruptly stop executing a task due to cancellation; cancellation is always cooperative in Swift.

Because cancellation is cooperative Swift will not only cancel the running child tasks, it will also implicitly await them. In other words, because we don’t know whether cancellation will be honored immediately, the parent task will implicitly await the child tasks to make sure that all child tasks are completed before resuming.

How unstructured and detached tasks relate to structured concurrency

In addition to structured concurrency, we have unstructured concurrency. Unstructured concurrency allows us to create tasks that are created as stand alone islands of concurrency. They do not have a parent task, and they can outlive the task that they were created from. Hence the term unstructured. When you create an unstructured task, certain attributes from the source task are carried over. For example, if your source task is main actor bound then any unstructured tasks created from that task will also be main actor bound.

Similarly if you create an unstructured task from a task that has task local values, these values are inherited by your unstructured task. The same is true for task priorities.

However, because an unstructured task can outlive the task that it got created from, an unstructured task will not be cancelled or completed when the source task is cancelled or completed.

An unstructured task is created using the default Task initializer:

func spawnUnstructured() async {
  Task {
    print("this is printed from an unstructured task")
  }
}

We can also create detached tasks. These tasks are both unstructured as well as completely detached from the context that they were created from. They do not inherit any task local values, they do not inherit actor, and they do not inherit priority.

I cover detached and unstructured tasks more in depth right here.

In Summary

In this post, you learned what structured concurrency means in Swift, and what its primary rule is. You saw that structured concurrency is based on a model called the fork join model which describes how tasks can spawn other tasks that run in parallel and how all spawned tasks must complete before the parent task can complete.

This model is really powerful and it provides a lot of clarity and safety around the way Swift Concurrency deals with parent / child tasks that are created with either a task group or an async let.

We explored structured concurrency in action by writing a function that leveraged various async let properties to spawn child tasks, and you learned that Swift Concurrency provides runtime guarantees around structured concurrency by implicitly awaiting any running child tasks before our parent task can complete. In our example this meant awaiting all async let properties before returning from our function.

You also learned that we can create unstructured or detached tasks with Task.init and Task.detached. I explained that both unstructured and detached tasks are never child tasks of the context that they were created in, but that unstructured tasks do inherit some context from the context they were created in.

All in all the most important thing to understand about structured concurrency is that it provide clear and rigid rules around the relationship between parent and child tasks. In particular it describes how all child tasks must complete before a parent task can complete.

Setting up a simple local web socket server

Every once in a while I find myself writing about or experimenting with web sockets. As an iOS developer, I’m not terribly familiar with setting up and programming servers that leverage web sockets beyond some toy projects in college.

Regardless, I figured that since I have some posts that cover web sockets on my blog, I should show you how I set up the socket servers that I use in those posts. Before you read on, I’m going to need you to promise me you won’t take the code I’m about to show you to a production environment…

You promise? Good.

I generally use the [WebSocket (or ws) package from npm](https://www.npmjs.com/package/ws) along with node.js. I chose these technologies because that’s what was around when I first learned about web sockets, and because it works well for my needs. If you prefer different tools and languages that’s perfectly fine of course; I just won’t cover them on here.

Once you have node installed on your machine (go here if you haven’t already installed node.js) you can create a new folder somewhere on your machine, and navigate to that folder in your terminal. Then type npm install ws to install the ws package in your current directory (so make sure you’re in your project folder when typing this!).

After that, create a file called index.mjs (that’s not a typo; it’s a fancy new JavaScript module extension) and add the following contents to it:

import WebSocket, { WebSocketServer } from 'ws';

const wss = new WebSocketServer({port: 8080});

Usually when I’m experimenting I like to do something simple like:

  • For a new connection, start listening for incoming messages and do something in response; for example, close the connection.
  • Send a “connection received” message
  • Send a new message every second
  • When the received connection is closed, stop sending messages over the socket (nobody is listening anymore)

The code to do this looks a bit as follows:

const wss = new WebSocketServer({port: 8080});

wss.on('connection', function connection(wss) {
    wss.on('message', function message(data) {
        console.log('received %s', data);
        wss.close();
    });

    wss.send('connection received');

    var t = setInterval(function() {
        console.log("sending message");
        wss.send('sending message!');
    }, 1000);

    wss.on('close', function close() {
        console.log("received close");
        clearInterval(t);
      });
});

Again, I’m not a professional JavaScript developer so there might be much nicer ways to do the above but this is what works for the purposes I tend to use web sockets for which is always purely experimental.

For a full overview of web socket events that you might want to add handlers for, I highly recommend you take a look at the docs for ws.