Streaming is available in most browsers,
and in the Developer app.
-
What’s new in Xcode 16
Discover the latest productivity and performance improvements in Xcode 16. Learn about enhancements to code completion, diagnostics, and Xcode Previews. Find out more about updates in builds and explore improvements in debugging and Instruments.
Chapters
- 0:00 - Introduction
- 0:29 - Updates in editing
- 0:33 - Code completion
- 1:01 - Adopting Swift 6 data-race safety guarantees
- 2:49 - Improvements to Previews
- 6:22 - Updates in builds
- 6:26 - Explicit modules
- 7:15 - Package resolution
- 7:15 - Package resolution
- 8:30 - What's new in debugging
- 8:35 - Build process and debugging
- 9:07 - Thread performance checker
- 9:23 - The organizer
- 12:16 - The RealityKit debugger
- 12:55 - Meet Swift Testing
- 18:42 - What's new in Instruments
- 19:44 - Meet the flame graph
- 21:38 - Wrap up
Resources
Related Videos
WWDC24
-
Download
Hi, I’m Daisy. Along with my colleague, Jake, we’ll show you some of what’s new in Xcode 16. We’re so excited, so let’s jump right in. At every phase of app development, Xcode is there with you. We’ll look at what’s new in Xcode 16 as you edit Swift code, run builds, debug amd test your code and maximize its performance. Let's start with three things that are new when editing code.
First, code completion provides more thorough code suggestions thanks to an on-device coding models specifically trained for Swift and Apple SDKs.
It uses the surrounding code context like function names and comments to move through your ideas faster.
This is all made possible by Apple Silicon and is available when running Xcode 16 on macOS Sequoia. Now let’s talk about Swift. Swift 6 brings a new language mode that provides concurrency safety guarantees. It turns data races normally only experienced at runtime into compile-time issues. This is a great way to improve the correctness and safety of your code. To take advantage of these guarantees, you’ll want to adopt the Swift 6 language mode. But you can start today in Xcode 16 by incrementally enabling warnings for each upcoming language feature. Let me show you how to do that. I’m working on an app named BOTanist where robots show off their planting skills. I want to prepare this app for Swift 6. So I’ll I start by navigating to the "Swift Compiler - Upcoming Features" section in the Build Settings. Here, I can enable upcoming compiler features one at a time. I'll start by setting "Isolated Global Variables” to Yes.
When I build, Xcode displays a new warning in the issues navigator.
Letting me know that logger, which is a global variable, is not concurrency safe.
Clicking on the issue, jumps right to where logger is defined.
I know logger doesn’t need to be mutated so I can change it from var to let. Making it safe from any data races.
Adopting upcoming features allows you to catch and address potential issues before switching to Swift 6. Where these warnings will appear as errors.
And when ready, you can set the Swift Language version in the build settings.
For more on this check out “Migrate your app to Swift 6”.
Xcode 16 also has some great improvements for Previews. Previews allow you to quickly iterate on your UI to help you create great user interfaces. There are two new APIs that make them simpler to write, more reusable, and better integrated with your model.
The first is the Previewable macro. Previewable can be attached to property wrappers like State, allowing you to use them directly within a preview block. Writing wrapper views is no longer necessary. Let me show you an example in our app.
Here’s RobotFaceSelectorView. It takes a Binding to a robot face.
When I create a preview, I can define my state right in the preview's body and attach the Previewable macro. This will tell the preview system to create a wrapper view behind the scenes, so I don’t have to.
Then I can add my view.
Which lets me see my UI live with just a couple of lines! The second new API is PreviewModifier. PreviewModifier makes it easier to share environment or data for previews. This not only reduces duplicate code, but it also enables the preview system to cache the data. I’ll switch to another view for this. RobotNameSelectorView requires a RobotNamer to work.
This type asynchronously retrieves possible robot names from the server. But hitting the server over and over while building my previews, isn’t necessary. I can use PreviewModifier to create a RobotNamer for all my previews without hitting our external server.
To begin, I’ll define a type that conforms to PreviewModifier. This protocol has two requirements. First, I use makeSharedContext to load and store the data. This method is async and throwing, so I can load data asynchronously and handle errors. In this case, I create a RobotNamer with names from a local file so I don’t hit our external server.
What’s key is that this method is only called once for all modifiers of the same type as the preview system caches this data for me.
The second requirement is the body method. The body method lets me wrap the preview with the shared context. In my conformance, I’ll pass RobotNamer using the environment modifier.
With my modifier defined, I just need to provide it to my preview using a trait. But given I'm going to use this modifier a lot, I like to define an extension on PreviewTrait to reduce the code at each call site. All that’s left is to create my preview.
This particular case is relatively simple. But PreviewModifier is especially helpful when you need to share data between previews, like when using SwiftData’s ModelContainer.
Beyond the API, this year is a huge leap forward for the performance and underpinnings of previews. Enabled by a new execution engine, previews are faster than ever! Powered by advances in the compiler, the build system, and operating systems, previews uses the same build products for your project and reassembles your program on the fly, no longer needing to produce a separate copy. Previews is one improvement with builds this year. To tell you about others, I'll hand it over to Jake. Thanks, Daisy. Let's rock and roll forward to builds. Xcode 16 supercharges your builds with Explicit Modules. This feature provides improved parallelism, better diagnostics, and faster debugging, all without changing a single line of code.
That's unreal! How do you get it? Well, for C and Objective-C, explicit modules are on by default. For Swift, you'll need to opt-in. Let's give that a try. I'll hop over to build settings. Enable Explicitly Built Modules.
And kick off a build.
Hmm. By the way, with improvements to Swift package integration in Xcode 16, I was able to queue up that build without having to wait for package resolution to finish first. I like that! So what's actually happening now? With explicit modules, Xcode splits up the processing of each compilation unit into three separate phases: scanning, building modules, and finally building the original code. The first two of these phases are now surfaced in the build log as "Scan dependencies" and "Compile Clang module" or "Compile Swift module", commands. Previously, these operations were performed implicitly as part of compiling your source files. Now, you get a more detailed breakdown of the build, better parallelism, and clearer error messages if the build fails due to a module issue.
Explicit modules are also reflected in the build timeline. This makes it easier to see where time is being spent during the build process, and helps you optimize your builds. To learn more, check out: "Demystify explicitly built modules." All right! Now that we've written and built some code, it's time to debug it.
But first, let me tell you about a couple ways the build process feeds into debugging improvements: Explicit modules make debugging faster, since lldb can re-use the build outputs when evaluating expressions, and DWARF5 is now the default debug symbol format when building against a deployment target of macOS Sequoia or iOS 18. With DWARF5, dSYM bundles are smaller, and symbol lookups are faster.
When running in Xcode 16, the thread performance checker does even more. In addition to finding main thread hangs and priority inversions, it will now show excessive disk write and slow app launch diagnostics to help you improve your app's performance.
In the Organizer, this comes with a new category of diagnostic logs for app launches. If your app takes a long time to launch on customer devices, Xcode will show you the slowest code path signatures, so you can prioritize fixing issues with the largest impact.
And if you're familiar with disk write diagnostics, you'll love this trendy new update. Now in the organizer, you can see how the impact of issues has changed across different versions of your app. When you go to the disk writes view you can see that there are up arrows next to some of the signatures. These arrows are a great place to start, helping you prioritize issues with the greatest impact to your users.
While debugging your code, the Thread Performance Checker will surface the worst of these as runtime issues, pinpointing the exact line of code to focus on, even if the issue isn't reproducing for you locally. In this example it looks like the Thread Performance Checker flagged an issue that's impacting our customers. It says we shouldn't be loading this video asset on the main thread. I'm going to open up Xcode and set a breakpoint so I can track this down. I'll go ahead and open the file with the issue and click in the gutter to set a breakpoint, and run.
OK, it hit the breakpoint. To make it easier to trace where this call is coming from, I'm going to enable the Unified Backtrace View from the Debug Bar.
This new visualization allows me to follow the call stack and view the surrounding code in each frame simply by scrolling down. I can even hover over variables to see their values.
Oh, here in the View body method there's an "await" calling into that code.
We've traced the call. It's coming from in-side the app! It looks like this video player initialization function is asynchronous and it's being invoked from a SwiftUI task modifier but it's inheriting the @MainActor context from the enclosing SwiftUI view and doing I/O on the main thread! I think I'll fix this issue by marking it nonisolated.
That ought to do it. Thanks to the Unified Backtrace View, along with the Thread Performance Checker and new Organizer reporting, Xcode helps you focus on what matters most. Spatial development gets a whole lot easier too, with the new RealityKit debugger. Now with the click of a button, you can capture a snapshot of your running app's entity hierarchy, and explore it in 3D, right inside of Xcode. This debugger lets you view entities and their components, and inspect both built-in and custom properties. To learn more about the RealityKit debugger, or the existing debuggers, check out: "Break into the RealityKit debugger", and "Run, break, and inspect: Explore effective debugging in LLDB." Now, back to Daisy to test things out! Awesome debugging Jake! Just like debugging helps you solve problems while in the middle of development, testing is a great way to catch issues and regressions in future iterations. Swift Testing is a new framework that leverages Swift language features to make expressing tests more powerful and concise. And these tests work right along side your existing XCTests. Let me show you an example of Swift Testing.
The team and I are adding a new feature to BOTanist. Robots are getting new planting styles and animation states! But I haven’t tested them all. So let’s add new tests. To start, I’ll create a new file in the test folder and name it PlantTests. To create a test, first I’ll import the Testing framework and our app, then write a function called plantingRoses. I can name this function whatever I want and to benefit from everything Swift Testing provides, all I need to do is add the Test macro.
As soon as I added the test macro, Xcode knows this is a test and displays it in the navigator.
In this test, I want to verify that a roses’s default planting style is grafting. The first thing I will do is create a Plant of type rose.
Next, I’ll create a second Plant of type rose but give it grafting as its explicit style. Nice, that’s exactly what I want! And finally, I’ll verify they are equal using the expect macro. This macro takes any boolean expression. You can use it to verify strings, floats, or other types.
And now let’s run the test.
Oh-oh. Unfortunately the test failed. The test is saying that the two roses are not the same. But based on the error description, they sure look the same Both display the same emoji! However, I don’t know what the other properties look like. With Swift Testing, I can view additional information about each value by clicking "Show Details" in the error so I can inspect more closely. Ahh, the planting styles are different. Good thing I wrote a test to check the default value! This is a quick fix.
.Seedling should be .graft.
Let’s run the test again to verify that it passes. I can do so using quick actions. To bring up quick actions I use the shortcut command shift A.
Then I’ll type “test again” to rerun this specific test.
Super, it passed! The test macro not only marks functions as tests. It also accepts a variety of traits that can add information or change their behavior. For example, you can provide a display name or arguments to a test function.
This test checks my robot animation state machine. I want to make sure these states can transition to celebrate. Instead of writing multiple test functions, we provide the test macro the list of states. And have a single test function that takes a state as its parameter. Now, let’s run this parameterized test.
In Swift Testing each provided argument runs in parallel as its own test case. The navigator displays each case individually, letting me know which variations failed. And it looks like the plant animation state did. I probably forgot to include .celebrate as a valid state transition. Let’s fix that.
Now, I can rerun just the failing test case by clicking on the button next to it.
Excellent! All test cases pass! Another benefit of Swift Testing in Xcode is tag-based organization. Swift Testing lets me create tags to group tests across different suites. I’d really like to do that with this feature so I know which tests relate to it. To start, I’ll extend the Tag type to include my custom tag, planting. Then I’ll add the tag to the Test macro of the tests I want to label. I'll add it to this test.
And also to plantingRoses.
With the tags applied, the navigator's tag view shows them grouped together.
If I want to run all tests in the tag, I can click the button next to it. Tags can also be used to include and exclude tests from test plans.
I’ll go to the test plan by choosing "Edit Test Plan" at the top of the navigator. The new planting and animation feature is still under development. And I don’t want to cause any instability in our CI because of these tests. So instead, I'll add the tag for this feature to the excluded tags list until I'm ready to enable it.
To learn more about Swift Testing, checkout “Meet Swift Testing” and “Go further with Swift Testing”.
We've debugged and tested our code. The tests run fast, but the app doesn’t. I’ll let Jake take a look. Thanks, Daisy. So we just got done verifying the functionality of our app using testing. But when I run it, I’ve noticed the launch takes a lot more time than I initially expected.
The best tool to diagnose your performance problems is Instruments, which you can access from the Profile action right from Xcode. I have a trace of our app's launch that Daisy recorded using the “Time Profiler” Instrument. This Instrument visualizes CPU usage in our code and allows us to measure how long it's taking to run. Let's take a look at that now.
Whoa! That's a lot of CPU usage. And a really long hang during the initial launch. Let’s see what we can do about it. To understand why it’s happening, I'll start by setting the inspection range to the hang interval.
Now that I’m only looking at a portion of the trace, let’s analyze the data. To help narrow down the problem, I'll use Instruments 16's new Flame Graph, which I can activate from the jump bar. Flame Graph is an excellent high level overview of the trace execution, which allows us to spot issues at a glance. Execution intervals are weighted by the percentage of time they took in the trace and the intervals are sorted left to right. This means that the left part of the graph will always visualize code that executed the most. Let’s take a look at what my program is executing.
It looks like this load function is nearly all of the execution time.
And it's being called from a SwiftUI View body. That doesn't seem right. I'll right click on this frame and select “Reveal in Xcode”, which will take me right to the code.
Ah, I think I see the problem. We're loading a bunch of assets serially in a loop, and we're doing it on the main thread. Not too good. To fix this, I'll parallelize the loading using a task group, which will also move execution of this work to the background where it belongs.
And now I'll rebuild, and take a new Time Profile in Instruments, to see if that helped.
Nice! The launch was blazing fast. I can see in the flame graph that the asset loading is now distributed across multiple background threads, and no longer blocking the main thread.
Flame graphs work for every Instrument that uses call-trees. This visual way of seeing your callstacks is a great way of finding issues, and another reason to profile your app regularly.
Those are just some of the new and exciting features of Xcode 16. To learn more about what you saw or improvements like Faster UI previews, the Hardened C++ runtime, improved localization workflows, and more, check out our release notes on developer.apple.com. Or better yet: download Xcode 16 and give it a try! Thanks for watching, and have a great WWDC!
-
-
3:37 - Inline State within Preview
#Preview { @Previewable @State var currentFace = RobotFace.heart }
-
3:45 - View using Inline State
RobotFaceSelectorView(currentFace: $currentFace)
-
3:53 - Complete Preview using Previewable
#Preview { @Previewable @State var currentFace = RobotFace.heart RobotFaceSelectorView(currentFace: $currentFace) }
-
4:40 - Type Conforming to PreviewModifier
struct SampleRobotNamer: PreviewModifier { typealias Context = RobotNamer static func makeSharedContext() async throws -> Context { let url = URL(fileURLWithPath: "/tmp/local_names.txt") return try await RobotNamer(url: url) } func body(content: Content, context: Context) -> some View { content.environment(context) } }
-
5:29 - Extension on PreviewTrait
extension PreviewTrait where T == Preview.ViewTraits { @MainActor static var sampleNamer: Self = .modifier(SampleRobotNamer()) }
-
5:38 - Preview using created PreviewModifier
#Preview(traits: .sampleNamer) { RobotNameSelectorView() }
-
10:26 - AVPlayer Creation
struct BOTanistAVPlayer { func player(url: URL) throws -> AVPlayer { let player = AVPlayer(url: url) return player } }
-
11:28 - AVPlayer Call Site
self.player = try? await robotVideoAVPlayer()
-
11:57 - AVPlayer Initialization
private nonisolated func robotVideoAVPlayer() async throws -> AVPlayer? { guard let url = Bundle.main.url(forResource: RobotVideo.resource, withExtension: RobotVideo.ext) else { throw BOTanistAppError.videoNotFound(forResource: RobotVideo.resource, withExtension: RobotVideo.ext) } let avPlayer = BOTanistAVPlayer() let player = try avPlayer.player(url: url) return player }
-
13:42 - Initial Test Scaffolding
import Testing @testable import BOTanist // When using the default init Plant(type:) make sure the planting style is graft @Test func plantingRoses() { // First create the two Plant structs // Verify with #expect }
-
14:36 - Complete Test
import Testing @testable import BOTanist // When using the default init Plant(type:) make sure the planting style is graft @Test func plantingRoses() { // First create the two Plant structs let plant = Plant(type: .rose) let expected = Plant(type: .rose, style: .graft) // Verify with #expect #expect(plant == expected) }
-
17:35 - Custom Tag
extension Tag { @Tag static var planting: Self }
-
17:42 - Tag Usage in @Test
.tags(.planting)
-
20:37 - Slow Asset Loading
for asset in allAssets { asset.load() }
-
20:54 - Fast Asset Loading
await withDiscardingTaskGroup { group in for asset in allAssets { group.addTask { asset.load() } } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.