WWDC2018 Session 408

Transcript

[ Music ]
[ Applause ]
>> 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
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.
[ Applause ]
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.
[ Applause ]
>> Thanks, David.
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.
[ Applause ]
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.
Feel free.
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 ]