Build your apps faster in Xcode 10. Learn how to structure your projects and tweak your code to take full advantage of all processor cores. Whether you've made a few small code changes you want to give a try, or you're building your full app for release, these techniques will cut the time it takes to build a running app.
Hello, and good morning everybody. I'm glad you could all make it out this morning. My name is David Owens. And I'm an engineer on the Xcode team. And today, with my colleague Jordan Rose, who's an engineer on the Swift team, we're going to be talking to you about building faster in Xcode.
Now, depending on your projects, their configurations, and their complexities, there's going to be a different set of opportunities that you're going to be able to take to improve, or in some cases, significantly improve the way in which your builds perform.
So today, when we talk about building faster in Xcode, we're going to be looking at it in two different perspectives. The first is going to be around increasing your overall build efficiency. And the second, it'll be about reducing the amount of work that you do on your builds, and especially your incremental builds.
Now, I'm going to be walking you through some of the project-level items, including how to parallelize your build process. How to declare and configure your run script phases.
And I'll be walking you through some new functionality in Xcode 10 around measuring your build times. Now, Jordan is going to be walking us through some of the source-level improvements that we can make to our projects, including understanding our dependence using Swift.
Dealing with complex expressions in Swift.
And how to limit your Objective-C to Swift interfaces.
So let's talk about parallelizing your build. Now, Xcode configures your projects through the use of targets.
And targets specify the output or the product that you would like to build. Some examples are iOS app, framework, and unit tests.
Now, there's another piece of information, that's the dependency between these targets. And Xcode provides us two ways to define our dependencies. There's an explicit means, which we do through the target dependencies phase. And there's an implicit means, which is primarily done through the linked binary with libraries phase. And we'll be taking a look at those more in depth in just a few moments.
Now, throughout this section I want to use a sample project to ground our discussion. And so we're going to take a look at a dependency graph for that project.
Now, a dependency graph is simply a listing of all of the targets. And this case we're going to have five targets that we're going to be building.
And it has that dependency information between those targets.
And based on these two pieces of information Xcode can derive our build order. Now, let's take a look at what this looks like on a timeline graph. So as we can see, each of these targets are building in in order, sequentially. And they each have to wait until the previous target is done building. Now, there's nothing wrong with this build timeline, per se. But it does represent a waste of potential hardware utilization, especially if you have a multi-core or a mini-core machine like an iMac Pro.
And what that means to you is a waste of time as a developer.
So instead, we want to move to something that looks more like this. Now, there's a couple things -- or a few things I want to note about this. First, the amount of work that we actually did in building our project, it did not change.
However, the time to build did decrease. And in this case, it actually decreased by a fairly significant margin. And we were able to decrease our build time by making better utilization of the hardware that we have available to us. So, if parallelization -- or parallelization is such a good thing, why don't we just create a build graph that looks like this? We just build everything at once up front in our build timeline.
Well, in the best case, you're going to deterministic build errors. And this because that dependency information is actually a vital part of your project configuration. And when it's set up like this, you're trying to build, for example, your game target before you've built your dependencies. So, this is not a good state to be in.
So, how do we get there? How do we get from the long, serialized build timeline to the better parallelized build time? Well first, we need to make sure that Xcode is actually set up and configured to allow our targets to be built in parallel. And we do that through the Scheme Editor.
You can get to the Scheme Editor by opening the Scheme Chooser and selecting Edit Scheme. And specifically, you'll need to look at the Build Action.
And in there, the Build Options. Now, there are two listed here. The first is Parallelize Build. And the second is Find Implicit Dependencies.
You'll want to check Parallelize Build. This will allow Xcode to use the dependency information across your targets so that it can attempt to build your targets in parallel. So let's look at how your dependencies are actually configured within Xcode.
This is done through the Build Phase Editor. And you can get to the Build Phase Editor by going to your Project Navigator and selecting your project. In this case we're looking at the Game Target -- or the Game Project. Next, you want to click on Build Phases.
So, let's take a look at the Game Target. This is the Build Phase Editor for our Game Target. And we're going to look at how its dependencies are configured. And I want to call your attention first to the Link Binary with Libraries phase. Now, this is the phase of your build process where you define all of the items that you would like to link with your target.
In this case I have two items. I have Physics and Utilities. Now, these are targets within our project and our workspace. So Xcode can create an implicit dependency on those targets.
If you're using other linking features such as Autolink or the other LD Build Flags build setting, those are not going to be made available to you here implicitly. So you either have to make an explicit dependency in this build phase, or in the Target Dependencies build phase. So you can see here that we have another item here called Shaders. And Shaders is something that is not used at link time, but instead it's used by another build phase within our current target. So it's important that we let Xcode know that this is a dependency and that we need to wait for the Shaders to finish its compilation and build before we can actually build the current target we're on. Now, this target actually exists in a different project. And if you would like to make a reference to that project you can do so by dragging the project as a child of the current project you're working in. So I want to walk through the rest of the dependencies of our project.
Our Shaders target has a dependency on our Utilities target.
Our Utilities target has a dependency on our Physics target.
And lastly, our Tests have a dependency on our Shaders and our Utilities targets.
So now that we have an understanding of the configuration of our project, let's look at the steps that are necessary to turn this serialized build process into one that we can build more in parallel. And we're going to start by looking at our test dependencies.
Now, I've broken down the dependencies into three different classes of dependencies that I want to talk about. This first dependency is the dependency that I call the "Do Everything" dependency. Right? It should a little bit clear here that this test is testing way too many components. It's testing our Game. It's testing our Shaders. And it's testing our Utilities.
Now in this case, it'd be better to simply break up our tests so that it's testing each individual component. And we're going to see by doing this, we're going to introduce our first bit of parallelism into our build process.
Right? Our test target, which was built in all three can now build just the component that it's looking for in the Game tests.
And then our Shaders tests and our Utilities tests can be moved to be built in parallel with our other targets. And they can be built as soon as their respective components are done, Shaders and Utilities.
Now, the next type of dependency I want to look at is the dependency that I call the "Nosy Neighbors." This is the dependency that needs to exist. It's look at another target. But it only needs a little bit of that target.
But instead it's getting everything that's in that target.
So if we look at our game, it has a dependency on Physics, Shaders, and Utilities. This is actually okay.
The suspect one is the dependency between our Shaders target and our Utilities target.
Now, our Shaders target produces a meta library, which is essentially just a bundle of GPU code that's going to run on our graphics card.
And our Utilities target just produces a normal frame, which is just CPU code. So there's already a little bit of a suspect dependency here. When we dig into it we see that the utilities target actually has a build phase in it that's generating some information that's used by both targets. Which is totally fine. It's just that Shaders doesn't need anything else from the Utilities target. So it's best to break out that into its own target.
And we're going to see that this small incremental change actually has a large and significant impact on our overall build timeline.
So the new green box that just moved in is our new code target. So we were able to shrink our utilities target down because we moved that work into Code Gen.
And since Code Gen has no other dependencies, it can move to the very front of our build process. It can also be built in parallel with our Physics target, which is the red box on the bottom.
And lastly, because Shaders no longer depends on Utilities, it doesn't have to wait for both Utilities and the Physics target to be built. And instead it can be built as soon as that Code Gen target is done. Now, the last dependency I want to talk to you about are the ones that I call the "Forgotten Ones." Throughout the evolution or the lifecycle of our products and our code, we tend to move code around and delete things. And we get things like dead code. Well, we get the same thing that happens with our dependencies. Sometimes we simply forget to clean them up.
And so in these cases, it's actually safe to just remove that dependency.
And this last change tightens up or build graph even further by allowing the Utilities target to be built right after the Code Gen target instead of having to wait for all of the Physics target to be done. Now, previously in Xcode, when you built targets that have a dependency on another target, you have to wait for the dependent target to finish its entire build process. Well, we have a new feature in Xcode 10 that allows us to introduce some parallelism into your build for free. And we do this by starting the compilation of your target as soon as the build phases that go into its dependencies that satisfy our compilation are complete.
So things like linking can now be done in parallel.
Now, if you have run script phases, this is one of those build phases that your target is going to have to wait on to finish in order before it can start taking advantage of some of these new parallelization benefits. So let's talk about run script phases.
Run script phases allow you to customize your build process to meet your needs. And so with this flexibility comes some responsibility for you as a developer as well. And so we want to walk you through the configuration process and make sure that you have your run script phases set up and configured well so that your builds behave.
Now, this is your Script Phase Editor. It can also be found within your Build Phase Editor. Now I want to call your attention to first the script body here. You can either put your entire script contents here. Or you can do what I've done and reference another script that's within your project.
Now, throughout the entirety of your run script phase, there are a set of build settings that are available to you. And I'm making use of one of those right now, which is the source group.
This gives you a convenient way to not have to provide absolute paths or try to do some relative path hacks to get your stuff to work.
The next section are you input files. Now, these are very important for your run script phase. As this is one of the key pieces of information that the Xcode Build System will use to determine if your run scripts should actually run or not. So this should include any file that your run script phase, the script content, is actually going to read or look at during its process.
Now, some of you may have a lot of inputs into your run script phase. And so this task might seem a little bit daunting. And so new in Xcode 10 we have the ability for you -- or we've provided you the ability to maintain this list in an external file, something we call a File List.
Now, a file list is a simple text file that has all of its inputs listed on its own line.
You get access to all of the same build settings that were available to you throughout the context of your run script phase.
The important thing to note here, though, is that these files cannot be modified or generated throughout your build process. They are read when your build process starts. And all that information is used then. Next I want to talk to you about your output files. Your output files are another key piece of information that's used throughout your build process. And Xcode will use this information to determine if your run script phase actually needs to run. And of course, we have support for output files -- or file lists for your output files as well.
So I want to recap for you when your run script phase is actually run.
If you have no input files declared, the Xcode build system will need to run to your run script phase on every single build. So this is one of key reasons it's important to declare your inputs.
Now if any of your input files change, including the contents of your file lists or any of the inputs that the file lists point to, Xcode will know that it needs to rerun your run script phase for you. And finally, if any of your output files are missing, the Xcode build system will run your run script phase to give you the opportunity to generate those missing output files.
Now also new in Xcode 10, we have documentation for the run script phases.
So, it goes through more detail of what I just explained and it tells you about all of the additional build settings that are made available to you, including how you can use those file lists within your own script content.
Now, when setting up your run script phases and declaring all of these new dependencies, and including when you modify the dependencies in your targets, you may run into a dependency cycle. And a dependency cycle is simply an interdependency graph somewhere there is a loop that's created.
Well, new in Xcode 10 we have better diagnostics to detect these cycles and will give you an error, including the ability for you to expand this box to get all of the inputs that the Xcode build system knows that went into creating the cycle.
So, cycles are bad for a couple of reasons. One, they represent a configuration issue within your project.
And two, they can be the source of spurious rebuilds within your project. Or of getting out-of-date information in your build process.
So we also have updated health topics on dependency cycles, including some specific sections that we call out of the most likely dependency cycles that you run into, and ways that you can fix those. So, the last thing I want to talk to you about today, is measuring your build time. We have two new pieces of functionality in Xcode 10 for this.
The first is we have introduced in-line task times to give you the duration that each of your tasks has taken to run. Now, I want to point out something about your build logs. There's a filter bar across the top.
And specifically, the All the Recent filters.
When you have "All" selected, it's going to show you all of the tasks that went into creating your entire final product outputs.
Which is usually not what you want to look at. What you want to look at, especially when you're trying to diagnose issues in your incremental builds, is the Recent tab.
That's going to show you all of the build paths that went into the previous build operation.
Now, another new feature in Xcode 10 is a timing summary. And you can get to this timing summary by going to the Product menu, selecting Perform with Action, and Build with Timing Summary.
When you do that, you're going to get a new log section out at the end of your build log. And if we focus in, you're going to see that it's going to give you an aggregate timing of all of the tasks that went into your last build operation.
So this is another important reason to look at the Recent filter tab. And there's one specifically I want to point to, the Phase Script Execution.
So, you can see in our last build that we just did, we had a shell script that ran. There was only one of them. It says one task. And it's taken 5 seconds.
If you're seeing these on every single one of your incremental builds, this is a good indication that you have something misconfigured in your run script phase. And that's something you might want to address to help decrease your overall build times.
Now, this build timing summary is also available to you from the Command line by passing in the Show Build Timing Summary flag.
And so now I want to bring up Jordan, who's going to talk to you about some of the source-level improvements that you can make to your project.
All right. So we've gone over a bunch of ways where you can improve your Xcode projects just by doing one small change. And before we get to the source-level and file-level topics, I want to talk about one more that's new in Xcode 10. And it's a particular workaround that we know that some of you have been using on your projects to make them build faster if they have a lot of Swift files.
You've already heard about this. It's the Whole Module setting being used in a debug configuration.
So, in previous versions of Xcode, for some projects, turning on the Whole Module Compilation mode, even for debug builds, produced a faster overall build than when used in Default Incremental Modes.
And, this did improve build times because it was able to share -- Swift's compiler was able to share work across files in a way that the Incremental Mode was not. But it also meant that you were giving up your incremental builds and would rebuild the entire target's worth of Swift files every time.
So in Xcode 10 we've improved the incremental build to have some of that same work sharing across files. So you should no longer need to use Whole Module mode to get good build times.
So, if you've done this in your project, then you should go back to your Build Settings Editor and select the debug configuration under the Compilation Mode build setting and hit Delete.
That will get it back to Xcode's default setting of an incremental build. I'm not going to talk much more about this because you already heard about it. We mentioned it in the "What's New in Swift" talk on Tuesday. And if you do want to know more, we'll cover this and other topics about your build in greater depth in tomorrow's session "Behind the Scenes of the Xcode Build Process." So we have a lot of topics that we're trying to get through today. And David's already covered half of them. I'm going to talk about the remaining three dealing with complex expressions at the top of that list.
And the reason for this is because it's the best one that exemplifies a key takeaway for both of our sections.
When a build is taking a long time, there's often a key piece of information that you can provide to Xcode to improve the situation.
And so we're going to look at that first in the context of complex Swift expressions. So here's an example of some code from my latest project.
And the problem with this struct is that I use it all over the place.
And it's perfectly fine to have a struct. It's perfectly fine to have a struct with a property. And it's fine to have a struct with a property with an inferred type. But the expression that we're inferring that type from here is a little bit complicated. It's not something simple like -- Oh, I took out a build from my slides. So I've given away the answer here. If this were something like 0.0, then this inference of double here wouldn't really have been necessary. But since we've got this big, complicated expression involving reduced and the power function from the system frameworks, you might not have even guessed that "double" was inferred type of this property.
And so by providing this information here, you've saved work that the compiler would have to do in every file that uses this struct.
And you've also saved work that your coworkers would have to do to figure out what really is the type of that big number property. So a lot of times you can get this extra key piece of information that will help your build times is also an example of a good software engineering practice.
Let's take a look at another example involving closures.
This time I'm trying to define a function that will return the sum of the non-optional values of its arguments.
And if all three arguments are nil, it will return nil.
And I'm trying to use one of Swift's cool features where if you have a closure with a single expression in its body, then the compiler will use that expression to help determine the type of the closure.
Sometimes this is really convenient. Other times it can lead to code like this.
That's pretty ugly. I don't think I'm going to get past code review with that one.
We've got some nested turnery operators and some explicit comparisons against nil. And then a force and wrap to go with it. I don't really think this is going to fly. And it's got another problem, too. Because this expression is getting so large, with so many independent pieces, the Swift compiler will report that it's not able to compile this in a reasonable amount of time.
Now, this is the ultimate in slow builds when even the compiler gives up.
And really, it's telling me something about this code.
So, my first option here would be to do the same thing as the previous example and provide additional types. With a closure, you can do that just before the In Key word.
But, this may not be the best solution for this particular problem. So let's go back to what we had before.
Recall that I said that I'm trying to write a single expression here so that it can be used to help determine the type of the closure.
But in this case, that's not really necessary.
We already know from the call to Reduce what this closure has to be. Reduce is being called on an array of optional integers. And the result type has to match the return type of the function.
So we already know that this callback for Reduce is going to be operating just on optional integers. That means there's no need to put a single expression in that closure. And it's perfectly okay to break it up into separate, more readable statements.
So here's a direct translation of the code that I had before.
But I also have the freedom now to make it something more Swifty. This is a lot more readable. A lot more maintainable. And it compiles in a quick, reasonable amount of time. Now, the last example I'm going to show in this section is something that won't apply quite as broadly as the previous two. It's about this type Any Object.
Now, Any Object is a convenient type that describes any class instance. So not a struct or an enum. Definitely a class. But we don't know which one.
But it also has an additional feature carried over from Objective-C's ID type.
And that's this method call syntax. If you try to call a method or access a property on a value of type Any Object, Swift will allow you to do so, as long as that method is visible somewhere in your project and exposed to the Objective-C runtime.
However, this does come at a cost.
Because the compiler doesn't know which method you're going to call, it has to go search out any possible implementations throughout your project and the frameworks you import and assume that they might be the one that's going to be used.
It has to do this because if none of them match, it needs to present you with an error.
So instead, we can do something much better and much more, again, declarative of our intent. We can define a protocol.
Now, this can be done in the same file, or a different file, but the important part is that once we change this delegate property to use our protocol instead of Any Object, the compiler knows exactly which method it's calling.
And now you also have the opportunity for all of your implementing types to be checked that they implement the method correctly.
So, we've talked about several techniques here for decreasing the amount of work the compiler does once it's already decided to recompile a file.
But what about not recompiling the file at all? What makes the compiler choose whether a file needs to be recompiled? For that, we need to understand Swift's dependency model.
Now, Swift's dependency model is based around files. And it's a little bit tricky because in Swift there are no header files. We just see everything that's defined somewhere in our target by default.
In this case, I'm declaring a struct point in the file on the left. And if I bring in a file on the right, the compiler knows that I'm referring to that first declaration. The same is true for the use of the X and Y properties in that file on the right. Now, this file-based dependency means that if I change the file on the left, both files will need to be recompiled.
And that's important because we're actually trying to call this initializer. And we want to make sure that we're calling it correctly. The compiler is smart enough to know that when you make change within a function body, in this case making the assertion more appropriate, that only that file will need to be recompiled.
Other files won't have to change how they use the API's from the first file. However, it does need to be conservative. And so if I add a separate type to this file, a human can tell that this path segment struct won't affect the file on the right. But the compiler will still be conservative and rebuild them both. Let's see how this applies to the game example that David was using earlier.
So here we have the app target and the Utilities framework. And I'm showing some of the Swift files that are in each target.
So if I change a file in the App target, well, we know already that that file needs to be recompiled. And of course, anything that depends on that file will also need to be recompiled.
But there's no chance that anything within the utilities target will be recompiled.
It's in a separate target. It has an explicit dependency. And it doesn't have implicit visibility between those two sets of files. Now, similarly, if I change something in the framework target, then I would need to recompile that file and anything else in the utilities framework that depends on it.
However, these dependencies are more coarse-grained. And so Xcode will also recompile everything that's in the Game target as well, unless the changes are entirely confined to function bodies. So to recap those rules, the compiler needs to be conservative. Even if a human can tell that a change doesn't affect other files, that doesn't necessarily mean that the compiler can.
However, one change that the compiler does know how to handle is function bodies. It knows that this doesn't affect the file's interface. And therefore, will not require other files to be recompiled.
This per-file dependency basis happens within a module, which is where Swift declarations are implicitly visible to one another.
When you're dealing with cross-module dependencies via your imports or your bridging header, these are dependencies on the entire target. So this is all good information about Swift dependencies, and Swift targets. But I know a lot of you out here have mixed Objective-C and Swift targets. And so the last section is going to be focused on that, on how to reduce the interface between the Swift and the Objective-C code in a mixed-source app.
And to do this, we're going to have to talk about the parts of a mixed-source app.
And this diagram's going to get a little complicated, so bear with me. And if you're watching on video, you may need to pause and restart.
We start off with the headers that describe your Objective-C interface.
This is the parts of your app that are written in Objective-C that you may want to expose to Swift. Or perhaps you're just declaring headers for other Objective-C parts of your app.
Then we have the bridging header. This is the header that collects all of the information that you want to expose to the Swift part of your app.
This is a build setting in Xcode that controls which header is used. And once it's set, the Swift compiler will know to expose those Objective-C interfaces to your Swift code.
The Swift compiler will then produce a generated header, which does the same thing in reverse. It describes which parts of your Swift code will be exposed to Objective-C.
That can then be used in your Objective-C implementation files, which probably also use some of those headers from the first step. And then of course, you might have Objective-C code that is not dependent on any of the Swift code. But that's less interesting for this part of the talk.
So, I'll step through that from left to right again.
We have the Objective-C headers.
The bridging header for getting some of that information into Swift.
Your Swift implementation files.
A generated header for presenting that information back to Objective-C. And then finally, your Objective-C implementation files. And in a diagram like this, all of these arrows represent dependencies. Not dependencies on a target level, but within on a file-by-file level within a target.
And so, what we want to do is focus on the generated header and the bridging header, because if we can shrink the content in these headers, then we know that there's fewer chances for things to change. And therefore, less need to rebuild. So let's take a look.
For the generated header, your strongest tool is going to be the private key word. So in this example, I have a view controller that I'm defining in Swift. And it has an it an IBOutlet property and an IBAction method.
By default, these will be exposed in your generated header because they're methods and properties exposed to Objective-C. And they're not declared as private.
But most of the time you don't need to expose these to any other files in your project. They're just for interacting with Interface Builder.
And so, in this case, I can mark these private and watch as the property and method vanish from the generated header.
Another example of this is when dealing with methods exposed to Objective-C for use with Objective-C runtime features like #selector. In this case, I'm using foundations Notification Center API, which takes a selector to use as a callback when the notification is sent.
Once again, the only requirement here is that the method is exposed to Objective-C. It doesn't actually need to be used from any other files in my project, Swift or Objective-C. So I can mark it private.
And once again have that reduction in the shape of my generated header. In cases like this, there's often another option as well. And that's to switch to block-based API's.
In many cases, this can even clear up your code because you can implicitly capture state from the function that's registering for the notification rather than having to carry it along as some kind of context object. Now, the last tip for reducing the contents of your generated header is actually a very old one. You can migrate to Swift 4.
And you've already heard that you're going to have to do that this year. That Xcode 10 will be the last set of releases where Swift 3 mode is supported.
And so, this is something you'll be doing anyway. Edit. Convert. To Current Swift Syntax.
However, when you do this migration, you may have actually selected to keep the Swift 3 compatibility mode for a particular build setting. And that's the Swift 3 @objc imprints.
This is an option when you migrate to Swift 4 to keep on a rule from Swift 3 which exposes internal methods and properties to Objective-C automatically on any subclass of NS Object. Now, if you are writing in Swift 3, you may be relying on this feature. But there's a lot of cases where you were not actually depending on this in any way. Not in the runtime sense. And definitely not at compile time.
So, once you get to the point where you've explicitly marked all of your Objective-C dependencies as either @objc or IBOutlet, IBAction, whatever, as appropriate, then you can also select this build setting and hit Delete to get it back to the default mode where the OB-C attribute will only be inferred for methods and properties that satisfy protocol requirements or those that override methods that come from Objective-C. So we've talked a lot about the generated header and what you can do to your Swift code. But you have Objective-C code as well. And the Objective-C code, likewise, causes rebuilds.
And so a bridging header looks something like this, usually. It's got a bunch of other headers in the project that you're trying to expose to Swift.
And we can zoom in on one of these headers here, the MyViewController header and see that it's a perfectly normal declaration of a view controller. But also that it itself includes another header.
What that means is that if any of these headers change, the Swift code in your target has to be recompiled because it might depend on something that changed.
This is suboptimal. And now we can notice that in this example, the only reason we're importing the MyNetwork Manager header is to declare this property, this network manager property on the view controller.
And it's possible that that property is never actually used from Swift. In which case, it's unnecessary for us to be declaring it here.
So what you can do is use categories, Objective-C's equivalent of extensions, to break up this interface.
So I'm going to define a new file here, MyViewController Internal, and use the special nameless category syntax that allows me to declare additional properties while still taking advantage of the property synthesis feature in my main Add Implementation block. Now I can just move the import and the property down to the category.
And voila! The headers that are being imported into Swift have gotten much smaller and are much less likely to change now and cause an unnecessary rebuild.
And there's one more note.
This file here that I defined, well, it's possible that nothing else in my Objective-C code needs to access this property, either.
In which case, there's no need for a separate file. I can put this category directly into my .m.
There's nothing wrong with doing this. Everything will work fine. And as I said before, property synthesis will still work for the network manager property. So what have we seen? We used private and block-based API's, and turning off that Build setting to shrink the contents of the generated header.
And, we've broken out separate contents from the Objective-C headers that we declared, which shrink the contents of the bridging header.
Less content means less work done on each build. And it also means fewer opportunities for changes, which means fewer chances for rebuilds. We win on both counts.
So let's wrap things up.
David and I talked a lot about quite a few different topics, of ways that you can get more information from Xcode and that you can provide more information to Xcode in ways that can speed up your builds. And this covers both increasing the build efficiency when you're doing a build and reducing the work that you have to do at all in a rebuild.
So, we went through this kind of fast. So if you want to see it again, check out the video page. And you can also come find us in the labs at noon today and tomorrow in the afternoon. Thank you very much. Enjoy the rest of the conference.
[ Applause ]
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.