WWDC2019 Session 239

Transcript

[ Music ]
[ Applause ]
>> Good morning.
[ Applause ]
My name is Josh.
And I'm part of the Technology
Evangelism team at Apple.
Our team has the incredible
honor of working with
developers, like you, from all
around the world.
And our goal is to help you
create truly great apps.
And in these conversations with
you, we learn so much.
We gain an understanding of the
processes you take; the
challenges, goals, and
aspirations that you have.
We learn about the tricks and
tools that help you out, and
then those that hinder.
And although every story that we
hear is a little different,
there's an incredible number of
common threads no matter which
part of the world the story
comes from.
Now, when you think about craft
you probably first think about
design.
But as developers and engineers,
we craft, too.
After all, craft is defined as
skill in planning, making, and
executing.
It's to create or produce
something with care, skill, or
ingenuity.
Codes written by hand.
It involves incredible skill.
And there's an ingenuity to the
technique that we take.
The choices that we make when
building an app.
And I want to talk to you today
about that craft and care, to
bring care into your code, into
your storyboards and into your
products.
This might seem easy at first.
But with all of the demands on
us, as developers today, this
can sometimes be quite hard.
Skill with a given craft is
something that develops over
time.
It takes dedication, patience,
and focus.
It's about learning to enjoy the
process of getting there almost
as much as the destination
itself.
Now, part of this process is
also about converting those
things that might take intense
and conscious focus at first,
into habits.
Similar to driving, with
experience and practice, the
number of things that we have to
consciously focus on while we're
driving reduces over time
because we convert things to
natural, automatic habits.
And we can do the same thing
with app development.
And to do this, it means
developing great habits, as
opposed to poor ones.
When it comes to building an
app, there's a lot of details to
pay attention to and as a
developer, so many of these
details that we have to care
about, well, they're rarely
actually directly seen by the
customers who use our apps.
And yet, they can be felt in
such significant ways, impacting
performance, reliability, and
stability.
And so, there's a lot of
details.
And there's just not enough time
to focus on them all.
So, I'd like to spend some time
today reviewing some practices
that can hopefully inform and
enrich our work as developers,
things for us to work towards
integrating into our regular
workflows, such that they just
become habit and automatic.
This will save us frustration,
hassle, and waste of time down
the road.
Now, for many of you, I'm sure
you're already doing many of
these things.
But perhaps there are some that
haven't yet fully become habits
for you.
And maybe you'll be inspired to
practice them more.
So, first, let's get organized.
In addition to being an app
developer, I'm also a
woodworker.
I find that it's this great
escape from the modern-day world
for me.
But one thing that's for certain
is that beautiful, well-crafted
furniture, is much easier to
build in a clean shop.
So, if your bench is cluttered
and poorly organized, it's hard
to find the tools and materials
that you need as you work along.
You constantly just have to
shuffle things around to make
room for the work that you're
actually doing.
And, in short, everything takes
so much longer than it should,
and more accidents and mistakes
happen along the way.
Our team sees a lot of Xcode
projects each year.
And there's definitely some
practices that can help you make
sure that your work space is
clean, tidy, and in a great
position to allow you to do your
absolute best work.
Xcode projects benefit from
structure and organization using
groups.
This makes it so much easier at
a glance to see the files
involved in each section of your
application.
It can help you hone in quickly
when you're trying to fix a bug.
Groups are best used to organize
your project functionally in a
way that logically follows how
someone might interact with your
application.
We often see projects that are
organized by file type or maybe
just don't use groups at all.
And that doesn't really help
someone out later when they're
quickly trying to understand how
all of these source files relate
to each other.
Furthermore, it can really help
to make sure that your Xcode
project structure and your file
system structure actually match
each other.
Since Xcode 9, when you create a
new group inside of your
project, it actually also
creates a folder on disk to
house the files that you place
inside of that group.
This means when you're looking
at your project in source
control, or just browsing the
file system, the structure is
mirrored, and this will really
help you reduce confusion and
missteps later on.
Storyboards are an incredibly
powerful tool for building out
user interfaces in a visual
manner.
But we do come across a lot of
projects that build out their
entire UI in a single
storyboard.
And there's no reason to do this
thanks to storyboard references.
Use a different storyboard file
for each major section of your
application and then use
references to tie them together.
You'll find it makes it so much
easier to isolate individual
changes.
And it makes it much simpler
when working with larger teams,
as you can avoid the risks of
those nasty merge conflicts and
make resolving those that occur
way easier.
Just like you wouldn't put your
entire source code in a single
file, don't put your whole UI in
a single storyboard either.
Keeping your project file modern
is a critical way to make sure
that Xcode can help you out and
avoids the accumulation of
issues.
This is one of those tasks that
can really be a non-issue if you
take care of it regularly.
But it can really cause problems
down the road if you don't keep
up with it.
First, when you're updating to a
new version of Xcode, you'll be
offered the opportunity
sometimes to have Xcode update
the project settings and update
your project file to the latest
format.
So, unless you have some
critical reason not to do this,
we'd recommend that you do it
whenever prompted, or whenever a
warning appears in the issue
navigator.
Second, make sure that your
project is using the new Xcode
build system first released in
2017.
It offers significant
improvements in performance,
dependency management, and it's
absolutely critical to your
adoption of Swift packages.
Now, it's been the default build
system since Xcode 10.
And you can verify the build
system that your project is
using by looking at the project
settings found under the file
menu.
Us woodworkers, we tend to hold
onto small scraps, just in case
we need them someday until that
scrap bin gets so full that we
just can't work around it
anymore and we have to come to
terms with the fact that these
tiny pieces of wood that we've
been holding onto are never
really going to make it into a
project.
As developers, we have this
tendency to hold on to scraps,
too.
But it's a simpler decision for
us.
Since you've got your project
under source control, and you do
have your project under source
control, right?
Get rid of that unneeded and
unused code.
Don't just comment it out just
in case you want to pull it back
someday.
If you ever actually need it,
it's going to be in the history
for that file.
And you can still get it back.
Let go of those scraps.
Another pile that we really
don't want to have grow out of
control are warnings.
And to that end, establish a
zero-warning practice for
yourself and for your team.
Code should never be checked in
that contains warnings.
And you should treat warnings as
errors while you're writing your
code.
Just fix them as you go along.
We've run into projects with
thousands of warnings.
And in most cases, they've just
accumulated for so long that the
developer gave up and just never
had time to go fix them all.
Furthermore, if you're
maintaining a project like this,
you're not going to see new
warnings when they show up.
So, this is getting organized.
Keeping a clean workspace and
project, it's critical to the
long-term health and success of
your app.
So, organize your project with
groups.
And have those groups mirror the
file system structure.
Break apart those large
storyboards using references.
And make sure your project file
is up to date.
Clean out that old and abandoned
code and get to the root problem
of warnings, and fix them as
they arise.
Doing these things will make
your project more nimble and
your development workflow way
better throughout the life of
your project.
Speaking of source control, one
of the things that you should
always do when you're setting up
your project, is enable source
control.
We do actually come across a lot
of projects that don't use
source control, especially those
with solo developer teams.
Conveniently, when you're
setting up a new Xcode project,
all it takes is to ensure that a
check box is checked and your
project will be under source
control using Git.
Now, you can always go back and
see what changes you've made in
the past,
what's about to change, when you
commit your current set of
changes.
And more easily catch any sort
of errors.
So, now that you've got Git
enabled, there's a few things
that you should keep in mind in
order to make it more helpful
and effective.
First, keep your commit small.
Check your code into your
working branch regularly in
small increments.
And keep those changes as
localized and self-contained as
possible.
This will give you a path to
look back upon when later on you
need clues or you're trying to
sort out a regression.
And, meanwhile, it's going to
reduce your odds of introducing
a regression because you're
making those smaller changes.
Second, write useful commit
messages because there comes a
day when we all ask the question
that we wish we could answer,
what on earth was I thinking?
Your commit messages are your
notes to future self when you're
trying to recall under what
circumstances some code changed
and the reasons why.
Run your source control, like
you would for a large team.
Even if you were a solo
developer.
It means maybe branching for
bugs and new features.
And then once you wrap those up,
squash them together back into
the main or dev batch, and use a
clean and helpful commit
message.
Now, there are several options
and patterns that you can follow
for your source control.
We recommend checking them out
and finding the one that works
really well for you and just
integrating that into your
developer workflow.
So, that's tracking.
Source control is absolutely
critical to a successful, modern
app development workflow.
So, adopt it as part of your
project and embrace it as part
of your regular practice.
Keep those commits small and
write useful commit messages.
And finally, utilize branches to
help isolate and manage those
changes, bug fixes, and feature
work.
Two of the greatest contributors
to clarity and maintainability,
in my opinion, are code comments
and documentation.
They're a trail of helpful
breadcrumbs for your teammates
or your future self.
Some might say, "I don't need
comments, my code is
self-documenting."
I don't buy this at all.
Well-written code is clear in
what it's doing algorithmically.
And it's self-documenting in
that respect.
But it doesn't convey why.
Why was this code written in the
first place?
How does this code fit into the
larger context?
Nor does it describe the
rationale behind the approach
taken when writing it.
The best developers that I work
with, that not only write
incredible, clear, and concise
code but they take the time to
sprinkle helpful review comments
throughout that code, to guide
the future reader into the
headspace of the original
author.
Junior developers will likely
benefit from this process, even
more because you're experience
at the beginning of a project,
varies so much from at the end.
And the decisions that you make
at the beginning of a project
might actually be at odds with
the decisions made at the end.
So, what makes for a good
comment?
Well, a good comment assumes
that the reader understands the
programming language being used
and can walk through the
sequence and steps being taken
in the code.
And instead, it really focuses
on why that code was written in
the first place.
What's the backing for that?
For example, this is the kind of
comment that just doesn't really
add any value.
And yet, we see it all the time.
I'm assuming that most of you
have written some code in Swift
and could just figure out that
we're creating a string constant
here carrying that value.
But we have no idea what id is,
what it's used for, or why has
this been hard coded into the
app?
So, with a little bit of
commenting, we now understand
why this value exists and where
it came from.
But we can take this one step
further; names for constants and
variables offer additional
opportunities for clarity.
So, if you find yourself using
single letters like m or i, or
things like id or idx, it might
be a great opportunity to choose
a more descriptive variable
name.
Autocomplete in Xcode works like
a charm.
So, you don't even really have
to type anything more.
And it will always be clear
throughout your code base what
particular identifier, in this
case, is being used at any point
in time.
The benefits to documentation
are very similar to that of
comments.
But these scale throughout your
application and beyond.
As you write your own apps,
you're creating layers upon
layers of abstraction and
algorithms taking what would be
large and winding passages of
code and breaking them down into
tidy, testable, reusable
functions.
But if you choose not to
document these functions, you're
forcing yourself to actually
rewrite that documentation in
your mind every single time you
go to use that function.
Typically, by having to go and
revisit the implementation of
the function, look at how each
of the parameters are being
used, and figure out how it's
going to transform them to
provide a result.
In case you aren't aware,
generating a Dock stub in Xcode
is incredibly easy.
Just place your curser on the
first line of the function
signature, press option command
slash and all the placeholder
text you need will be generated
automatically.
Fill in the blanks and you're
done.
Option clicking on any usage of
that function will now bring up
your own documentation in the
same contextual quick help popup
that you've come to know and
love for the native SDK and
Swift standard library.
Comments and documentation:
They're one of these really low
effort but incredibly high
reward investments of your time.
And it pays off repeatedly
throughout the life of your
project.
So, aid future understanding by
sprinkling your code with
helpful comments.
Bring readers into the headspace
of the original author with
those comments.
Use descriptive names for your
variables, and fully document
your functions, properties,
Structs and Enums.
Next, I'm going to talk about
testing, and specifically, unit
testing.
And to do that, I want to
introduce Marshall.
Marshall is our Swift and
developer tools evangelist.
He's an incredibly brilliant and
kind fellow.
And he also happens to be a
walking, talking Swift-linter.
Every time I submit my code for
review, I brace for the tsunami
of insightful comments and
feedback to help me improve what
I've written in both form and
function.
But the other day, Marshall
nudged me in the right direction
on another topic, unit testing.
Now, I must admit, I don't
exactly have an impeccable track
record in writing unit tests.
It's not that I don't appreciate
the potential value in them or
that I'm necessarily new to
them.
It's just I tend to always leave
it to last.
And by the time I've finished
implementing the actual code,
the last thing I feel like doing
is writing a unit test.
Nevertheless, the other day,
while implementing the data
model for the new lab queuing
feature, in the DubDub app,
Marshall piped in.
>> And while you're doing that,
you might as well add a unit
test to make sure that the round
trip between the Struct and the
dictionary representation keeps
working.
>> Now, in my mind, I really
couldn't figure out how this
would be messed up in the
future.
But nevertheless, I listened to
Marshall and I put in a simple
round trip unit test.
I ran it.
And I felt this significant bit
of satisfaction when the green
check mark showed that the test
passed.
So, I submitted my changes for
review and I didn't think about
that test again until a couple
of weeks later, when we wanted
to include some additional data
in that Struct.
So, made the changes to the
Struct.
And I didn't see any issues at
runtime.
I'm done, right?
So, went to submit my changes.
And then I remembered to run
that unit test.
And sure enough, I'd forgotten
to change how the dictionary
deserialization was working and
the test caught it.
The bug would have shown up much
later, when we implementing the
UI.
And would have undoubtedly
wasted a fair bit of our time,
trying to figure out what went
wrong.
So, thanks, Marshall, for
reminding me to include unit
tests, as part of my regular
practice.
>> You're welcome, Josh [brief
laughter].
[ Applause ]
>> So, even for sections of code
that seem deceptively simple, as
this particular one did to me at
the time, it's so important to
write those unit tests.
With the malleability of code
comes the potential introduction
of regressions.
And given that we never seem to
have enough time to test things
thoroughly, let's put Xcode to
work as an extra set of eyes.
So, make the implementation of
unit tests a part of your
regular development practice and
run those tests before each
commit.
Also, unit tests are a key
component to continuous
integration.
So, you can get yourself set up
for that.
Tests are another one of these
hidden details that your
customers will never actually
see.
But yet, could mean the
difference between an incredible
experience using your app or a
very frustrating one when some
important piece of their data
has been corrupted.
There's forms of analysis that
you'll want to keep as part of
your regular workflow.
Now, some of these do require
some extra time investment.
But others can happen for you in
the background without you even
having to think about it.
One tool that can be very
helpful is the Network Link
Conditioner.
After all, app development tends
to be performed in homes and
offices with incredible network
performance.
But this really isn't a
representative environment of
where your app is likely to be
used.
So, by enabling the Network Link
Conditioner, you can
artificially constrain your
network performance to one
similar to that of a typical
cellular network.
Or even a poorly performing one.
You'd be amazed at the number of
issues that you'll catch with
loading and race conditions so
that you customers don't.
Inside of your scheme settings,
there's also several sanitizers
and checkers that can help
discover various issues
throughout your development
cycle.
The Address Sanitizer will watch
for things like memory
corruptions and buffer
overflows.
Memory issues are frequently the
cause of security
vulnerabilities.
So, using the Address Sanitize,
will help you make sure that you
don't ship these is the first
place.
By enabling the Thread
Sanitizer, while testing your --
and debugging your app in the
simulator, you can help discover
data races.
Data races are when you have two
threads that are not
synchronized and at least one of
those two threads is attempting
to do a write on the same piece
of data.
Now, these can be particularly
nasty bugs and they can have
programs acting unpredictably.
Or they can even result in
memory corruption.
The Undefined Behavior Sanitizer
captures bugs like dividing by
zero, out of range casts between
floating point types, overflows,
and misaligned pointers.
And when a program has undefined
behavior, it might cause a
crash.
It might act in unpredictable
ways, or it might act like it
has no problem at all with
different results at different
times seemingly with no reason.
Incredibly frustrating bugs; the
sanitizer can help you get rid
of them, before they can wreak
havoc on your project.
And finally, there's the Main
Thread Checker which ensures
that you're not performing
invalid usage of appKit, UIKit,
and other API's on background
threads.
For example, if you're updating
the UI on a thread other than
the main thread, it can cause
missed UI updates, visual
defects, data corruptions and
crashes.
Sometimes these bugs can be
really hard to track down
because they might only appear
intermittently.
Now, there's minimal performance
impact by having this enabled.
So, we just recommend leaving it
enabled whenever possible.
While debugging your apps, keep
an eye on performance and
resource utilization.
And make sure that your app is
being as efficient with system
resources as possible.
The first step is, use the Debug
Gauges.
These are found in the debug
navigator in Xcode anytime
you've built and run your
project.
Here you can check out CPU,
memory, disk, and network
utilization throughout the
lifecycle of your app, quickly
understand if your app is doing
something like connecting to
unexpected servers over the
network.
Or maybe it's constantly pulling
at an end point, and chewing up
a ton of bandwidth and battery.
And finally, you can take this
even further, by clicking in the
profile and instruments button
which will allow you to run an
even more in-depth analysis.
One particular instrument that I
use a lot is the time profiler.
This allows you to ascertain
which passages of your code are
taking up the most cycles and
has allowed us to narrow in on
passages of work that might need
to be made asynchronous.
Or perhaps, I just implemented
in an unscalable manner.
Analysis is a really broad
subject.
But most of the tools I
described here only require that
you remember to turn them on.
So, simulate typical and poor
networks using that Network Link
Conditioner.
Use those sanitizers and
checkers frequently.
And just leave them enabled, if
you can.
Refer to those Debug Gauges
regularly.
And just keep an eye on the
footprint and performance of
your app.
And dig deeper into issues and
address them with great
precision, by analyzing your
app, using instruments.
Turning these small efforts into
habits will go a long way into
improving the performance and
reliability of your apps.
Back when I lived in Toronto, I
had a single car garage that I
had converted into my
woodworking shop.
It was a cozy space.
And I had it entirely to myself.
But since moving to the Bay
Area, I don't have a space to
myself anymore.
And I've been using various
shared and community woodshops
in the area.
Now, doing so can be a bit
frustrating at times because now
I have to share the tools and
the equipment and the space with
others.
But something I hadn't realized
I would appreciate so much is
the opportunity to bounce ideas
off of others in the shop.
And get their opinions on ways
to go about doing things.
I think with app development the
analog here is that of code
review.
So, for many of the apps I've
built over the years, I've been
the solo developer.
And much like having your own
shop, it feels incredibly fast
and nimble because only your own
opinion matters.
But the drawback is that you
don't have that opportunity to
learn from your colleagues and
peers on better ways to use the
language, frameworks, and SDK.
Often, although there's lots of
ways to approach a problem,
there's often a better way.
Something that stands out in
terms of being more concise.
Or maybe is more -- has greater
performance, maintainability or
reliability.
Because, after all, just because
it works, doesn't mean that it's
necessarily right.
Or that it could somehow be
significantly improved.
At Apple, all teams have a
policy where no code makes it
into a project without code
review.
Our team has learned so much
from each other through this
process.
And our code is way more
consistent in its style, let
alone the improvements in the
reliability.
It also ensures that our entire
team is more familiar with a
broader set of the code base,
allowing the range of bugs and
features that we can each tackle
to be much wider.
Now, I have this fortune of
being on a great team of
experienced developers, which
makes this much simpler.
But what if you're running a
company on your own or are the
sole developer on your project?
Well, try to find a way to
connect with fellow developers
in your area or from around the
world.
And come up with a way to do a
code review exchange with them.
Maybe investigate meet-ups,
local conferences, and
co-working spaces.
So, now that you're going to do
code review, as part of your
development practice, what makes
a great code review?
Well, first, it means taking the
time to understand each changed
line of code.
There's no point in doing a code
review if it's just a quick
skim.
Second, actually build the
project.
Run it. Don't assume that the
original author actually did
this.
Especially if the last commit
you see in the history was a
merge.
Run those tests.
First, doing so reminds you to
check and see that there are
actually tests.
And that the unit tests pass.
Remember, that just because it
builds doesn't mean that it's
not broken somehow.
Read those comments and
documentation thoroughly.
I mean, there are comments and
documentation, right?
And then look for spelling and
grammatical errors.
Similarly, look for spelling
errors in variable names.
So, as a Canadian, I have this
long-standing habit of including
"u" in words like colour, which
drives my team absolutely nuts,
when they go and search for
color.
Ensuring consistency in the code
base helps with finding and
using these functions and
variables later on.
And again, it just saves time.
So, even though it might feel
like this process is slowing you
down in the short-term, it will
undoubtedly save you time,
money, and customers in the
future through the reduction of
potential errors and issues in
the long-run.
And your skills, as a developer,
will benefit significantly, when
you approach similar patterns or
challenges in the future.
As developers, we're all
endeavoring to create small,
refined, reusable and testable
sections of code.
After all, we don't want to have
to constantly recreate the same
code over and over.
Packages and frameworks offer an
opportunity to maintain that
code in a more centralized way.
And offer that functionality in
a portable fashion.
Not only through your current
app, but through other apps that
might be able to leverage that
effort.
If your app includes extensions,
by packaging up your shared code
between -- in a framework, your
binary size will actually reduce
because both your main app and
your extensions can actually
share that same framework.
Of course, creating packages
also offers the opportunity to
share your efforts with the
community especially with the
tight integration now found in
Xcode 11.
But even more than the code that
lives in your app, shared
frameworks, packages, and
libraries need to be accompanied
by great documentation, in order
to be useful to others.
So, embrace packages and
frameworks as a way to break
apart your code base.
This will also allow you to
scale your work across multiple
apps you might be working on and
maintaining.
Frameworks can help you reduce
the binary size.
And then you can, of course,
share your efforts with the
community.
But be sure to include that
great documentation.
The last area that I want to
talk to you about today is
dependencies, and, specifically,
understanding the benefits and
risks of bringing them into your
project.
Using Swift packages,
frameworks, and other libraries,
offers many benefits.
But before you start to use a
given package, it's really
important to know what's inside
of it.
And what could be potentially
coming along for the ride.
Make sure that you understand
what your dependency is doing
with data.
Ultimately, you're responsible
for the contents of your app.
And what it's doing with user
data.
Make sure that the framework
isn't collecting metrics or
device information that's
unnecessary.
And make sure that it's
definitely not sending that data
off device.
Note what other dependency, a
giving dependency, depends on.
And research into those, as
well.
After all, including a
dependency with dependencies now
means that your app is actually
riding on the security and
success of that entire chain.
And, finally, there's one other
possibility.
What if the framework breaks on
you?
What if it becomes unmaintained?
Or what if it just disappears?
It's really important to have a
plan on how you're going to deal
with each of these situations
anytime you're introducing a new
dependency into your project.
After all, your applications
future is now dependent on it.
So, are you going to be able to
fix the open bugs yourself?
Are you going to bring that
project in-house and maintain
it?
Or are you going to plan to have
to completely swap out that
dependency later?
With all of the necessary work
that comes with that task?
The use of external
dependencies, such as Swift
packages, can allow you to move
more quickly and avoid
recreating tools that might
already exist in the community.
But be diligent in their use.
Ensure that they only do what
you expect them to.
And absolutely ensure that they
respect the privacy of people
using your apps.
Make sure you establish that
plan of what you're going to do
if they break or otherwise go
away in the future.
If you make answering these
questions a habit when adding a
new dependency to your project,
it's going to payoff in the long
run, and maximize the benefits
of using them.
With app development projects,
it can sometimes feel like the
last 10% of the project takes
just as long as the first 90% of
the project.
But I think that by trying to
convert some of these practices
and principles into habits, you
can help avoid that feeling.
So, by effectively organizing
your workspace, you can work
faster and more efficiently
keeping focused on the actual
code.
Through the power of source
control, you can track your code
base with precision, reduce the
odds of regressions, and
expedite the investigation of
bugs that might occur.
By writing helpful and
meaningful comments and
documentation, you can reduce
the cognitive burden whenever
you revisit code in the future
and every time you make use of a
class, Struct, or function that
you've built.
Unit tests will save you at the
eleventh hour, from checking
encode that introduces new
regressions.
Sanitizers and checkers offer
ongoing analysis of your code
and they run in the background
without you even having to think
about it.
Gauges and instruments ensure
that you are being efficient
with your use of system
resources and they'll allow you
to chase down performance and
other issues, with precision.
Code review is not only a chance
to evaluate the style and
function of your code, but an
enormous learning opportunity
for developing -- for evolving
your skills as a developer and
sharing them with your team and
the community.
Breaking your projects into
smaller and reusable packages
and frameworks can help scale
your work across multiple
projects and allow you to share
it.
There's also those benefits to
binary size.
And finally, the use of external
dependencies, such as Swift
packages, can help you move more
quickly and reuse functions that
might already exist in the
community.
But be diligent in their use.
Understand what they do with
user data.
And establish a plan in case
they go away.
Including these practices as
part of your work as an app
developer will only add a small
bit of time to each phase of
your project.
But it will save you an
incredible amount of time, over
the long run, ensuring your app
is built to last.
I hope that this collection of
ideas and suggestions I've
offered you today, has allowed
you to think about how you too
can further improve your craft
as an app developer.
Practices you might incorporate
to allow you to raise the
quality and durability of your
work and conscious efforts that
you can turn into automatic
habits, that allow you to direct
your energy to the areas of most
importance.
For the people who engage with
your apps will feel that care
and love that you've poured into
your work even if they can't
exactly say why.
And you can take great pride in
what you have crafted.
Thank you.
[ Applause ]