Successful app development requires mastering a lot of different things. Discover practices you can incorporate into your development workflow to enhance your productivity, and improve your app's performance and stability. Learn how to improve the quality of code you write with Xcode. Gain a practical understanding of some valuable development techniques.
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].
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.
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.