Streaming is available in most browsers,
and in the Developer app.
-
Xcode essentials
Edit, debug, commit, repeat. Explore the suite of tools in Xcode that help you iterate quickly when developing apps. Discover tips and tricks to help optimize and boost your development workflow.
Chapters
- 0:00 - Introduction
- 1:31 - Find the right content
- 1:45 - Filter navigators
- 3:05 - The Find navigator
- 5:43 - Move between files
- 5:47 - The tab bar
- 7:41 - The jump bar
- 8:38 - Tricks for creating new files
- 9:34 - Warnings and error annotations
- 10:42 - Using bookmarks
- 10:56 - Using Mark comments
- 11:15 - Leverage shortcuts
- 11:22 - Open Quickly
- 12:12 - Useful commands and shortcuts
- 14:19 - Code completion
- 15:10 - VIM mode
- 15:25 - Common emacs commands
- 15:43 - Get the most out of git
- 15:47 - Show the last change for a line
- 16:07 - Changes navigator
- 16:55 - Debugging
- 17:21 - Setting breakpoints
- 21:40 - Using the console
- 23:01 - Testing
- 23:51 - Test Navigator
- 26:09 - Running tests
- 27:42 - Using Test plans
- 28:38 - Code coverage
- 29:36 - Explore the Test report
- 30:23 - Distributing your app
- 30:48 - TestFlight
- 31:26 - Archiving
- 33:26 - Explore the Organizer
- 35:41 - Wrap up
Resources
- Forum: Developer Tools & Services
- Including notes for testers with a beta release of your app
- Testing
- Xcode updates
Related Videos
WWDC24
WWDC23
- Create practical workflows in Xcode Cloud
- Debug with structured logging
- Simplify distribution in Xcode and Xcode Cloud
WWDC22
- Author fast and reliable tests for Xcode Cloud
- Debug Swift debugging with LLDB
- Get the most out of Xcode Cloud
Tech Talks
-
Download
Hey Software Engineers! My name is Myke and I’m a Project Manager for Xcode. And I’m Cheech, and I'm a designer on the Xcode team. Whether you're a veteran of Apple SDKs or a new developer on our platform, we're going to show you the essentials of working efficiently in Xcode so that you can cruise through the development cycle with speed, clarity, and joy. Every day we edit, debug, test, commit and repeat. When our project is new, this cycle is quick, little research is involved, But, as our projects and teams grow, or when we join a new team, the balance shifts and getting started on the problem, becomes half the problem: adding features and fixing bugs involves more research, so we can make the right changes in the right places. The good news? Xcode has tons of features to help you find the code you need to make your next change. I’m going to show you how to make edits faster. Then Myke will walk you through debugging and testing your changes. Once you’re ready to share your app with an audience, it's time to distribute.
There’s a ton of functionality in Xcode, including a lot of power features. You’ll learn how to harness these features to find the right code to edit, by effortlessly moving between files. We’ll learn Xcode’s most powerful key commands, and I’ll share a few tricks so you can get the most out of git. Let’s start with: how to find the right content.
If you’re opening Xcode for the first time, you might think to yourself “Wow, there’s a lot going on.” And you’re right. Everything you need to swiftly iterate on the development cycle is already embedded into Xcode. On the left-hand side of the interface, each navigator gives you a different perspective of your project. In this view, the project navigator is selected: showing my project’s file hierarchy.
If you’re in the flow, creating new files and writing code and find that you’re navigator is chaotic, well, first I’d recommend adding a folder or two, then try typing a file’s name into the bottom bar to find it. Remember the power features I mentioned? Turns out, there are specialized filters in many of the navigators.
If I start typing, and my text matches one of these filters, you’ll see a menu pop-up above the input, offering additional functionality, like filtering by target name.
The icons that appear at the right of the bottom bar also change based on the navigator. Clicking the icon on the far right, filters the files by git status. This makes it easy to return to files in your upcoming commit, while showing them in the context of your whole project. If you have commitment issues, in Xcode, try using this filter to easily review changes before committing. Filtering is great if you actually know the file name, but what if you can’t remember what the file is called? I know, that doesn’t happen to me either, let’s just pretend. In this situation, I recommend Xcode’s Find Navigator. Command-Shift-F will take you there. The Find navigator allows you to search across your entire project. Sometimes the search only has a handful of matches, and the results are all you need. Other times, you’re searching for something that appears all over your project, so you need to narrow down the results. This happens to me way too often. Luckily, Xcode’s got you. Here are some tools you can use to quickly find the desired result. If you guessed the bottom bar, then you nailed it. Just like the project navigator, you can use the bottom bar to narrow a search. Try filtering for additional words on the matching lines, or by file names to see results in that file. But if you’re not quite sure what you’re looking for, here are some nifty ways to interact with the content in the navigators. To scan the file names before looking at any of the text matches, I’ll hold Command while clicking on the disclosure arrow to the left of a file name, and collapse all of its siblings. This works on all outline views in Xcode.
Now, I’ve decided I don’t want to make any changes to “padding” in the first 3 files, and I want to remove them from my search to focus on the more relevant files. I’ll go ahead and select, And press the Delete key to remove them. Don’t worry, I didn’t just delete a whole bunch of code. Those files still exist, but they’re hidden from the current query. Now I can focus on the results I care about. If your query is like mine, and has a lot of matches, but you only care about files in certain groups, Use the menu beneath the search field to focus the search. You can choose any of the groups containing the file you’re editing. Choose "Custom Scopes…" to pick a different group, or even multiple groups. If you find you’re often using the same search scope, save it and it will show up in the initial menu in the navigator. Filter Xcode’s symbol index from the menu that opens when you click the word “Text” above the search field. "Descendant Types" is excellent for getting an overview of a class hierarchy.
Have you ever wanted to copy and paste something to the find search field, just to realize that you already had important info copied and now it’s gone forever? Luckily, macOS has a solution. Press Command-E with any text selected, and it will go into the find field, leaving your clipboard intact. This works in any macOS app. Neat, huh? Ok, so we’ve covered filtering and searching to find the right content, now I want to show you some handy ways to move between files. First is the tab bar, located towards the top of the source editor, underneath the toolbar, its a great way to switch between documents.
Even though we’ve been to a handful of files today, I only have 4 tabs open in the tab bar. Why is that? Turns out, Xcode has 2 types of tabs. There are permanent tabs for the documents I’ve shown explicit interest in, like editing code. Then there is an implicit tab, that Xcode creates for files I’ve only passed through. It disappears when I exit the file. You can tell the implicit tab from the others because of its italicized title.
If you’d like to make a tab permanent without editing it, you can choose “Keep Open” from the context menu, or double click on the tab.
Finished a task and ready to close out a bunch of tabs at once? Instead of closing each tab individually, try Option clicking the close button of one tab to satisfyingly close all of the others. To the left of the tab bar, the back and forward buttons work as, well, back and forward buttons. But, if you click and hold one, like the back button, you can see a full history. Immediately jump to the desired location without having to click the button twenty or so times. To the left of the back button, is the related files menu and its name is likely self-explanatory. There’s a recent files list, and then different types of symbolic relations will appear based on the text: like subclasses of the current class, or callers of the current function. There’s all sorts of stuff to explore in here. The three buttons to the right of the tab bar are for configuring the editor UI. The middle button controls the editor layout. You can add a SwiftUI Preview, the editor mini-map, and line accessories like blame or code coverage.
Sometimes your navigator is already showing important info, and you want it to stay put. Remember our find query from earlier? But, you still want to jump between related files. You could poke around the tab bar, or you could use the jump bar. Located just below the tab bar, the jump bar shows the path to the current file. Every step in this path is interactive.
Click on an item to view neighboring files. These lists can get big, and if you’re like me, navigating large menus can be disorienting. Start typing, and a filter field will appear in the top of the menu, and you can, that's right, filter the menu. Many menus in Xcode have this functionality. Try it out. To find an item from the jump bar in the project navigator, press Command-Shift-J.
Try this when you want to create new files near the current file. And there are a whole bunch of ways to create a new file in Xcode.
You can get an empty file, skipping the template chooser, from right clicking just about anywhere.
If you want to use the format of an existing file to create a new one, copy & paste, or duplicate are classics. My personal favorite is to hold option then click and drag an item to copy it. I use option drag for everything.
Maybe your file is getting too big. You can cut some of it to the clipboard, using Command-X, then right click anywhere in the navigator. Hold option and some of the menu items will change. Choose “New file with contents of clipboard.” Alternate menu options, revealed by holding option, are another power feature on Mac. Clever, huh? Or you can paste the content directly into the navigator and Xcode will create the new file for you.
If you make a mistake in your code, it’s rare, of course, Xcode will annotate the line with a warning or error. These annotations are interactive.
If it has more info than you can see, click to open and show the rest of the information. If there is more than 1 issue on a line, click to open. If the compiler has a fixit, you guessed it, click to open. Then click Fix to apply the fix-it Fix-its offer suggested fixes for syntax errors as you write code.
When the issues turn gray, that means you’ve changed the file since the issue last refreshed. If the issues disappear, you’re in the clear. But if they stay? You’ll have to try again, my friend. You can actually insert your own warnings as a form of task management. Type this mark: “#warning" with a message into the editor.
For those of you who have 238 unread text messages, you know how you are, error annotations are probably great. Personally, I prefer a clean slate.
Bookmarks are where its at. Right-click anywhere in the editor, to add a bookmark to a line. They’re a great way to privately manage tasks. Plus it feels great to check them off in the bookmarks navigator.
Looking for a more permanent annotation? Try adding Mark comments to your file. They act as section titles and they appear in the mini-map, and the editor content jump bar segment.
Wow, there are a lot of ways to move between files. Hopefully, you’ve picked up something new. I’ve mentioned a lot of key commands thus far, but believe it or not, Xcode has even more shortcuts to leverage. Open Quickly is the closest thing Xcode has to teleporting: it’s the fastest path between point A and B. Just press Command-Shift-O from anywhere, then type in some part of a file or symbol name, and open quickly will immediately give you a list of destinations. It uses code completion-like matching rules, so you can type any of the unique words in your destination name to get there immediately.
If you include a slash in the query, you can match file paths instead of file names, and you can end a query with a colon, and the line number, to get to a specific line.
Maybe you’d prefer to open the file in a new split instead. Hold Option when pressing return And now you can view the two files side by side. Check out Xcode’s navigation settings to customize modifier behavior. Xcode has SO many powerful commands, we’re running out of keys to create new ones. I have my favorites memorized but instead of sifting through menus for the rest, I use Quick actions. Command-Shift-A opens quick actions, where you can do a search of Xcode’s commands with natural language. You can explore and customize Xcode’s many unique commands by navigating to the Key Bindings tab in the Xcode’s Settings pane. Here are some of the go-to commands amongst our team: “Jump to Definition”, which you can invoke with Command-click, will take you to the definition of a function or type. Use Option-click to “Show quick help”, and see either the documentation for the clicked symbol, or for Swift variables, to see their inferred type. “Edit All In Scope”, which you can invoke by pressing Command-Control-E with text selected, and rename a symbol and all of its occurrences in the current file. You can see all of the callers for the current function by selecting “show callers” when you right click. If a function call is running on, try “Control-M” to reformat it to many lines.
Trying to figure out where a matching paren, bracket or quote is? Double click on one side to jump to the other. You can use Option and the arrow keys to move by word. And also use Control with the arrow keys to move by subword. And Command-Left or Right is helpful if you’re familiar with pressing Home and End to move to the beginning and end of the line. These text movement commands are essential with the source editor’s multi-cursor editing feature.
Creating a repetitive switch statement or initializer, and need to do the same thing to many lines? Hold Control-Shift, then click where you’d like to insert multiple cursors to create multiple statements at once.
If you need to put similar code into several places, you can create a template with the same placeholders you see in code completion by surrounding text with these characters.
Xcode’s code completion helps you complete your code, even when you can only remember a few words. In Xcode 16, predictive completions suggest whole statements and methods based on the surrounding code. These completions appear inline with your code. And you can press the Tab key to accept what’s shown. Or hold Option to expand the entire prediction. Press Option-Tab to accept the whole thing. Here’s a pro tip, your comments and variable names help inform the predictions: the more expressive you are, the better the predictions will be. The completion window packs a lot of functionality into a small space. You can see the full type signature of a method at the bottom of the window.
You can hold Option when choosing the completion and press Enter to accept all of the arguments.
If you’ve ever used vim before, then you know it’s tough to quit. Xcode makes it easy to toggle in and out of vim mode in the Editor menu. And Xcode 16 supports vim's repeat command as another form of multi-cursor editing. Maybe you enjoy moving around the line without using the arrow keys: Xcode also supports all of the native macOS text editing interactions, which includes support for many basic emacs commands, like Control-A, E, P, and N, along with many others. Now let's look at some quick tricks to get the most out of git. Imagine you’re fixing an error, and want to know more about how it came to be, Right-click and try “show last change for line”. You’ll get an overview of the commit for that line. It’s like a focused version of blame. Just be ready to find out that you introduced the problem.
When you’re ready to review all of your changes before making a commit, preview the upcoming commit in the changes navigator. Take a moment to appreciate your progress before staging your changes, committing, and going to lunch. Let’s recap how to make edits faster in Xcode: we talked about how to Find the right content by Filtering content in the navigators, and using the Find Navigator. We learned new ways to Move between files using the Tab bar, Jump bar and with annotations in your code. Don’t forget to leverage Xcode’s many shortcuts using open quickly & quick actions And lastly, we talked about a few tricks for using git.
Now I’m going to pass it to Myke to talk about Debugging. Thanks, Cheech! Debugging is all about finding the right code to change. You might know exactly which line is misbehaving, but it's often unclear why. The code might execute hundreds of times and then something goes wrong. And sometimes a seemingly innocuous bug may just be setting things in motion for a much more distant failure. I’d like to show you some cool tricks for using breakpoints and then I’ll show you how to make your print statement debugging way more effective. First, let me show you some of the features of breakpoints to help you isolate these problems, especially in high traffic code.
When things are simple, just click on a line number to add a breakpoint and your program will stop when it gets to that line. If the breakpoint would be hit too frequently to be practically useful, here are some techniques to try. You can use two breakpoints in tandem. Let’s say you have a busy function running constantly, that also runs just after you click a button in your app, and it’s only that one invocation that's problematic. Add a breakpoint to your button handler, and add a second breakpoint to your busy function. Disable the breakpoint in the busy function by clicking it a second time. When you run your program, hit the breakpoint in your button handler, and re-enable the breakpoint in your busy function then continue. Now you'll hit that second high traffic breakpoint in just the right state.
When you’re done with those breakpoints, drag them out to remove them. If you have an operation that’s unexpectedly failing with a thrown Swift error, it can be difficult to find where the error is originating from. You can stop at the throw, instead of the catch, by adding a “Swift Error Breakpoint” in the breakpoints navigator. Your app will stop immediately right where any error is thrown. If your application is regularly throwing and catching expected errors, this breakpoint might be a little too active. Consider using the breakpoint enabling technique from earlier to scope the Swift error breakpoint. Sometimes one of those busy functions already has enough info to filter the interesting cases from the noise. For example, I know this function only fails for a specific connection type. I can focus my breakpoint on just that case by double-clicking the breakpoint to edit it, and adding a condition for that connection type. Now the breakpoint will only stop when that condition is true. Other times you may just want to debug with logging, but don't want to stop, insert log statements, and rebuild to figure it out. Well, the breakpoint editor also lets you add debugger expressions to run when the breakpoint is reached. Add a print expression, and set the breakpoint to automatically continue. Now you can have temporary logs without rebuilding.
Have you ever spent several minutes setting up a debugging scenario, only to take one step too far, and have to set it up all over again? Let me show you how to use those same debugger commands we used for logging to see why your program did what it did. We’ll get to the root cause without starting over. For example, I expected my function to execute when I stepped over this guard clause, but it didn't, and I don't know why. I can retroactively pick the expression apart, evaluating parts of it in the debugger with commands like "p session" or even evaluate the guard conditional to find the cause of the unexpected return. You can even use the debugger as a crystal ball, just do this before stepping to predict the future and decide if you want to step-in or step-over.
Did you know that you can even drag the green program counter backwards? It will attempt to re-execute your previous expressions again. Side effects won't be rewound, so your program may be in an odd state, but if your other choice is to click stop, and restart the debugging session, why not? This could save quite a bit of time.
And finally, if you’re in the middle of debugging an issue with a bunch of breakpoints set but want to get back to the beginning without interruptions, disable all of your breakpoints with the breakpoint button in the debug bar, continue, turn them back on, and re-trigger the problem. Sometimes you need to debug the same thing twice, three times, or who am I kidding, 30 times. If your program is crashing, you have to relaunch it each time you walk through the crash diagnosing it. This can be frustrating, especially when you have to wait just a few more seconds to build and run your application that you didn’t even change. Use Command-Control-R to “run without building,” skipping the whole build step and getting immediately back to debugging. You can even use this if you’ve started changing your app, but want to debug the old code one more time, and haven’t rebuilt since your last session. When you’re deep in a debugging session, or debugging unfamiliar code, it can be difficult to get your bearings. In Xcode 16, you can see full backtraces in the editor, bringing together functions from all over your project into a single editor. This gives you a great overview of how you got to where you are. You can activate this viewing mode in the debug bar, next to the controls for the memory and view debuggers. Are you using "print" statements for debugging today? They’re pretty handy and I use them a lot too. But they can quickly get unwieldy, especially if you have to share the debug console output with your whole team. You may notice that I used some macros in my print statements. There are several of these that you can use such as fileID to get a shortened file path or function to get the name of the function you’re in. Check out the documentation for more macros. But let’s clean that up. Instead of print statements, consider switching to `os_log` which gives you a debug level for each message that you set. And then you don’t need those macros anymore. And then when you run, you can filter by searching for text or filter for just the messages from your library. You can enable metadata to show the type such as error, info, or debug, a timestamp, and the library. And the reason we don’t those macros in the log messages anymore is because you can just jump right to the line in your source where the log message is coming from by clicking the go-to arrow. You can’t do that with just a print statement! To learn more about debugging, check out this year’s "Run, Break, Inspect" session. To dive deeper into the cool features you can do with the Console and all of the metadata that comes with os_log, check out "Debug with structured logging". And to learn more about the debugger, watch "Debug Swift debugging with LLDB".
Now that we’ve removed the bugs, let’s talk about testing.
Testing is important because it catches bugs before shipping. And when you do find bugs, adding test cases prevents them from coming back. Especially as your code base becomes larger and more involved, testing is critical to allow you to do the fun part — writing code.
If I want to run all of the tests in my project, I use Command-U.
But I typically want to quickly iterate on just one test at a time. To do that, click on the diamond for that test function or anywhere up the hierarchy to do a collection of tests.
That was just the beginning of what you can do with tests. Let’s dive in to some more techniques for working efficiently with tests. For Xcode itself, we have great CI coverage. Every pull request is run through Xcode Cloud which ensures our tests are passing prior to getting integrated. The test navigator, Command-6, shows all of my tests. If I have multiple test plans for different portions of my app, I can show just the tests in the current plan by filtering to “only included tests.” Depending on the size of my test suite, this may be enough for me to scroll around and run the right tests. And if you have a lot of great, well-written tests, like we all do you’ll want even more focus. If you can identify the relevant tests by title, you can filter textually. Or with Swift Testing, you can filter by tag. Once you’ve identified the right subset, just select them and use the context menu to run the focused set. After running these tests, they’ll have status. If I just want to focus on the failures, there’s a filter for that too. As I fix the tests, they’ll drop off the list automatically which is nice and rewarding. Just like with debugging my app, I often need to run my test a second, or 30th time (there is no test that defeats us 31 times!) I might have left the original test method a while ago and be several searches or debugging stacks deep into the core of my application. Instead of going back to find that test diamond to re-run the test, you can always run the previous tests again with Command-Control-Option-G.
And just like with running, I’m often restarting the debugging sessions without changing the code, just to get a second perspective on the problem. You can use “test without building” or Command-Control-U.
Besides the soothing green check, and the well meaning but off-putting red X, there are several other test statuses that you might see in Xcode. I want to tell you how to use them.
Sometimes it isn’t possible to fix a test. If you’re a fan of test-driven development, you may have written the tests before the API is functional. You can mark these tests as expected failures. They'll show up as a gray icon with an X.
Sometimes you’ve introduced a regression, and you’re just going to live with it for a bit even though your project manager like me tells you not to. You can mark these tests as skipped. They’ll be represented as a grey icon with an arrow.
For tests that live in suites, you’ll see icons for the aggregate status when there are mixed outcomes.
When you’re ready to get everything back to green, just use the navigator filters for these statuses to focus on the right tests, use the debugging techniques we discussed earlier, and get everything ready to ship.
I showed you how to run an individual test already, but there are many other ways to run tests as well. If you have a test that usually passes, but sometimes fails, you may have a race condition, or some other non-deterministic behavior. Use “Run test Repeatedly” in the context menu. You can run the test a fixed number of times, or repeatedly until failure. Consider adding logging to your test, so that when it does finally fail, you can look through the log to see why.
You can run your tests from the command line with "xcodebuild test". Just specify a scheme, a test plan, or an individual test. This works great with tools like git bisect to find out when a regression was introduced.
When you’re ready to run your tests in a Continuous Integration environment, to get that workload off your Mac, your developer account comes with Xcode Cloud, bundled for free with 25 compute hours per month.
You can configure workflows to start testing in the cloud as soon as you push to a certain branch, view the results right inside Xcode, and even configure it to submit directly to TestFlight or App Store Connect when your tests pass.
Xcode Cloud is also secure and private, data is encrypted at rest and access is protected by two-factor authentication. Your source code is only used when building and the build environment is destroyed as soon as your build completes. To get started with Xcode Cloud, check out “Create practical workflows in Xcode Cloud.” Then watch "Get the most out of Xcode Cloud" for more advanced tips. To learn how to write better tests, check out "Author fast and reliable tests for Xcode Cloud".
Test Plans are how we create groupings of tests to run just the ones we want, when we want. As your project grows, you may want to run tests across several different schemes, or create different groupings of tests to run just your quick unit tests, then another to test everything before committing. This is where Test Plans come into play. Each scheme can have multiple test plans and a test plan can span multiple schemes.
New projects come with a test plan already created for you. To start, edit this test plan from the Product > Test Plan menu. First, choose which targets to include, such as both our unit tests and UI tests. Then select which tests from those targets to include in this test plan.
You can also add that test plan to multiple schemes. First, select the scheme. And then click the plus button and "Add existing Test Plan…". To run your test plan, select it from Product > Test Plan then choose Product > Test or use Command-U to run it.
Code Coverage, or Test Coverage, is a way for you to determine how much of your code base is executed when running your tests.
Writing tests is important for finding issues so you can turn on coverage to estimate test effectiveness, and see if your existing tests cover your new code.
To get started, enable Code Coverage from the Editor menu. And then run your tests. This is how Xcode can determine how much coverage you have so you won’t have results until your first run.
Once tests are run, you’ll see a number on the right side of the editor. This is the number of times that block of code was executed while testing. A 0 means that code was never executed so either you have a gap in your testing, or possibly that your app never executes that code at all.
This block of code was executed 5 times while testing.
You can also see an overview of all of your code coverage through the Report Navigator — Command-9.
This shows you, file-by-file, and function-by-function how much code coverage you have so you can target improvements. That tab where you saw the Code Coverage is called the Test Report. There is a depth of data that’s in there about how your tests are doing and what went wrong when they fail. Let’s click on the test run that we just did to see a summary of what was tested, or click the Tests item to see just a result of the tests. From either this view or the summary view, double-click on a failed test to see what happened.
You’ll see the sequence of events for how the test was executed side-by-side with a screen recording. This is super useful to see the state of your app while it’s being tested so you can pinpoint exactly what went wrong. And in the timeline at the bottom, you can see exactly when the failure occurred. There are so many great sessions on testing in the Developer app. The first place I would start is the "Testing your apps in Xcode" documentation and then check out this year’s talk "Meet Swift Testing".
All right, now that you’ve written your app, gotten all of the bugs out, and tested everything, it’s time to show your creation to the world, or at least your beta testers. We’ve got a product launch coming up so it’s time to christen our 1.0 build.
I’m going to show you how to use TestFlight to distribute your app to beta testers, archive your build products, and then use the built-in Xcode Organizer to gain more insight into your app.
Before you release your app to everyone, you want to take your testing to the next level by having beta testers live on your app and provide feedback. Even with the best tests, there is no substitute for real world usage.
Your paid developer account includes TestFlight which allows you to distribute your app to up to 10,000 beta testers. You can either invite them by email or by publishing a link on social media. Beta testers get new versions of your app automatically on their device. TestFlight also works with all platforms.
When you publish a new build, TestFlight includes release notes so that your testers know what to focus on. And then they can provide feedback and analytics to you, which is all built right into Xcode. Publishing your app to either TestFlight or the App Store begins with an archive. An archive is a snapshot of your compiled app, containing a release build. That release version is optimized to save space so it no longer includes the debug information you would need to look into issues. But the debug symbols are also included in that archive so if you save them, you can debug that version later.
This also allows you to distribute your app by repackaging the contents for whichever destination you choose.
When you’re ready to ship your build, go to the Product menu and choose Archive. Xcode will build your app one more time, and then bundle it up for you and show you the results in the Organizer.
Whenever you get to a good working state, you probably want to not only commit your changes, but you may want to create an archive so you have that working app in a state where you could easily install it on your device or distribute it. To do so, select your archive and then click the Distribute App button.
You’ll be presented with several presets for distributing your app. The “App Store Connect” option uploads your app to either TestFlight or App Store Connect.
“TestFlight Internal Only” skips App Review and includes protections so that you cannot accidentally submit it to the App Store.
This can only be used for beta testers in your content provider/organization and not external testers.
The “Release Testing,” “Enterprise,” and “Debugging” options all produce optimized builds to be installed by users with devices registered on your portal.
Xcode Cloud integrates with TestFlight too. You can setup a workflow so when your build is successful and your tests pass, it submits directly to TestFlight so your testers can get that latest build.
You can even configure scripts to automate your tester notes by pulling from your git commit messages.
Learn more about how to use TestFlight in the "Get started with TestFlight" Tech Talk video. And read how to automate those tester release notes in the article, "Including notes for testers with a beta release of your app".
Unlock the power of Xcode Cloud with "Simplify distribution in Xcode and Xcode Cloud" from last year.
Now that our build has been submitted to TestFlight and testers are running the latest beta version, and I have users on the latest release version, let’s dive deeper into the Xcode Organizer.
This Organizer — Command-Option-Shift-O, is where you’ll get access to a wealth of analytics, in a way that preserves the user’s privacy, built right into Xcode.
Only users who consent to sharing feedback and diagnostics with third party developers like you will be reported here, but you can generally expect that they are a representative sample of your users. After you ship to either TestFlight or the App Store, this window is where you’ll find the next thing that you’re going to edit, debug, test, and commit.
That iteration is us as software engineers doing our job to make sure our customers have the best experience with your app, and it keeps us, project managers, employed too, as there is never a shortage of features to track, bugs to fix, and regressions to resolve.
Let’s first check out the Feedback tab to find what our next great feature should be because features are the most fun thing to work on. This is where your TestFlight beta testers can share feedback with you. And we want to listen to our user’s feedback. I guess we should get started on some of these features right away, so let’s add that to our next sprint. But first — and here’s that real project manager in me coming out, we have some quality issues to address.
In the launches organizer, I can see that the released version of my app is taking a long time to launch. Maybe that feature we rushed to production where we refresh data from the cloud synchronously at app launch could have been written in a more efficient way if only we had more time.
And we’ve got a pretty huge spike in terminations with the latest beta version. I guess we’re going to have to defer those features to a future sprint so we can address these issues first.
As you’re looking at this data, remember that your user’s environment may be different than yours.
You probably have the latest OS version on your device but your users might not, or there could be network availability issues in the real world that you don’t see when you’re testing at your desk. This is the whole reason we have a beta program to collect more data about how our app is doing.
As a developer or as a project manager or as a product marketing partner, you have free access to all of this information built right into Xcode to help drive your product’s directions.
Today we covered so many details about navigating, tricks to make editing faster, and organizing your code.
I showed you tips for using breakpoints to debug tricky situations and how to use the console as your own personal crystal ball.
We went over how testing catches bugs before they ship. With those tests, the Test Report shows you a history of what your tests did.
Then we talked about using TestFlight to distribute your app to beta testers. And how to review feedback in the organizer.
Cheech and I hope you learned something about how to optimize your edit, debug, test, and commit workflows. Thank you for joining us. Now let’s all go fix some bugs!
-
-
10:26 - Warning and error annotations
#warning("This is a warning annotation") #error("This is an error annotation")
-
10:58 - Mark comments
// MARK: This is a section title
-
14:09 - Placeholder
<#placeholder#>
-
17:30 - showStarView()
showStarView()
-
17:51 - Breakpoint #1
let task = URLSession.shared.dataTask(with: cloudURL, completionHandler: handleUpdatesFromCloud)
-
17:53 - Breakpoint #2
videos = loadVideosFromCloud()
-
18:17 - Swift error breakpoint
let url = try! getVideoResourceFilePath()
-
18:34 - Swift error throw
throw URLLoadError.fileNotFound
-
18:59 - Conditional breakpoint
cloudURL.scheme == "https"
-
19:18 - Print statement in conditional breakpoint
p "Username is \(cloudURL.user())"
-
19:44 - guard clause
guard cloudURLs.allSatisfy({ $0. scheme == "https" }), session.configuration.networkServiceType == .video else { return }
-
19:56 - p session
p session
-
19:58 - p first part of guard clause
cloudURLs.allSatisfy({ $0. scheme == "https" })
-
20:02 - p second part of guard clause
p session.configuration.networkServiceType == .video
-
20:11 - Random star rating
var starRating: Int { let randomStarRating = Int.random(n: 1..<5) return randomStarRating }
-
21:16 - Converting starRatingPercentage to Int
var starRating: Int { return Int((starRatingPercentage * 5).rounded()) }
-
21:46 - print statements for debugging
var releaseDate: Date { print("🎬 Entering func \(#function) in \(#fileID)...") let currentDate = Date() let gregorianCal = Calendar(identifier: .gregorian) var components = DateComponents() components.year = releaseYear print("\(#fileID)@\(#line) \(#function): 📅 releaseYear is \(releaseYear)") if releaseYear == gregorianCal.component(.year, from: currentDate) { components.month = Int(releaseMonth) isNewRelease = true print("\(#fileID)@\(#line) \(#function): 🆕 this is a new release!") } if releaseYear < 2000 { isClassicMovie = true print("\(#fileID)@\(#line) \(#function): 🎻 this one is a classic!") } let calendar = Calendar(identifier: .gregorian) return calendar.date(from: components)! }
-
22:09 - os_log statements for debugging
var releaseDate: Date { os_log(.debug, "🎬 Entering func \(#function) in \(#file)...") let currentDate = Date() let gregorianCal = Calendar(identifier: .gregorian) var components = DateComponents() components.year = releaseYear os_log(.info, "📅 releaseYear is \(releaseYear)") if releaseYear == gregorianCal.component(.year, from: currentDate) { components.month = Int(releaseMonth) isNewRelease = true os_log(.info, "🆕 this is a new release!") } if releaseYear < 2000 { isClassicMovie = true os_log(.info, "🎻 this one is a classic!") } let calendar = Calendar(identifier: .gregorian) return calendar.date(from: components)! }
-
23:19 - Sample unit tests
import Testing @testable import Destination_Video struct DestinationVideo_UnitTests { private var library = VideoLibrary() // Make sure starRating is returning a percentage @Test func testStarRating() async throws { for video in library.videos { #expect(video.info.starRating > 0) #expect(video.info.starRating <= 5) } } // Make sure the library loads data from the json file @Test func testLibraryLoaded() async throws { #expect(library.videos.count > 1) } }
-
24:15 - Sample UI tests
import XCTest final class Destination_VideoUITests: XCTestCase { private var app: XCUIApplication! @MainActor override func setUpWithError() throws { // UI tests must launch the application that they test. app = XCUIApplication() app.launch() // In UI tests it is usually best to stop immediately when a failure occurs. continueAfterFailure = false } @MainActor func testABeach() throws { // Tap the button to load the detail view for the "A Beach" video let aBeachButton = app.buttons["A Beach"].firstMatch aBeachButton.tap() // Make sure it has a Play Video button after going to that view let playButton = app.buttons["Play Video"] XCTAssert(playButton.exists) // Make sure the star rating for this video contains 4 stars to avoid issue we saw previously where it was only a single star because starRating was incorrectly a percentage instead of an Int let theRatingView = app.staticTexts["TheRating"] XCTAssert(theRatingView.label.contains("⭐️⭐️⭐️⭐️⭐️")) } @MainActor func testMainView() throws { // We should have at least 10 buttons for the various videos let buttons = app.buttons XCTAssert(buttons.count >= 10) // Check that the most popular videos have buttons for them for expectedVideo in ["By the Lake", "Camping in the Woods", "Ocean Breeze"] { XCTAssert(app.buttons[expectedVideo].exists) } } @MainActor func testLaunchPerformance() throws { if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { // This measures how long it takes to launch your application. measure(metrics: [XCTApplicationLaunchMetric()]) { XCUIApplication().launch() } } } }
-
24:19 - Swift Testing tags
@Test(.tags(.stars)) func testStarRating() async throws { for video in library.videos { #expect(video.info.starRating > 0) #expect(video.info.starRating <= 5) } } @Test(.tags(.library)) func testLibraryLoaded() async throws { #expect(library.videos.count > 1) } extension Tag { @Tag static var stars: Tag @Tag static var library: Tag }
-
26:35 - Running xcodebuild test from the command line
xcodebuild test -scheme DestinationVideo xcodebuild test -scheme DestinationVideo -testPlan TestAllTheThings xcodebuild test -scheme DestinationVideo -testPlan TestAllTheThings -only-testing "Destination VideoUITests/testABeach"
-
29:03 - Missing Code Coverage
func toggleUpNextState(for video: Video) { if !upNext.contains(video) { // Insert the video at the beginning of the list. upNext.insert(video, at: 0) } else { // Remove the entry with the matching identifier. upNext.removeAll(where: { $0.id == video.id }) } // Persist the Up Next state to disk. saveUpNext() }
-
29:19 - Code Coverage executed 5 times
init() { // Load all videos available in the library. videos = loadVideos() // The first time the app launches, set the last three videos as the default Up Next items. upNext = loadUpNextVideos(default: Array(videos.suffix(3))) }
-
-
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.