Solving “Capture of non-sendable type in @Sendable closure” in Swift

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

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

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

Capture of non-sendable type in @Sendable closure

This warning tells us that we're capturing and using a property inside of a closure. This closure is marked as @Sendable which means that we should expect this closure to run in a concurrent environment. The Swift compiler warns us that, because this closure will run concurrently, we should make sure that any properties that we capture inside of this closure can safely be used from concurrent code.

In other words, the compiler is telling us that we're risking crashes because we're passing an object that can't be used from multiple tasks to a closure that we should expect to be run from multiple tasks. Or at least we should expect our closure to be transferred from one task to another.

Of course, there's no guarantees that our code will crash. Nor is it guaranteed that our closure will be run from multiple places at the same time. What matters here is that the closure is marked as @Sendable which tells us that we should make sure that anything that's captured inside of the closure is also Sendable.

For a quick overview of Sendability, check out my post on the topic here.

An example of where this warning might occur could look like this:

func run(completed: @escaping TaskCompletion) {
    guard !metaData.isFinished else {
        DispatchQueue.main.async {
            // Capture of 'completed' with non-sendable type 'TaskCompletion' (aka '(Result<Array<any ScheduledTask>, any Error>) -> ()') in a `@Sendable` closure; this is an error in the Swift 6 language mode
            // Sending 'completed' risks causing data races; this is an error in the Swift 6 language mode
            completed(.failure(TUSClientError.uploadIsAlreadyFinished))
        }
        return
    }

    // ...
}

The compiler is telling us that the completed closure that we're receiving in the run function can't be passed toDispatchQueue.main.async safely. The reason for this is that the run function is assumed to be run in one isolation context, and the closure passed to DispatchQueue.main.async will run in another isolation context. Or, in other words, run and DispatchQueue.main.async might run as part of different tasks or as part of different actors.

To fix this, we need. to make sure that our TaskCompletion closure is @Sendable so the compiler knows that we can safely pass that closure across concurrency boundaries:

// before
typealias TaskCompletion = (Result<[ScheduledTask], Error>) -> ()

// after
typealias TaskCompletion = @Sendable (Result<[ScheduledTask], Error>) -> ()

In most apps, a fix like this will introduce new warnings of the same kind. The reason for this is that because the TaskCompletion closure is now @Sendable, the compiler is going to make sure that every closure passed to our run function doesn't captuire any non-sendable types.

For example, one of the places where I call this run function might look like this:

task.run { [weak self] result in
    // Capture of 'self' with non-sendable type 'Scheduler?' in a `@Sendable` closure; this is an error in the Swift 6 language mode
    guard let self = self else { return }
    // ...
}

Because the closure passed to task.run needs to be @Sendable any captured types also need to be made Sendable.

At this point you'll often find that your refactor is snowballing into something much bigger.

In this case, I need to make Scheduler conform to Sendable and there's two ways for me to do that:

  • Conform Scheduler to Sendable
  • Make Scheduler into an actor

The second option is most likely the best option. Making Scheduler an actor would allow me to have mutable state without data races due to actor isolation. Making the Scheduler conform to Sendable without making it an actor would mean that I have to get rid of all mutable state since classes with mutable state can't be made Sendable.

Using an actor would mean that I can no longer directly access a lot of the state and functions on that actor. It'd be required to start awaiting access which means that a lot of my code has to become async and wrapped in Task objects. The refactor would get out of control real fast that way.

To limit the scope of my refactor it makes sense to introduce a third, temporary option:

  • Conform Scheduler to Sendable using the unchecked attribute

For this specific case I have in mind, I know that Scheduler was written to be thread-safe. This means that it's totally safe to work with Scheduler from multiple tasks, threads, and queues. However, this safety was implemented using old mechanisms like DispatchQueue. As a result, the compiler won't just accept my claim that Scheduler is Sendable.

By applying @unchecked Sendable on this class the compiler will accept that Scheduler is Sendable and I can continue my refactor.

Once I'm ready to convert Scheduler to an actor I can remove the @unchecked Sendable, change my class to an actor and continue updating my code and resolving warnings. This is great because it means I don't have to jump down rabbit hole after rabbit hole which would result in a refactor that gets way out of hand and becomes almost impossible to manage correctly.

Solving “Reference to captured var in concurrently-executing code” in Swift

In Xcode 16, this error actually is sometimes presented as "Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure". The cause is the exact same as what's covered in this post.

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

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

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

Reference to captured var in concurrently-executing code

This warning tells you that you're capturing a variable inside of a body of code that will run asynchornously. For example, the following code will result in this warning:

var task = NetworkTask<Int, URLSessionUploadTask>(
    urlsessionTask: urlSessionTask
)

upload(fromTask: urlSessionTask, metaData: metaData, completion: { result in
    Task {
        await task.sendResult(result) // Reference to captured var 'task' in concurrently-executing code; this is an error in the Swift 6 language mode
    }
})

The task variable that we create a couple of lines earlier is mutable. This means that we can assign a different value to that task at any time and that could result in inconsistencies in our data. For example, if we assign a new value to the task before the closure starts running, we might have captured the old task which could be unexpected.

Since strict concurrency is meant to help us make sure that our code runs as free of surprises as possible, Swift wants us to make sure that we capture a constant value instead. In this case, I'm not mutating task anyway so it's safe to make it a let:

let task = NetworkTask<Int, URLSessionUploadTask>(
    urlsessionTask: urlSessionTask
)

upload(fromTask: urlSessionTask, metaData: metaData, completion: { result in
    Task {
        await task.sendResult(result)
    }
})

This change gets rid of the warning because the compiler now knows for sure that task won't be given a new value at some unexpected time.

Another way to fix this error would be to make in explicit capture in the completion closure that I'm passing. This capture will happen immediately as a let so Swift will know that the captured value will not change unexpectedly.

var task = NetworkTask<Int, URLSessionUploadTask>(
    urlsessionTask: urlSessionTask
)

upload(fromTask: urlSessionTask, metaData: metaData, completion: { [task] result in
    Task {
        await task.sendResult(result.mapError({ $0 as any Error }))
    }
})

Altenatively, you could make an explicit constant capture before your Task runs:

var task = NetworkTask<Int, URLSessionUploadTask>(
    urlsessionTask: urlSessionTask
)

let theTask = task
upload(fromTask: urlSessionTask, metaData: metaData, completion: { result in
    Task {
        await theTask.sendResult(result)
    }
})

This is not as elegant but might be needed in cases where you do want to pass your variable to a piece of concurrently executing code but you also want it to be a mutable property for other objects. It's essentially the exact same thing as making a capture in your completion closure (or directly in the task if there's no extra wrapping closures involved).

When you first encounter this warning it might be immediately obvious why you're seeing this error and how you should fix it. In virtual all cases it means that you need to either change your var to a let or that you need to perform an explicit capture of your variable either by making a shadowing let or through a capture list on the first concurrent bit of code that accesses your variable. In the case of the example in this post that's the completion closure but for you it might be directly on the Task.

Adding values to the SwiftUI environment with Xcode 16’s Entry macro

Adding custom values to SwiftUI’s environment has never been very hard to do to. However, the syntax for doing it is verbose and easy to forget. To refresh your mind, take a look at this post where I explain how to add your own environment values to a SwiftUI view.

To summarize what’s shown in that post; here’s how you add a custom value to the environment using Xcode 15 and earlier:

private struct DateFormatterKey: EnvironmentKey {
    static let defaultValue: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "MM/dd/yyyy"
        return formatter
    }()
}

extension EnvironmentValues {
    var dateFormatter: DateFormatter {
        get { self[DateFormatterKey.self] }
        set { self[DateFormatterKey.self] = newValue }
    }
}

We have to define an environment key, define a default value, and write a getter and setter to retrieve our value from the environment using our key.

This is repetitive, easy to forget, and just annoying to do.

If you prefer learning thorugh video, here's the video to watch:

Luckily, in Xcode 16 we have access to the @Entry macro. This macro allows us to define the exact same environment key like this:

extension EnvironmentValues {
    @Entry var dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "MM/dd/yyyy"
        return formatter
    }()
}

All we have to define now is a variable that’s annotated with @Entry and we’re done.

The property name is used as the environment key so in this case we’d set our date formatter like this:

myView
    .environment(\.dateFormatter, Dateformatter())

I absolutely love this new syntax because it gets rid of all the boilerplate in one go.

And the best part of this macro?

We can use it in projects that target iOS versions older than 18! So as soon as you start developing your project with Xcode 16 you’ll be able to use this macro regardless of your deployment target.

Let and var in Swift explained

Virtually every programming language will have some means to define properties; Swift does too. We have two approaches to defining a property in Swift. We can use a var or a let. The code below shows how we can define a var or a let as a member of a class:

class Member {
  let id: UUID
  var name: String

  init(name: String) {
    self.id = UUID()
    self.name = name
  }
}

This class has two properties. One is a let, the other is a var.

If you're coming from a Javascript background you might expect that there's a third option here; const. That's not the case in Swift. Swift only has let and var and a let in Swift might not be what you think it is.

A var property is a variable. That means that whatever we assign to the var can change over time. For example, when I make an instance of my Member object, I can change the name as needed:

var instance = Member(name: "Donny")
instance.name = "Hello, world!"

And because I defined instance as a var, I'm even able to create a new Member and assign it to my instance variable:

var instance = Member(name: "Donny")
instance.name = "Hello, world!"

instance = Member(name: "Oliver")

We also refer to a var as being mutable. This is another way of saying that the value for this property can change.

A let is the opposite. It's a constant value. This means that once we've assigned a value, we can't change it.

For example, if I define my instance as a let instead of a var I'm no longer allowed to assign a new value to instance:

// notice how intstance is now defined as a let
let instance = Member(name: "Donny")
instance.name = "Hello, world!"

instance = Member(name: "Oliver") // not allowed, instance is a let

Additionally, because my Member defined id as a let, I can't change that either:

let instance = Member(name: "Donny")
instance.id = UUID() // not allowed, id is a let

I can, however still change the name:

let instance = Member(name: "Donny")
instance.name = "Hello, world!"

That's because changing a property on my class instance will propagate as a change to let instance. The class instance assigned to let instance is still the exact same one. We just changed one of the properties.

This changes when we'd make Member a struct:

struct Member {
  let id: UUID
  var name: String

  init(name: String) {
    self.id = UUID()
    self.name = name
  }
}

The properties on Member are the exact same. The only difference is that we've made Member a struct instead of a class.

I won't expand into the difference between structs and classes too much in this post, but it's important to understand that a class is assigned to a variable(var) or constant(let) using its address in memory. So instead of storing the actual class value in our property, we only store the location of our class instance. That's why changing a value on our instance doesn't re-assign to our let instance in the example above.

Structs on the other hand are generally stored by value. This means that when you change a property on a struct, Swift will have to re-assign the new value to the property that's storing your instance. Let's see this in action:

let instance = Member(name: "Donny")
instance.name = "Hello, world!" // this is not allowed because `instance` is immutable

What's happening in the code above is that we've assigned a value to let instance. When we change the name of our instance, Swift has to replace the old value of instance with a new one because it's a struct and structs are stored using their values.

To allow mutating our instance.name, we have to store the instance as a var:

var instance = Member(name: "Donny")
instance.name = "Hello, world!" // this is allowed because `instance` is a variable

Now Swift is able to make a copy of our Member with the updated name and then assign it back to var instance.

We generally like to write our code using let instead of var whenever we can. The fewer properties we can change, the more predictable our code becomes, and the fewer bugs we'll ship. However, a program that never changes any of its properties wouldn't be very interesting because it'd just be a static page. So in those situations where you do need the ability to re-assign or update a property it makes sense to define that property as a var. When in doubt, use let. Then change it to a var when you find that you do have a need to update that specific property later on.

Using PreviewModifier to build a previewing environment

Xcode 16 and iOS 18 come with a feature that allows us to build elaborate preview environments using a new PreviewModifier protocol. This protocol allows us to define objects that can create a single context or environment that’s cached and used across your SwiftUI previews.

This is useful because it means that you could, for example, populate a database with a bunch of mock data that is then used in your previews.

You can also use PreviewModifier to apply specific styling to your previews, to wrap them all in a specific wrapper, and more.

Essentially, they’re a tool that allows you to configure your previews consistently across the board.

Decorating views using PreviewModifier

The PreviewModifier protocol specifies two methods that you can implement:

  • A static makeSharedContext method
  • An instance method called body

The body instance methods is passed the view that’s being previewed and a Context. This context can either be an object that you created in makeSharedContext or Void if you don’t implement makeSharedContext.

For this example, let’s assume that you went ahead and did not implement makeSharedContext. In a situation like that, we can use PreviewModifier to decorate a view for our previews. For example, we could wrap it in another view or apply some styling to it.

I’m pretty sure that you’re more creative than me so I’m just going to go ahead and show you how you would apply a orange background to your previewed view. Yeah, I know… very creative. The point is to show you how to do this so that you can do something much smarter than what I’m describing here.

struct OrangeBackground: PreviewModifier {
    func body(content: Content, context: Void) -> some View {
        content
            .padding()
            .background {
                RoundedRectangle(cornerRadius: 16)
                    .fill(.orange)
            }
    }
}

#Preview(traits: .modifier(OrangeBackground())) {
    Text("Hello, world!")
}

Let’s look at the PreviewModifier first, and then I’ll explain how I applied it to my preview.

The modifier is defined as a struct and I only implemented the body function.

This function is padded Content which is whatever view the #Preview macro is being used on (in this case, Text), and it receives a context. In this case Void because I didn’t make a context.

The content argument can be styled, modified, and wrapped however you need. It’s a view so you can do things like give it a background, transform it, adjust its environment, and much much more. Anything you can do with a view inside of a View body you can do here.

The main difference is that you’re receiving a fully instantiated instance of your view. That means you can’t inject new state or bindings into it or otherwise modify it. You can only apply view modifiers to it.

This brings us to our next feature of PreviewModifier creating a context to provide mocked data and more.

Using PreviewModifier to inject mock data

To inject mock data into your previews through PreviewModifier all you need to do is implement the makeSharedContext method from the PreviewModifier protocol. This method is static and is called once for all your previews. This means that the context that you create in this method is reused for all of your previews.

In practice this is nice because it means you get consistent mock data for your previews without the overhead of recreating this data frequently.

Here’s what a sample implementation for makeSharedContext looks like:

struct MockDataSource {
    // ...
}

struct OrangeBackground: PreviewModifier {
    static func makeSharedContext() async throws -> MockDataSource {
        return MockDataSource()
    }
}

In this case, I’m creating an instance of some data source in my makeSharedContext method. This MockDataSource would hold all mocks and all data for my views which is great.

However, the only way for us to really use that mock data in our view is by adding our data source (or the mocked data) to our previewed view’s environment.

struct OrangeBackground: PreviewModifier {
    static func makeSharedContext() async throws -> MockDataSource {
        return MockDataSource()
    }

    func body(content: Content, context: MockDataSource) -> some View {
        content
            .environment(\.dataSource, context)
    }
}

Since we can’t make a new instance of our content, we can’t inject our mock data source directly into the view through its initializer. The only way we can get the data source to the view is by adding it to the environment.

This is not ideal in my opinion, but the design makes sense.

I’m also pretty sure that Apple designed this API with mocking SwiftData databases in mind and it would work great for that.

On top of having to use the environment, the PreviewModifier only works in projects that target iOS 18 or later. Not a huge problem but it would have been nice if using Xcode 16 was good enough for us to be able to use this handy new API.

Mixing colors in SwiftUI and Xcode 16

SwiftUI in iOS 18 and macOS 15 has gained a new trick; it can mix colors. This means that it’s now possible to take a color and modify it by applying another color to it using a provided percentage.

The video below shows how this works:

Notice how the large rectangle updates its color to be a certain mix of a left and right color.

In the video I use distinct colors but you can also mix with white or black to lighten or darken your color.

One use of color mixing I like a lot is to explore color palettes. Since you can see which colors “fit” between two distinct colors you get to explore color in a way that, to me, is very inspiring.

If you prefer learning through video over learning through text, here's a video that I made a companion for this post:

Here’s the code that allows you to mix two colors in SwiftUI:

let leftColor = Color.pink
let rightColor = Color.blue
let mix = 0.5

// create a rectangle filled with our mixed color
RoundedRectangle(cornerRadius: 16)
    .fill(leftColor.mix(with: rightColor, by: mix, in: .perceptual))
    .frame(width: 100, height: 100)

The API is pretty straightforward. You take a color and you call mix on it. You pass a second color, a mixing value between 0 and 1, and whether you want to interpolate the mixed color in a perceptual color space or using the device color space.

By default, perceptual will be used since that should, in theory, mix colors in a way that makes sense to the human eye and is consistent between different device screens. Mixing based on device color space can yield different results that may or may not be what you’re looking for; I recommend experimenting to see the exact differences.

The mixing value that you provide determines how much of the second color should be mixed into the source color. A value of 0 gets you the original color and a value of 1 replaces the original color entirely with the color you’re mixing in.

If you’re interested in rebuilding the experiment UI from the start of this post, you can grab the code right here:

struct ColorMix: View {
    @State private var leftColor = Color.blue
    @State private var rightColor = Color.pink
    @State private var mix = 0.5

    var body: some View {
        VStack {
            HStack(spacing: 8) {
                ColorPicker("Left", selection: $leftColor)
                    .labelsHidden()
                ColorPicker("Right", selection: $rightColor)
                    .labelsHidden()
            }

            HStack {
                VStack {
                    RoundedRectangle(cornerRadius: 16)
                        .fill(leftColor)
                        .frame(width: 100, height: 100)
                    Text("\((1 - mix), format: .percent.precision(.fractionLength(0...2)))")
                }

                VStack {
                    RoundedRectangle(cornerRadius: 16)
                        .fill(rightColor)
                        .frame(width: 100, height: 100)
                    Text("\(mix, format: .percent.precision(.fractionLength(0...2)))")
                }
            }

            // create a rectangle filled with our mixed color
            RoundedRectangle(cornerRadius: 16)
                .fill(leftColor.mix(with: rightColor, by: mix, in: .perceptual))
                .frame(width: 100, height: 100)

            Slider(value: $mix, in: 0...1)
        }
    }
}

Using iOS 18’s new TabView with a sidebar

In iOS 18, Apple has revamped the way that tab bars look. They used to be positioned at the bottom of the screen with an icon and a text underneath. Starting with iOS 18, tab bars will no longer be displayed in that manner.

Instead, on iPad you will have your tab bar on the top of the screen with text-only items while on iPhone your tab bar will retain its old look.

In addition to changing how a tab bar looks, Apple has also added new behavior to the tab bar; it can expand into a sidebar that contains a more detailed hierarchy of navigation items.

In this post, I’d like to take a look at this feature and in particular I’d like to share some things that I’ve learned about how Apple handles sidebars that contain sectioned content. Consider this post to be both a demonstration of how you can have a TabBar that doubles as a sidebar as well as some tips and tricks that will help you craft a great experience when you choose to adopt a TabBar that can become a sidebar with sections.

Understanding our goal

Now, I could show you the SwiftUI views and view modifiers you need in order to build a sidebar / tabview pair for iPad and I could show you that it works and end this post there. However, that would be a little bit too shortsighted and you might just as well watch Apple’s own content on this topic instead.

What I’d like to show you in this post, is how you can leverage a sectioned sidebar that makes sense and also has a tab bar that actually works well on phones. In this screenshot you can see all the different variants of the tab/sidebar that I want to support.

Our TabView in various configurations

Notice how my tab bar has only a couple of items in it in the compact mode that’s used for a split-screen iPad or iPhone. On my full width iPad display I have a tab bar that contains several elements like “Blog” and “Books”. And when shown as a sidebar, these tab bar items become category headings instead.

Supporting all this is fairly straightforward but it comes with some gotchas that I’d like to outline in this post.

Setting up our TabView and Sections

While we do need to take into account several form factors and write some special code to handle smaller screens we’ll start by building out our large-screen TabView first.

Within a TabView we can define both Tab and TabSection items. A Tab is shown as a tab in the tab view and the sidebar too. In the screenshot above I’ve added Main and Search as Tab in my TabView. You can see that they’re not grouped under any header.

Then there’s Blog, Books, Courses, and more. These are sections that all contain their own list of tabs.

Let’s go right ahead and look at the code that I use to build my hierarchy of tabs and sections. I’ll only include a single TabSection since the code would be pretty long and repetitive otherwise.

var body: some View {
    TabView {
        Tab("Main", systemImage: "house") {
            OverviewView()
        }

        TabSection("Blog") {
            Tab("All topics", systemImage: "pencil") {
                Text("This is the blog page")
            }

            Tab("SwiftUI", systemImage: "swift") {
                Text("SwiftUI topic")
            }

            Tab("Concurrency", systemImage: "timelapse") {
                Text("Concurrency topic")
            }

            Tab("Persistence", systemImage: "swiftdata") {
                Text("Persistence topic")
            }
        }

        // .. more TabSections

        Tab(role: .search) {
            Text("Search the site")
        }
    }
}

If I’d run this code as-is, my TabView would work but user’s won’t be able to toggle it into a sidebar. We’ll fix that in a moment. Let’s look at my hierarchy first.

My top-level Tab objects will always be shown on my tab bar. The Tab(role: .search) that I have here is a special case; that tab will always be shown on the trailing side of my tab bar with a search icon.

My TabSection is an interesting case. In tab bar view, the section’s name will be used as the name for my tab bar item. The view that’s shown to the user when they select this tab bar item is the detail view for the first Tab in the section. So in this case, that’s “All topics”. This is great because “All topics” is an overview page for the section.

TabView on iPad

When running on a small screen however, every Tab is added to the tab bar regardless of their sections. This means that on iPhone, the tab bar is cluttered with all kinds of tab bar items we don’t want.

Here’s what we get when we run on iPhone. Notice that we don’t see the same tab bar items. Instead, every Tab we’ve defined at any level is being listed.

The same TabView on iPhone with way too many tabs

We’ll fix this after we enable sidebar toggling.

Enabling sidebar toggling

To allow users to switch our tab bar into a sidebar, we need to apply the tabViewStyle view modifier to the TabView as follows:

var body: some View {
    TabView {
      // tabs and sections...
    }
    .tabViewStyle(.sidebarAdaptable)
}

By setting the tabViewStyle to sidebarAdaptable, users can now toggle between our tab bar and a sidebar easily.

In sidebar mode, all of our root Tab items are listed first. After that, sections are listed with the section name as headers, and in each section we see the Tab views that we’ve added.

Our app with a sidebar

Switching between a sidebar and tab bar looks pretty good now and it works well.

But for smaller size classes (like phones and split-view iPad) we’ll want to do something else.

Let’s see how we can adapt our TabView to smaller screens.

Adapting the TabView to smaller screens

In SwiftUI, we can gain access to the current size class for our view through the environment. Since our TabView will become a traditional tab bar at the bottom of the screen on compact size classes and be in the new style on regular we can actually change the contents of our TabView based on the size class so that all extra items we had before will be gone if the size class is compact. Here’s what that looks like:

@Environment(\.horizontalSizeClass)
var horizontalSize

var body: some View {
    TabView {
        Tab("Main", systemImage: "house") {
            OverviewView()
        }

        if horizontalSize == .regular {
            TabSection("Blog") {
                Tab("All topics", systemImage: "pencil") {
                    Text("This is the blog page")
                }

                Tab("SwiftUI", systemImage: "swift") {
                    Text("SwiftUI topic")
                }

                Tab("Concurrency", systemImage: "timelapse") {
                    Text("Concurrency topic")
                }

                Tab("Persistence", systemImage: "swiftdata") {
                    Text("Persistence topic")
                }
            }
        } else {
            Tab("Blog", systemImage: "pencil") {
                Text("This is the blog page")
            }
        }

        // repeat for other sections...
    }
}

The code is relatively simple and it’s very effective. We’ll just have different tab items depending on the size class.

If you want to make sure that tab selection is maintained, you can actually reuse the same tag for tabs that represent the same screen in your app.

And that’s it! With this setup you’re ready to support iPhone and iPad while using the new tab bar and sidebar hybrid view.

Building a stretchy header view with SwiftUI on iOS 18

In iOS 18, SwiftUI's ScrollView has gotten lots of love. We have several new features for ScrollView that give tons of control to us as developers. One of my favorite interactions with scroll views is when I can drag on a list an a header image animates along with it.

In UIKit we'd implement a UIScrollViewDelegate and read the content offset on scroll. In SwiftUI we could achieve the stretchy header effect with GeometryReader but that's never felt like a nice solution.

In iOS 18, it's possible to achieve a stretchy header with little to no workarounds by using the onScrollGeometryChange view modifier.

To implement this stretchy header I'm using the following set up:

struct StretchingHeaderView: View {
    @State private var offset: CGFloat = 0

    var body: some View {
        ZStack(alignment: .top) {
            Image(.photo)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(height: 300 + max(0, -offset))
                .clipped()
                .transformEffect(.init(translationX: 0, y: -(max(0, offset))))

            ScrollView {
                Rectangle()
                    .fill(Color.clear)
                    .frame(height: 300)

                Text("\(offset)")

                LazyVStack(alignment: .leading) {
                    ForEach(0..<100, id: \.self) { item in
                        Text("Item at \(item)")
                    }
                }
            }
            .onScrollGeometryChange(for: CGFloat.self, of: { geo in
                return geo.contentOffset.y + geo.contentInsets.top
            }, action: { new, old in
                offset = new
            })
        }
    }
}

We have an @State private var to keep track of the ScrollView's current content offset. I'm using a ZStack to layer the Image below the ScrollView. I've noticed that adding the Image to the ScrollView results in a pretty stuttery animation probably because we have elements changing size while the scroll view scrolls. Instead, we add a clear Rectangle to the ScrollView to push or content down by an appropriate amount.

To make our effect work, we need to increase the image's height by -offset so that the image increase when our scroll is negative. To prevent resizing the image when we're scrolling down in the list, we use the max operator.

.frame(height: 300 + max(0, -offset))

Next, we also need to offset the image when the user scrolls down in the list. Here's what makes that work:

.transformEffect(.init(translationX: 0, y: -(max(0, offset))))

When the offset is positive the user is scrolling downwards. We want to push our image up what that happens. When the offset is negative, we want to use 0 instead so we again use the max operator to make sure we don't offset our image in the wrong direction.

To make it all work, we need to apply the following view modifier to the scroll view:

.onScrollGeometryChange(for: CGFloat.self, of: { geo in
    return geo.contentOffset.y + geo.contentInsets.top
}, action: { new, old in
    offset = new
})

The onScrollGeometryChange view modifier allows us to specify which type of value we intend to calculate based on its geometry. In this case, we're calculating a CGFloat. This value can be whatever you want and should match the return type from the of closure that you pass next.

In our case, we need to take the scroll view's content offset on the y axis and increment that by the content inset's top. By doing this, we calculate the appropriate "zero" point for our effect.

The second closure is the action that we want to take. We'll receive the previous and the newly calculated value. For this effect, we want to set our offset variable to be the newly calculated scroll offset.

All this together creates a fun strechy and bouncy effect that's super responsive to the user's touch!

Modern logging with the OSLog framework in Swift

We all know that print is the most ubiquitous and useful debugging tool in a developer’s toolbox. Sure, we have breakpoints too but what’s the fun in that? Sprinkling some prints throughout our codebase to debug a problem is way more fun! And of course when we print more than we can handle we just add some useful prefixes to our messages and we’re good to go again.

What if i told that you can do way better with just a few lines of code. You can send your prints to more places, give them a priority, and more. Of course, we don’t call it printing anymore; we call it logging.

Logging is a key method to collecting important data for your app. From simple debugging strings to recording entire chains of events, having a good logging strategy can help you debug problems while you’re writing your app in Xcode and also once you’ve shipped your app to the store.

In this post, I’d like to show you how you can set up a Logger from the OSLog framework in your app, and how you can use it to log messages that can help you debug your app and gain insights about problems your users experience.

Setting up a Logger object

To set up a logger object all you need to do is import OSLog and create an instance of the Logger object:

import OSLog

let logger = Logger()

struct MyApp: App {
  // ... 
}

This approach creates a global logger object that you can use from anywhere within your app. Since I didn’t pass any custom configuration, the logger will just log messages using the default parameters.

That said, it’s wise to actually provide two pieces of configuration for your logger:

  • A subsystem
  • A category

By providing these two parameters, you can make filtering log messages a lot easier, and it allows you to group messages from multiple loggers together.

For example, I like to create a data model debugger that I can use to log data model related information. Here’s how I can create such a logger:

let modelLogger = Logger.init(
    subsystem: "com.myapp.models",
    category: "myapp.debugging"
)

Apple recommends that we name our subsystems using reverse-DNS notation. So for example, com.myapp.models for a subsystem that encompasses models within my app. You could create loggers for every module in your app and give each module its own subsystem for example. That way, you can easily figure out which module generated which log messages.

The second argument provided to my logger is a category. I can use this category to group related messaged together, even when they originated from different subsystems. Apple doesn’t provide any naming conventions for category so you can do whatever you want here.

It’s perfectly acceptable for a single app to have multiple loggers. You can create multiple loggers for a single subsystem for example so that you can provide different categories. Having narrowly scoped loggers in your apps with well-named categories and subsystems will greatly improve your debugging experience as we’ll see later on.

Once you’ve created an instance of your logger and found a nice place to hold on to it (I usually like to have it available as a global constant but you might want to inject it or wrap it in a class of your own) you can start sending your first log messages. Let’s see how that works.

Logging your first messages

When you log messages through your logger instance, these messages will end up in different places depending on which kind of log level you’re using. We’ll discuss log levels later so for now we’ll just use the simple log method to log our messages.

Let’s log a simple “Hello, world!” message in response to a button tap in SwiftUI:

Button("Hello, world") {
  modelLogger.log("Hello, world!")
}

Calling log on your Logging instance will cause a message to be printed in your Xcode console, just like it would with print…

However, because we’re using a Logger, we can get Xcode to show us more information.

Here’s an example of the kinds of information you can view in your console.

An example of a message logged with a Logger

Personally, I find the timestamp to be the most interesting aspect of this. Normally your print statements won’t show them and it can be hard to distinguish between things that happened a second or two apart and things that happen concurrently or in very rapid succession.

For comparison, here’s what the same string looks like when we print it using print

An example of a message logged with print

There’s no extra information so we have no clue of when exactly this statement was printed, by which subsystem, and what kind of debugging we were trying to do.

Xcode won’t show you all the information above by default though. You need to enable it through the metadata menu in the console area. The nice thing is, you don’t need to have done this before you started debugging so you can enable that whenever you’d like.

The metadata menu in Xcode's console area

Gaining so much insight into the information we’re logging is super valuable and can really make debugging so much easier. Especially with logging categories and subsystems it’ll be much easier to retrace where a log message came from without resorting to adding prefixes or emoji to your log messages.

If you want to filter all your log messages by subsystem or category, you can actually just search for your log message using the console’s search area.

Searching for a subsystem in the console

Notice how Xcode detects that I’m searching for a string that matches a known subsystem and it offers to either include or exclude subsystems matching a given string.

This allows you to easily drown out all your logging noise and see exactly what you’re interested in. You can have as many subsystems, categories, and loggers as you’d like in your app so I highly recommend to create loggers that are used for specific purposes and modules if you can. It’ll make debugging so much easier.

Accessing logs outside of Xcode

There are multiple ways for you to gain access to log messages even when Xcode isn’t running. My personal favorite is to use Console app.

Finding logs in the Console app

Through the Console app on your mac you can connect to your phone and see a live feed of all log messages that are being sent to the console. That includes messages that you’re sending from your own apps, as you can see here:

Console.app

The console provides plenty of filtering options to make sure you only see logs that are interesting to you. I’ve found the Console app logging to be invaluable while testing stuff that involves background up- and downloads where I would close my app, force it out of memory (and detach the debugger) so I could see whether all delegate methods are called at the right times with the expected values.

It’s also quite useful to be able to plug in a phone to your Mac, open Console, and browse your app’s logs. Within an office this has allowed me to do some rough debugging on other people’s devices without having to build directly to these devices from Xcode. Very fast, very useful.

Accessing logs in your app

If you know that you’d like to be able to receive logs from users so that you can debug issues with full access to your log messages, you can implement a log viewer in your app. To retrieve logs from the OSLog store, you can use the OSLogStore class to fetch your log messages.

For example, here’s what a simple view looks like that fetches all log messages that belong to subsystems that I’ve created for my app:

import Foundation
import OSLog
import SwiftUI

struct LogsViewer: View {
    let logs: [OSLogEntryLog]

    init() {
        let logStore = try! OSLogStore(scope: .currentProcessIdentifier)
        self.logs = try! logStore.getEntries().compactMap { entry in
            guard let logEntry = entry as? OSLogEntryLog,
                  logEntry.subsystem.starts(with: "com.donnywals") == true else {
                return nil
            }

            return logEntry
        }
    }

    var body: some View {
        List(logs, id: \.self) { log in
            VStack(alignment: .leading) {
                Text(log.composedMessage)
                HStack {
                    Text(log.subsystem)
                    Text(log.date, format: .dateTime)
                }.bold()
            }
        }
    }
}

It’s a pretty simple view but it does help me to obtain stored log messages rather easily. Adding a view like this to your app and expanding it with an option to export a JSON file that contains all your logs (based on your own Codable models) can make obtaining logs from your users a breeze.

Logging and privacy

Sometimes, you might want to log information that could be considered privacy sensitive in order to make debugging easier. This information might not be required for you to actually debug and profile your app. It’s a good idea to redact non-required personal information that you’re collecting when it’s being logged on user’s devices.

By default, when you insert variables into your strings these variables will be considered as data that should be redacted. Here’s an example:

 appLogger.log(level: .default, "Hello, world! \(accessToken)")

I’m logging an access token in this log message. When I profile my app with the debugger attached, everything I log will be printed as you would expect; I can see the access token.

However, when you disconnect the debugger, launch your app, and then view your logs in the Console app while you’re not running your app through Xcode, the log messages will look more like this:

Hello, world! <private>

The variable that you’ve added to your log is redacted to protect your user’s privacy. If you consider the information you’re inserting to be non-privacy sensitive information, you can mark the variable as public as follows:

 appLogger.log(level: .default, "Background status: \(newStatus, privacy: .public)")

In this case I want to be able to see the status of my background action handler so I need to mark this information as public.

Note that whether or not your log messages are recorded when the debugger isn’t attached depends on the log level you’re using. The default log level gets persisted and is available in Console app when you’re not debugging. However, the debug and info log levels are only shown when the debugger is attached.

Other log levels that are useful when you want to make sure you can see them even if the debugger isn’t attached are error and fault.

If you want to be able to track whether privacy sensitive information remains the same throughout your app, you can ask the logger to create a hash for the privacy sensitive value. This allows you to ensure data consistency without actually knowing the content of what’s being logged.

You can do this as follows:

 appLogger.log(level: .default, "Hello, world! \(accessToken, privacy: .private(mask: .hash))")

This helps you to debug data consistency issues without sacrificing your user’s privacy which is really nice.

In Summary

Being able to debug and profile your apps is essential to your app’s success. Logging is an invaluable tool that you can use while developing your app to replace your standard print calls and it scales beautifully to production situations where you need to be able to obtain collected logs from your user’s devices.

I highly recommend that you start experimenting with Logging today by replacing your print statements with debug level logging so that you’ll be able to apply better filtering and searching as well as stream logs in your macOS console.

Don’t forget that you can make multiple Logger objects for different parts of your app. Being able to filter by subsystem and category is extremely useful and makes debugging and tracing your logs so much easier.

@preconcurrency usage in swift explained

When you enable strict concurrency checks for your existing projects, it’s likely that Xcode will present loads of warnings and/or errors when you compile your project for the first time. In this post, I’d like to take a look at a specific kind of error that relates to code that you didn’t write.

The @preconcurrency declaration can be added to:

  • functions
  • types
  • protocols
  • imports

Let’s take a look at all of these areas to fully understand how @preconcurrency helps us enable strict concurrency checks even if we can’t update all of our dependencies just yet.

@preconcurrency imports

To be specific, Xcode will sometimes offer a message that reads a lot like this:

Add @preconcurrency to suppress Sendable-related warnings from module MyModule

This error tells us that we’re importing a module that doesn’t appear to completely adhere to modern concurrency rules just yet. Since this might not be a module that you own, Xcode offers you the ability to silence strict concurrency warnings and errors coming from this module.

You can do this by adding @preconcurrency to your import statement:

@preconcurrency import MyModule

By doing this, Xcode will know that any warnings related to types coming from MyModule should be suppressed.

If MyModule is not a module that you own, it makes a lot of sense to suppress warnings; you can’t fix them anyway.

Note that this won’t suppress warnings related to code from MyModule that is Sendable or up-to-date with modern concurrency. So if you see warnings related to concurrency on a module that you’ve marked with @preconurrency, you’ll want to fix those warnings because they’re correct.

Adding @preconcurrency to types, functions, and more

Alternatively, you might be working on a module that has adopted Swift Concurrency and you’ve fixed your warnings. If that’s the case, you might want to add @preconcurrency to some of your declarations to ensure that code that depends on your module doesn’t break.

Adopting Swift Concurrency will mean that your module’s ABI changes and that some older code might not be able to use your modules if that older code doesn’t also adopt Swift Concurrency.

If this is the situation you’re in, you might have updated some of your code from this:

public class CatalogViewModel {
  public private(set) var books: [Book] = []

  public init() {}

  func loadBooks() {
    // load books
  }
}

To this:

@MainActor
public final class CatalogViewModel {
  public private(set) var books: [Book] = []

  public init() {}

  public func loadBooks() {
    // load books
  }
}

If you have pre-concurrency code that uses this class, it might look a bit like this:

class TestClass {
  func run() {
    let obj = CatalogViewModel()
    obj.loadBooks()
  }
}

Unfortunately adding @MainActor to our class in the module makes it so that we can’t use our view model unless we dispatch to the main actor ourselves. The compiler will show an error that looks a bit like this:

Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context.
Call to main actor-isolated instance method 'loadBooks()' in a synchronous nonisolated context.

This tells us that in order to interact with CatalogViewModel, we’ll need to update our project to use the main actor. This will often snowball into more and more code updates which makes the changes in our module severely breaking.

We can apply @preconcurrency to our view model to allow code that hasn’t been updated to interact with our view model as if it was never main actor annotated:

@preconcurrency @MainActor 
public final class CatalogViewModel {
  public private(set) var books: [Book] = []

  public init() {}

  public func loadBooks() {
    // load books
  }
}

Note that the above only works for projects that do not enable strict concurrency checking

With the @preconcurrency annotation in place for our entire class, the compiler will strip the @MainActor annotation for projects that have their concurrency checking set to minimal. If you’re using strict concurrency checks, the compiler will still emit errors for not using CatalogViewModel with the main actor.

In Summary

With @preconcurrency, we can import old modules into new code and we can allow usage of new code in old projects. It’s a great way to start to incrementally adopt strict concurrency as the release of Swift 6 comes closer and closer.

Adding @preconcurrency to your imports is very useful when you’re importing modules that have not yet been updated for strict concurrency.

Adding @preconcurrency to declarations that you’ve annotated as @Sendable, @MainActor, or otherwise updated in a way that makes it impossible to use them in non-concurrent code can make a lot of sense for library authors.