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.

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.

Programmatic navigation in SwiftUI with NavigationPath and navigationDestination

One of the key features that was missing from SwiftUI when it first shipped was a good way to do programmatic navigation. There were some ways to handle this before iOS 16 introduced NavigationPath but it wasn’t very satisfying to use those APIs and they could be rather unreliable at times. To see an example, take a look at this post I wrote about handling deeplinks on iOS 14.

In this post, I’d like to revisit programmatic navigation through iOS 16’s NavigationPath API which is a huge leap forward in terms of developer experience and reliability at the same time.

In this post we’ll look at:

  • Understanding iOS 16’s navigation API
  • Navigating through a NavigationPath

Understanding iOS 16’s navigation API

On iOS 16, Apple introduced the NavigationStack view. This is a view that’s pretty much analogous to UIKit’s UINavigationController and it allows developers to implement stack-based navigation. This is the kind of navigation that you’ll actually find in most apps that allow you to navigate into items that are shown in a list for example.

A navigation view in iOS has a stack of views that it holds on to as a hierarchy of how the user got to where they currently are. For example, the root view might be a list, the next view might be a movie view and the next one might be a view where users can view the cast of a movie. Each view would exist on the stack and a user can navigate back one level by swiping from the edge of their screen.

I’m sure you’re familiar with the UX of this.

The stack of views that represents the navigation hierarchy wasn’t available to use until iOS 16. The main difference between UIKit’s UINavigationController and how NavigationStack manages its navigation is that in SwiftUI we can actually navigate based on models.

This means that we can map instances of, for example, a Movie model to a MovieView that can present a movie to the user.

Essentially this means that we can model a navigation hierarchy using model data rather than views.

Let’s take a look at an example of how we can set up a NavigationStack along with a detail page for a given model type. We won’t introduce a NavigationPath just yet. Behind the scenes our NavigationStack will manage its own path if we don’t provide one so we’ll just rely on that for now.

The code below defines a simple list view with NavigationLink views to enable navigation. Notice that the NavigationLink receives a value instead of a destination. Also, notice how we’re applying a navigationDestination view modifier to specify a destination view for our model.

struct ContentView: View {
  @State private var exercises: [Exercise] = Exercise.sample

  var body: some View {
    NavigationStack {
      ExercisesList(exercises: exercises)
        .navigationDestination(for: Exercise.self, destination: { exercise in
          ExerciseDetail(exercise: exercise)
        })
    }
  }
}

struct ExercisesList: View {
  let exercises: [Exercise]

  var body: some View {
    List(exercises) { exercise in
      NavigationLink(value: exercise, label: {
        ExerciseListItem(exercise: exercise)
      })
    }
    .navigationTitle("My exercises")
  }
}

What’s especially interesting here is where we apply the navigationDestination view modifier.

I chose to add it to my list. This means that any NavigationLink inside of my list with an instance of Exercise as its value will use the destination view that I provided as its view. This means that I can define my destination views all in one place which means that I can quickly reason about which view will be shown for a model.

If I were to define a second navigationDestination for the same model type on my List, that second destination would overwrite my first. This allows me to override the destination if needed so that each view can still explicitly define its own “exit views” but it’s not required. This is really powerful and allows for very flexible navigation setups.

At this point, we’re able to push new models onto our navigation stack’s navigation path using our navigation link and we’ve configured a destination view using the navigationDestination view modifier.

Now let’s set up a navigation path so we can start performing some programmatic navigation, shall we?

Navigating with a NavigationPath

A NavigationStack can be set up with a NavigationPath object which will allow you to gain control over the stack’s navigation hierarchy.

The simplest way to set up a NavigationPath is as follows:

struct ContentView: View {
  @State private var exercises: [Exercise] = Exercise.sample
  @State private var path = NavigationPath()

  var body: some View {
    NavigationStack(path: $path) {
      ExercisesList(exercises: exercises)
        .navigationDestination(for: Exercise.self, destination: { exercise in
          ExerciseDetail(exercise: exercise)
        })
    }
  }
}

With this code, we’re not yet doing anything to gain control of our navigation path. We’re just making an instance of NavigationPath and we pass a binding to NavigationStack. From now on, whenever we navigate to a new view, the model that’s used as a value will be added to the path we created.

Essentially, when a user taps on a NavigationLink, we take the model instance that was passed as a value and it’s added to the navigation path automatically.

We can pass any Hashable model as the value for a navigation destination and we can also mix models. So we could pass instances of Exercise, Int, String, and more to the same navigation path.

In fact, you normally don’t worry about which model types you pass. You just pass the model that you need to draw your destination view and you let the system handle everything else.

Let’s take a look at how we can replace our NavigationLink with a Button so we can manually append our model to the NavigationPath that we’ve created before.

We can create a binding to the NavigationPath and we pass it to the ExercisesList, allowing it to append new items to the path which will allow the NavigationStack to navigate to the destination for our model:

struct ContentView: View {
  @State private var exercises: [Exercise] = Exercise.sample
  @State private var path = NavigationPath()

  var body: some View {
    NavigationStack(path: $path) {
      // 1
      ExercisesList(exercises: exercises, path: $path)
        .navigationDestination(for: Exercise.self, destination: { exercise in
          ExerciseDetail(exercise: exercise)
        })
    }
  }
}

struct ExercisesList: View {
  let exercises: [Exercise]
  // 2
  @Binding var path: NavigationPath

  var body: some View {
    List(exercises) { exercise in
      Button(action: {
        // 3
        path.append(exercise)
      }, label: {
        ExerciseListItem(exercise: exercise)
      })
    }
    .navigationTitle("My exercises")
  }
}

Before I explain the code, let me say that I don’t think this is a good idea. The code was better with NavigationLink. That said, the point of this example is to demo putting items in a NavigationPath programmatically which we can do from a button handler.

First, we pass a binding to our navigation path to the list view. This means that now our NavigationStack and ExercisesList both have access to the exact same NavigationPath instance.

The ExercisesList was updated to take a binding to a NavigationPath, and we’ve swapped the NavigationLink out in favor of a Button. In the button handler, I call append with the Exercise model for the button on path. This will add the model to the path which will cause SwiftUI to navigate to the destination view for that model.

This is really cool!

In addition to appending elements to the path, we can actually remove items from the path too by calling remove on it.

We can even get the number of items on the path to implement a “pop to root” style function:

func popToRoot() {
  path.removeLast(path.count)
}

This function will remove all elements from the navigation stack’s path, only leaving its root to be displayed.

The API for NavigationPath is really flexible. You can even add multiple views in a single pass, resulting in the last added view becoming the top one and all others being part of the stack so the user sees them when they navigate back.

In Summary

With NavigationPath we’ve gained loads of power in terms of being able to navigate programmatically. By leveraging model-based navigation we can represent a navigation stack’s hierarchy as data rather than views, and we’re able to pass our NavigationPath around through bindings in order to allow views to append new models to the path.

Handling deeplinks and restoring navigation stacks with NavigationPath is loads better than it used to be pre iOS 16 and I’m sure that Apple will keep improving NavigationPath over time to make managing navigation through code better and better.

Turn off sidebar hiding on NavigationSplitView in SwiftUI

By default, a NavigationSplitView in SwiftUI will show users an option to toggle the visibility of the sidebar. If you want to prevent this button from showing up so that users will always have to see your sidebar, you can do this by applying the toolbar(removing:) view modifier to your split view's sidebar as follows:

NavigationSplitView(sidebar: {
  ExercisesList()
    .toolbar(removing: .sidebarToggle)
}, detail: {
  ExerciseDetail(exercise: exercises.first!)
})

The downside of doing this is that the button is hidden both in portrait and landscape modes. The result is that landscape users can no longer access your app's sidebar.

To fix this you can try applying the view modifier conditionally based on the device's orientation but that's not ideal; apps in landscape might also show the split view as a single column. I have yet to find a good, reliable solution to conditionally presenting and hiding the sidebar toggle.

One upside for users is that they can still summon the sidebar in portrait mode by swiping from the leading edge of the screen towards the middle. It's not perfect, but it's better than nothing I suppose.

How to decide between a Set and Array in Swift?

Collections are a key component in any programming language. We often refer to collections as Array or Set but there are several other kinds of collections in programming like String (often a collection of type Character) and ArraySlice (referring to a part of an array).

In this post, I’d like to explore two of the most common collection types; Set and Array. We’ll take a look at the key characteristics for each and we’ll explore use cases where we can use each.

We’ll cover the following topics:

  • Understanding Array’s key characteristics
  • Understanding Set’s key characteristics
  • Exploring performance considerations
  • Use cases for Set and Array

Understanding Array’s key characteristics

An Array in Swift is defined as follows:

let myList = ["one", "two", "three"]

If we fully write out the type for myList, we’d write let myList: Array<String>. That’s because arrays in Swift can only contain a homogeneous collection of objects. In other words, it can only contain objects of a single type. In this case that type is String.

We can have any kind of object in an Array, the only restriction is that your array must only contain objects that are all of the same type. In other words, we can’t have an array that contains both Int and String, but we can have an array that contains a custom enum:

enum MixedValue {
  case int(Int)
  case string(String)
}

let myList: [MixedValue] = [.int(1337), .string("Hello")]

Our array in this example only contains values of type MixedValue. Even though the associated values for my array are mixed, Swift will allow this because our array is still an array of MixedValue.

Items in an array are ordered. This means that items in an array will always be in the same order, no matter how many times you iterate over your array. For example, if you use a for loop to iterate your array thousands of times, the ordering of your elements won’t change.

You can reorder your array if you’d like by sorting it, and from that point on the new sorting will remain as the single ordering for your array.

Arrays can also contain duplicate values. This means that you can have multiple objects that are equal in the same array.

If we want to find an item in an array we can use the first(where:) function to iterate the array until we find what we’re looking for:

let myList: [Int] = [1337, 1338, 1339]

let item = myLIst.first(where: { $0 == 1340 })

The code above would iterate all items, not find a match based on my comparison and set item to nil.

There’s a lot more to know about working with arrays and collections in general, but to keep this post focused on the comparison between set and array, these are the key characteristics that I wanted to show you on array.

Arrays are meant to hold data that is ordered and this data doesn’t have to be unique

Understanding Set’s key characteristics

A Set in Swift holds a single type of object, just like Array does. For example, we can have a Set of strings like this:

let mySet: Set<String> = ["hello", "world"]

Notice how defining the set looked pretty much the same as defining an array which would have looked as follows in this specific case:

let myArray: Array<String> = ["hello", "world"]

Both sets and arrays can be initialized using array literal syntax.

One key difference between sets and arrays is that elements in a Set must be Hashable, and a Set only contains unique values.

This means that we can add items like String to a Set because String is Hashable. We can also add custom types to a Set as long as the type is Hashable.

Also note that I wrote earlier that items in a Set must be unique. Items in a Set are compared based on their hash value and when you add a second item with a hash value that’s already in your set the old item is removed and the new one is kept in the set instead.

If we want to find out whether an item in our Set exists we can use contains and pass the value we’re looking for:

let mySet: Set<String> = ["hello", "world"]
let hasValue = mySet.contains("hello")

If we want to find a specific item in our Set we can use the same first(where:) method that you saw earlier on Array. That’s because this method is part of the Collection protocol that both Array and Set conform to.

When you iterate over a set, the order of elements in the set is not guaranteed. This means that when you perform many iterations, you’ll notice that sometimes the order of items in your set gets shuffled. That’s expected.

A Set is meant to hold on to unique, unordered data that conforms to Hashable

If you require Set semantics but also need ordering, you could consider pulling in the swift-collections package and use its OrderedSet object which holds unique Hashable items but it also maintains an ordering. In a way, OrderedSet is an Array that enforces unique items and has O(1) lookup. Kind of the best of both worlds.

Performance considerations

It’s hard to give you a complete overview and advice for performance comparisons between Set and Array because there’s loads of things we can do with them.

The key aspect of performance that we can reason about is looking up items in either.

An array performs an item lookup in O(n) time. This means that in a worst case scenario we’ll need to look at every element in our array before we find our item. A Set on the other hand performs a lookup in O(1). This means that a set always takes the exact same amount of time to find the item you want to look for. This is orders of magnitude better than O(n), especially when you’re dealing with large data sets.

In Summary

In the end, the decision between Set and Array is one that I believe is made best based on semantics. Do you have a list of Hashable items that need to be unique in a collection without ordering; you’re thinking of a Set. Do you care about order? Or maybe you can’t make the items Hashable, then you’re probably thinking of an array.

There is of course the exception where you might want to have unique items that are Hashable while maintaining order, in which case you can choose to use an OrderedSet from swift-collections.

I would always base my decision on the above and not on things like performance unless I’m working on a performance-critical piece of code where I can measure a difference in performance between Set and Array.

Swift’s “if” and “switch” expressions explained

In Swift, we sometimes want to assign a property based on whether a certain condition is true or false, or maybe based on the value of an enum. To do this, we can either make a variable with a default value that we change after checking our condition or we define a let without a value so we can assign a value based on our conditions.

Alternatively, you might have used a ternary expression for simple assignments based on a conditional check.

Here’s what a ternary looks like:

let displayName = object.isManaged ? object.managedName : object.name

This code isn’t easy to read.

Here’s what it looks like if I had written the exact same logic using an if statement instead.

let displayName: String

if object.isManaged {
  displayName = object.managedName
} else {
  displayName = object.name
}

This code is much easier to read but it’s kind of weird that we have to declare our let without a value and then assign our value afterwards.

Enter Swift 5.9’s if and switch expressions

Starting in Swift 5.9 we have access to a new approach to writing the code above. We can have switch and if statements in our code that directly assign to a property. Before we dig deeper into the rules and limitations of this, let’s see how an if expression is used to refactor the code you just saw:

let displayName = if object.isManaged {
  object.managedName
} else {
  object.name
}

This code combines the best of both worlds. We have a concise and clear approach to assigning a value to our object. But we also got rid of some of the unneeded extra code which means that this is easier to read.

We can also use this syntax with switch statements:

let title = switch content.type {
  case .movie: object.movieTitle
  case .series: "S\(object.season) E\(object.episode)"
}

This is really powerful! It does have some limitations though.

When we’re using an if expression, we must provide an else too; not doing that results in a compiler error.

At the time of writing, we can only have a single line of code in our expressions. So we can’t have a multi-line if body for example. There is discussion about this on the Swift Forums so I’m sure we’ll be able to have multi-line expressions eventually but for the time being we’ll need to make sure our expressions are one liners.

What are enums in Swift?

Swift comes with types of objects that we can use to write type declarations. They all have their own distinct features, upsides, and downsides. In this post I’d like to zoom in on the enum type so you can get a sense of what enums are, and when they can be useful.

In this post we’ll cover the following topics:

  • Understanding the basics of enums
  • Knowing when an enum should be used
  • Avoiding enum overuse

Let's jump right in!

Understanding the basics of enums

In Swift, enums are values types that are declared using the enum keyword. Every possible value of the enum is called a "case". Enums can either be used as a list of cases that models specific (mutually exclusive) values or constants. For example, we could represent a list of operating systems for SwiftUI apps as follows:

enum OperatingSystem {
  case iOS, iPadOS, macOS, visionOS, tvOS, watchOS
}

If we were to pass an operating system to a function or assign it to a variable, that would look a little like this:

// assigning a variable
var os: OperatingSystem = .macOS

// or passing to a function
let binary = buildAppBinaryFor(.watchOS)

Since enums are first class citizens in Swift, they can conform to protocols, define functions, and even have computed properties. Note that you can’t add stored properties to an enum like you can to a struct.

Enums are incredibly useful when we need to represent a finite set of predefined states. With a enum, it’s impossible to create or use state that are not correct which can be incredibly useful. Imagine that you’d have to manually type out an animation type as a string in SwiftUI instead of passing .easeInOut and knowing that if your code compiles you passed a correct animation type. If you had to pass the string "easeInOut" the compiler wouldn’t be able to type check that you passed a valid value and you could be bitten by typos such as "aeseInOut" which isn’t ideal.

Even though you can’t leverage stored properties when using enums, you can associate values with enums and you can give them raw values.

An enum with raw values looks like this:

enum OperatingSystem: String {
  case iOS = "iOS"
  case iPadOS = "iPadOS"
  case macOS = "macOS"
  // etc...
}

In this case, my raw enum values match the enum case names directly. This means that instead of adding every possible value after the case name, I can specify that my enum uses a raw String value without modifying my cases:

enum OperatingSystem: String {
  case iOS, iPadOS, macOS, visionOS, tvOS, watchOS
}

Defining enums with raw values is incredibly useful when you want to create your enum values from external resources like when you’re decoding JSON. Having raw values allows you to create enum instances like this:

let os: OperatingSystem? = OperatingSystem(rawValue: "iOS")

Of course, when hardcoding the raw value it would be much better to just use .iOS directly but if the raw value comes from an external source we can try to make an enum with the value we received. If this works, we get an enum case back and if not, we get nil. This happens when we pass a raw value that’s not defined on the enum.

The last thing I’d like to show you before we talk about up and downside of enums is defining an enum with an associated value.

An example of such an enum is Swift’s built-in Result type:

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

This enum uses generics to allow developers to use custom values for Success and Failure but you can also define an enum without generics for associated values. For example:

enum Animation {
  case none
  case easeIn(duration: CGFloat)
  // ...
}

With this Animation enum we can pass different animation configurations that are all grouped as possible values under a single type. This is super convenient!

If you get to a point where you’d like to compare some value to an enum case that has an associated value, you’ll want to read up on if case let syntax using this post I have on that topic.

Now that you know a bit about what enums are, let’s discuss when it makes sense to use them.

Knowing When to Use an Enum

Enums are ideal for managing a set group of related values in a way that makes your code clearer and safer. Learning to recognize when an enum would make sense is a key skill that will truly help you to write better and safer code over time.

The list below can be used to help you figure out whether an enum fits your current use case:

  • Enums are perfect when a variable can only take a limited set of predetermined values. If you're dealing with options, states, or modes within your application that are finite and known, enums help enforce this constraint with the help of the compiler.
  • Enums are excellent for managing state in your applications. For instance, if you have a UI control with multiple states like active, inactive, disabled, etc., an enum ensures you handle all possible states while also preventing developers from passing impossible or incorrect states.
  • If you’re using constants that are closely related and should be grouped under a single type, enums make this organization clear and logical. For example, error types, configuration options, and other relatively short finite lists of values.

Over time you’ll grow more and more confident in your ability to know whether an enum makes sense for your use case. I think it’s important to understand that not everything can or should be represented as an enum.

For example, I think it’s wise to make sure that your enums are fairly small. An enum that holds dozens of cases is probably too large and you’re no longer grouping closely related values together. It could make sense to refactor your large list of cases into a smaller list that uses associated values to group sub-values together.

It’s also a good idea to keep a close eye on how Apple uses enums in Swift and SwiftUI themselves. Using enums for similar scenarios should give you some confidence that you’re choosing the right tool for the job.

And when in doubt, refer to the list above and trust your instincts; they might be better than you think.