Chapter 9. Life Cycle of a Project

This chapter surveys some of the main stages in the life cycle of an Xcode project, from inception to submission at the App Store. The survey will provide an opportunity to discuss some additional features of the Xcode development environment as well as various tasks you’ll typically perform as you work on your app, including editing, debugging, and testing your code, running your app on a device, profiling, localization, and releasing to the public.

Environmental Dependencies

It may be useful to have your app behave differently depending on the environment in which it finds itself when compiling or running:

Compile-time dependencies

These are choices made at compilation time; the compiler can substitute different code, depending on the target environment while building. Typical dependencies are:

  • The version of Swift under which we’re compiling.

  • The type of destination for which we’re compiling — a simulator or a real device.

  • A custom compilation condition defined for the current build configuration in the build settings.

Runtime dependencies

These are choices made depending on what the app discovers its environment to be when it runs. Typical dependencies are the type of device we turn out to be running on (iPad vs. iPhone) and the system version installed on this device:

  • Should the app be permitted to run under this environment?

  • Should the app do different things, or load different resources, depending on the environment?

  • Should the app respond to the presence of an argument or environment variable injected by Xcode?

Conditional Compilation

Stretches of code that might or might not be compiled into your built app depending on the compile-time environment are fenced off by #if...#endif directives, as shown in Example 9-1.

Example 9-1. Swift conditional compilation
#if condition
    statements
#elseif condition
    statements
#else
    statements
#endif

The #elseif and #else sections can be omitted, and there can be multiple #elseif sections. There are no curly braces, and the statements will not in fact be indented by the code editor.

The conditions are treated as Bools, so they can be combined with the usual Boolean logic operators. However, they are not Swift code expressions! They must come from a limited predefined set of reserved words. The ones you are most likely to use are:

swift(>=5.1), compiler(>=5.1) (or some other version number)

The version of the Swift language or compiler under which we’re building. The only legal operators are < and >=. The swift version depends on the Swift Language Version build setting, but the compiler version depends on the Xcode version. Under Xcode 11, if the Swift Language Version is 4.2, then swift is less than 5, but compiler is not.

targetEnvironment(simulator)

Whether we’re building for a simulator or device destination. You might use this to allow an app to be tested coherently on the simulator even though the simulator lacks certain capabilities, such as the camera. I also use it to load test data on the simulator.

canImport(UIKit) (or some other module name)

Whether the module in question is available on the platform for which we’re building.

Custom compilation condition

A name that you enter in the Active Compilation Conditions build setting will yield true for purposes of conditional compilation.

The statements enclosed in each block will not be compiled at all unless the appropriate condition is met, as this (very silly) example demonstrates:

#if swift(>=5.1)
print("howdy")
#else
Hey! Ba-Ba-Re-Bop
#endif

That code compiles without complaint in a plain vanilla project built from the Single View App template in Xcode 11. The statement Hey! Ba-Ba-Re-Bop is not a legal Swift expression, but the compiler doesn’t care, because our Swift version is Swift 5.1, and the compiler never looks into the #else block.

An #if condition can distinguish between build configurations by way of the Active Compilation Conditions build setting. In fact, your project already comes with one such condition by default: the DEBUG condition is defined for the Debug configuration but not for the Release configuration (Figure 9-1). This means that for a Debug build, but not for a Release build, the test #if DEBUG will succeed.

ios12 0901b
Figure 9-1. Compilation conditions in the build settings

Permissible Runtime Environment

Under what environments should this app be permitted to run? The choices are determined by build settings, but you can configure them through a more convenient interface:

Device Type

The device type(s) on which your app will run natively. This is the app target’s Targeted Device Family build setting; to set, edit the app target, switch to the General pane, and use the Device checkboxes (under Deployment Info):

iPhone

The app will run on an iPhone or iPod touch. It can also run on an iPad, but not as a native iPad app; it runs in a reduced enlargeable window (Apple sometimes refers to this as “compatibility mode”).

iPad

The app will run only on an iPad.

Both

The app will run natively on both kinds of device; it is a universal app (and the Targeted Device Family setting will be Universal).

iOS Deployment Target

The earliest system your app can run on: in Xcode 11, this can be any major iOS system as far back as iOS 8.0. To set, edit the app target, switch to the General pane, and choose from the Target pop-up menu (under Deployment Info). There is also a drop-down list when you edit the project, in the Info pane.

Backward Compatibility

Writing an app whose iOS Deployment Target version is lower than the current version — that is, an app that is backward compatible to an earlier system — can be challenging. With each new system, Apple adds new features. You’ll want to take advantage of these. But your app will crash if execution encounters features not supported by the system on which it is actually running!

Fortunately, when the compiler knows that a feature is unsupported by an earlier system, it will prevent you from accidentally using that feature on that system. Here’s a line of code where we prepare to draw a small image:

let r = UIGraphicsImageRenderer(size:CGSize(width:10,height:10))

The UIGraphicsImageRenderer class exists only in iOS 10.0 and later. If your deployment target is earlier than iOS 10.0, the compiler will stop you with an error: “UIGraphicsImageRenderer is only available on iOS 10.0 or newer.” You cannot proceed until you guarantee to the compiler that this code will run only on iOS 10 or later. And Xcode’s Fix-it feature (discussed later in this chapter) will show you how to do that, by surrounding that line with an availability check:

if #available(iOS 10.0, *) {
    let r = UIGraphicsImageRenderer(size:CGSize(width:10,height:10))
} else {
    // Fallback on earlier versions
}

The if #available condition tests the current system at runtime against a set of requirements matching the actual availability of a feature as specified in its declaration. The UIGraphicsImageRenderer class declaration is preceded (in Swift) with this annotation:

@available(iOS 10.0, *)

The detailed meaning of that annotation isn’t important (if you’re interested, consult the Attributes chapter of https://docs.swift.org/swift-book/ReferenceManual/Attributes.html). The important thing is that your #available condition should match that annotation, and Xcode’s Fix-it will make sure that it does. You can use #available in an if construct or a guard construct.

You can annotate your own type and member declarations with an @available attribute, and your own code will then have to use an availability check. If your method is declared @available(iOS 13.0, *), then you can’t call that method, when the deployment target is earlier than iOS 13, without an availability check that matches it: if #available(iOS 13.0, *). Within such a method, you don’t need that availability check, because you’ve already guaranteed that this method won’t run on a system earlier than iOS 13.

New in Xcode 11, the app template itself not backward compatible, because it uses a scene delegate (Chapter 6). Scene delegates and the related classes are new in iOS 13; they don’t exist in iOS 12 and before, and the launch process is different. To make a project generated from an app template backward compatible, you need to mark the entire SceneDelegate class, and any methods in the AppDelegate class that refer to UISceneSession, as @available(iOS 13.0, *). You also need to declare a window property in the AppDelegate class:

var window : UIWindow?

The result is that when this app runs in iOS 13, the scene delegate has the window, but when it runs in iOS 12 or before, the app delegate has the window — and your other code may then need to take account of that in order to be backward compatible.

A more insidious problem arises when the very same method or property behaves differently on different systems. Often this is because Apple introduces a bug, or fixes a bug, or both. For example, setting UIProgressView’s progressImage property worked in iOS 7.0, didn’t work at all from iOS 7.1 through iOS 8.4, and then started working again in iOS 9 and later. When that sort of thing happens, you usually have no way of knowing about it other than trial and error, and working your way around the problem coherently can be tricky.

To test your app on an earlier system, you’ll need a destination that runs that earlier system. You can download an earlier Simulator SDK going back as far as iOS 10.3.1 through Xcode’s Components preference pane (see Chapter 6). To test on an earlier system than that, you’ll need an older version of Xcode, and probably an older device. This can be difficult to configure, and may not be worth the trouble.

Device Type

An app might need to respond differently depending on the hardware on which it finds itself running. A universal app might need to behave differently depending on whether it is running on an iPad or an iPhone, different devices have different screen resolutions that might call for using different images, and so on.

You can learn in code whether we’re running on an iPhone or an iPad. The current UIDevice (UIDevice.current), or the traitCollection of any UIViewController or UIView in the hierarchy, will tell you the current device’s type as its userInterfaceIdiom, which will be a UIUserInterfaceIdiom, either .phone or .pad.

When it comes to loading resources, there are some built-in shortcuts. Image files to be loaded from the top level of the app bundle can be distinguished automatically by using the same name but different suffixes, such as @2x and @3x to indicate the screen resolution, or ~iphone and ~ipad to indicate the device type. Or you can use an asset catalog (see “Resources in an asset catalog”), which allows you to specify different images for different runtime environments just by putting them in the correct slot. Either way, the runtime will automatically choose the image variant appropriate to the current environment.

Certain Info.plist settings come with name suffixes as well. For example, it is usual for a universal app to adopt one set of possible orientations on iPhone and another set on iPad: typically, the iPhone version permits a limited set of orientations while the iPad version permits all orientations. You configure this using two groups of “Supported interface orientations” settings in the Info.plist:

UISupportedInterfaceOrientations

A general set of orientations.

UISupportedInterfaceOrientations~ipad

An iPad-only set that overrides the general set when the app runs on an iPad.

The clearest and most reliable way to make these configurations is to edit the Info.plist directly. Alternatively, there are some checkboxes you can use, in the General pane when you edit the target, under Deployment Info (these constitute one of Xcode’s least intuitive bits of interface):

  1. Uncheck iPad, check iPhone, and check the desired Device Orientation checkboxes for the iPhone.

  2. Uncheck iPhone, check iPad, and check the desired Device Orientation checkboxes for the iPad.

  3. Check both iPad and iPhone. Even though you’re now seeing just one set of orientations, both sets are remembered.

Similarly, your app can load different nib files and display different interfaces depending on the device type. If the nib comes from a .xib file, use the image file naming convention: a nib file by the same name with ~ipad appended will load automatically if we are running on an iPad. If you want to have two different main storyboards, use the Info.plist naming convention: configure two “Main storyboard file base name” keys, UIMainStoryboardFile and UIMainStoryboardFile~ipad — or, to use window scenes under iOS 13, configure two “Application Scene Manifest” keys, UIApplicationSceneManifest and UIApplicationSceneManifest~ipad.

Arguments and Environment Variables

You can inject key–value pairs into the environment, making them available to your code, when running the app from Xcode. Edit the scheme and go to the Arguments tab of the Run action. There are two categories (Figure 9-2); to add a key–value pair, click the Plus button under the desired category and enter a name and value:

ios12 0901
Figure 9-2. The Arguments tab of the scheme’s Run action
Arguments Passed On Launch

The name of the argument must be preceded by a hyphen, and followed with a space and the value. This allows you to inject key–value pairs into user defaults. If an argument is -TEST1 1, then UserDefaults.standard.integer(forKey: "TEST1") will be 1.

Environment Variables

There’s a Name column and a Value column (which is always a string). To retrieve an environment variable, use the ProcessInfo class. If the name is TEST2 and the value is 2, then you can say:

if let t = ProcessInfo.processInfo.environment["TEST2"], t == "2" {

A configured pair can be toggled on or off for future builds by clicking the checkbox to its left. These arguments and environment variables are present only when you build and run from Xcode; a user who launches your app on a device will be unaffected by them. So you can take advantage of this feature during development (perhaps to inject default data for testing) without worrying that it will leak out into the real world.

Tip

New in Xcode 11, this feature is also available for individual test plan configurations. I’ll discuss test plans later in this chapter.

Version Control

Sooner rather than later in the life of any real app, you should consider putting your project under version control. Version control is a way of taking periodic snapshots (technically called commits) of your project. Its purpose might be:

Security

Version control can store your commits in a repository offsite, so that your code isn’t lost in case of a local computer glitch or some equivalent “hit by a bus” scenario.

Publication

You might want to make your project’s source publicly available through an online site such as GitHub.

Collaboration

Version control affords multiple developers ready, rational access to the same code.

Confidence

Progress on your code may require changes in many files, possibly over many days, before a new feature can be tested. Version control tracks and lists those changes, and if things go badly, helps to pinpoint what’s gone wrong, and lets you withdraw the changes altogether if necessary. You can confidently embark on a programmatic experiment whose result may not be apparent until much later.

Xcode’s version control facilities are geared primarily to git (http://git-scm.com). You can use a different version control system with your projects, but not in an integrated fashion from inside Xcode. Even with git, it is possible to ignore Xcode’s integrated version control and rely on the Terminal command line or a specialized third-party GUI front end such as Sourcetree (http://www.sourcetreeapp.com). In that case, you might turn off Xcode’s version control integration by unchecking Enable Source Control in the Source Control preference pane. If you check Enable Source Control, additional checkboxes spring to life so that you can configure what automatic behaviors you want. In this discussion, I’ll assume that Enable Source Control is checked.

When you create a new project, the Save dialog includes a checkbox that offers to place a git repository into your project folder from the outset. If you have no reason to decide otherwise, I suggest that you check that checkbox! If you don’t, and if you change your mind later and want to add a git repository to an existing project, open the project and choose Source Control → Create Git Repositories.

When you open an existing project, if that project is already managed with git, Xcode detects this and displays version control information in its interface. Files in the Project navigator are marked with their status. You can distinguish modified files (M), new untracked files (?), and new files added to the index (A).

Version control management commands are available in these places:

  • The Source Control menu

  • The file’s contextual menu, in the Source Control submenu (Control-click in the editor, or Control-click the file’s listing in the Project navigator)

  • The change bars in a source editor pane

  • The Source Control navigator (Command-2) and Source Control inspector (Command-Option-4)

  • The Code Review editor (Command-Option-Shift-Return)

  • The History inspector (Command-Option-2); new in Xcode 11

To commit changes for a single file, choose Source Control → Commit [Filename] in the contextual menu for that file; to commit changes for all files, choose Source Control → Commit from the menu bar. These commands summon a comparison view of the changes; each change can be excluded from this commit (or reverted entirely), so related file hunks can be grouped into meaningful commits.

You can discard changes, push, and pull using the Source Control menu. New in Xcode 11, cherry-pick and stashing commands are added. To download a working copy of an existing project from a remote server, choose Source Control → Clone and enter the required information.

Branches, tags, and remotes are handled in the Source Control navigator. Selecting an item here causes relevant information to be displayed in the Source Control inspector; selecting a branch displays its corresponding remote, and selecting a remote displays its URL. Selecting a branch also shows the log of its commits in the editor. The list of commits is filterable through a search field at the top of the editor. Selecting a commit in this list displays in the inspector its branches, its commit message, and its involved files. Double-click a commit to see its involved files and their differences from the previous commit in a comparison view.

Other relevant commands appear in the contextual menu for items in the Source Control navigator. To add a remote, Control-click the Remotes listing. To make a new branch, check out a branch, tag a branch, delete a branch, or merge a branch, Control-click the branch listing.

To see a comparison view for the file currently being edited, display the Code Review editor: choose View → Show Code Review, or click the Code Review button in the project window toolbar. This editor fills the entire editor area, covering all individual editor panes. You can switch target files using the Project navigator or jump bar while the Code Review editor is showing.

In Figure 9-3, I’m using a comparison view to see that in the more recent version of this file (on the left) I’ve changed my titleTextAttributes dictionary (because the Swift language changed). The jump bar at the bottom permits me to view any commit’s version of the current file. In the contextual menu I can choose Copy Source Changes to capture the corresponding diff text (a patch file) on the clipboard.

ios13 0901
Figure 9-3. Version comparison

New in Xcode 11, the History inspector (Command-Option-2) lists the commit log along the current branch for any file, without having to show the comparison view. Click a commit to summon a popover containing the full commit message along with buttons to show the commit and its files, switch to the Code Review editor, or address an email to the commit’s author.

For similar information in a source editor pane, choose Editor → Authors, or choose Authors from the Editor Options pop-up menu at the top right of the pane. The file is divided into hunks corresponding to the individual commits, with the commit author and date listed at the right. Click a commit to summon the popover. Or, to get information on just a given line of code, select it and choose Editor → Show Last Change For Line (or the contextual menu).

If you’ve checked Show Source Control Changes in the Source Control preferences pane, change bars appear in the source editor pane, in the gutter at the left, showing what you’ve changed since the last commit (Figure 9-4). By clicking on a change bar, you can display both the old committed text and the new uncommitted text simultaneously (new in Xcode 11). You can also discard an individual change.

ios13 0901c
Figure 9-4. An uncommitted change marked in the gutter

If you have an account with any of three popular online sites that allow ready management of code submitted through version control — GitHub (https://github.com), Bitbucket (https://bitbucket.org), and GitLab (https://gitlab.com) — you can enter your authentication information into Xcode’s Accounts preference pane. Once you’ve done that:

Create a remote repository

If your project is already under git control locally, switch to the Source Control navigator, Control-click Remotes, and choose Create [Project Name] Remote. A dialog lets you choose a remote site and upload to it.

Clone a remote repository

When you choose Source Control → Clone, your repositories on those sites are listed in the dialog and you can clone one of them directly. Also, when you’re in a web browser looking at a GitHub repository consisting of an Xcode project, if you click the “Clone or download” button, there’s an additional button offering to let you Open in Xcode.

Editing and Navigating Your Code

Many aspects of Xcode’s editing environment can be modified to suit your tastes. Your first step should be to go to Xcode’s Fonts & Colors preference pane and choose a theme and a Source Editor font face and size. Nothing is so important as being able to read and write code comfortably! I like a pleasant monospaced font. SF Mono is included and is the default; I think it’s very nice. Other possibilities might be Menlo or Consolas, or the freeware Inconsolata (http://levien.com/type/myfonts) or Source Code Pro (https://github.com/adobe-fonts/source-code-pro). I also like a largish size (13, 14, or even 16). You get a choice of line spacing (leading) and cursor. You can change the theme and font size on the fly with the Editor → Font Size and Editor → Theme hierarchical menus.

Text Editing Preferences

The exact look and behavior of a source code editor depends upon your settings in the three tabs of Xcode’s Text Editing preference pane — Display, Editing, and Indentation. I like to check just about everything here. Here are some particularly interesting Text Editing settings.

Display

With “Line numbers” checked, line numbers appear in the gutter to the left of source code text. Visible line numbers are useful when debugging.

Code folding lets you collapse the text between matching curly braces. With “Code folding ribbon” checked, code folding bars appear to the left of your code (at the right of the gutter), displaying your code’s hierarchical structure and allowing you to collapse and expand just by clicking the bars. I’m not fond of code folding, and I don’t want to trigger it accidentally, so I leave that checkbox unchecked. If I need code folding, it remains available through the Editor → Code Folding hierarchical menu.

New in Xcode 11, a divider line for a MARK: comment (discussed later in this chapter) can appear in your code, above the comment; check “Mark separators.”

Editing

The first two checkboxes under Editing have to do with autocompletion; I’ll discuss them separately in a moment.

With “Enable type-over completions” checked, Xcode helps balance delimiters. Suppose I intend to make a UIView by calling its initializer init(frame:). I type as far as this:

let v = UIView(fr

Xcode automatically appends the closing right parenthesis, with the insertion point still positioned before it:

let v = UIView(fr)
// I have typed ^

If I finish typing the parameter and then type a right parenthesis, Xcode moves the insertion point through the existing right parenthesis (so that I don’t end up with two adjacent right parentheses):

let v = UIView(frame:r)
//       I have typed ^

With “Enclose selection in matching delimiters” checked, if you select some text and type a left delimiter (such as a quotation mark or a left parenthesis), the selected text is not replaced; rather, it is surrounded with left and right delimiters. I find this natural and convenient.

With Editor Overscroll turned on, Xcode pretends that your file ends with some extra whitespace, so that when you scroll to the end, the last line appears in the middle of the editor pane rather than at the bottom. Since the bottom of the editor pane is usually the bottom of your screen, this let you keep your eyes focused more or less straight ahead as you work; also, you can append lines without the editor constantly scrolling to accommodate them.

Indentation

I like to have just about everything checked under Indentation; I find the way Xcode lays out code to be excellent with these settings. If a line of code isn’t indenting itself correctly, select the problematic area and choose Editor → Structure → Re-Indent (Control-I). New in Xcode 11, pasted code is not indented automatically unless you’ve checked “Re-Indent on paste.”

Multiple Selection

You probably know from experience how to use the mouse and keyboard to select text. In addition to the familiar forms of selection, Xcode lets you set multiple simultaneous selections in your code. With a multiple selection, your edits (including keyboard navigation) are performed at each selection site simultaneously, which is useful when you have many parallel changes to make. Some ways to get a multiple selection are:

  • Option-click and drag to create a rectangular selection.

  • Control-Shift-click to add a selection to the existing selection.

  • Select a symbol and choose Editor → Selection → Select All Symbols; each occurrence of that symbol is selected simultaneously.

  • Select any text and choose Find → Select Next Occurrence.

  • Press Command-F to bring up the search field at the top of the editor, enter a search term, and choose Find → Find and Select Next, or Find → Select All Find Matches.

  • Select a stretch of code consisting of multiple lines, and choose Editor → Selection → Split Selection By Lines.

Autocompletion and Placeholders

As you write code, you’ll want to take advantage of Xcode’s autocompletion feature. Type names and member names can be astonishingly verbose, and whatever reduces your time and effort typing will be a relief. Autocompletion behavior is governed by two checkboxes in the Editing pane of the Text Editing preferences.

If “Suggest completions while typing” is checked, autocompletion is basically on all the time. I don’t like that, so I uncheck it. Instead, I check “Use Escape key to show completion suggestions.” When I want autocompletion to happen, I ask for it manually, by pressing Esc.

Suppose I want my code to create an alert. I type as far as let alert = UIAl and press Esc. A menu pops up, listing completions, including UIAlertController. You can navigate this menu, dismiss it, or accept the selection, using the mouse or the keyboard alone. I like to use the keyboard. I arrow down to UIAlertController and hit Return, and UIAlertController is entered in my code. Now I type a left parenthesis, so that I’ve got UIAlertController(, and again I press Esc. Now the menu pops up listing the initializers appropriate to a UIAlertController (Figure 9-5). The last one is the one I want, so I arrow down to it (or up, because arrowing up from the top jumps to the bottom) and hit Return.

ios13 0902
Figure 9-5. The autocompletion menu

At this point, the template for the method call is entered in my code (I’ve broken it into multiple lines here):

let alert = UIAlertController(
    title: <#T##String?#>,
    message: <#T##String?#>,
    preferredStyle: <#T##UIAlertController.Style#>)

The expressions in <#...#> are placeholders, showing the type of each parameter. They appear in Xcode as cartouche-like “text tokens” to prevent them from being edited accidentally. To navigate between placeholders, press Tab or choose Navigate → Jump to Next Placeholder (Control-/). In this way, you can select each placeholder in turn, typing to replace it with the actual argument you wish to pass and then tabbing to the next placeholder. To convert a placeholder to a normal string without the delimiters, select it and press Return, or double-click it.

When you’re entering a declaration for a method that’s inherited from a superclass or defined in an adopted protocol, you don’t need to type the initial func; just type the first few letters of the method’s name. In my app delegate class I might type:

applic

If I then press Esc, I see a list of methods such as application(_:didFinishLaunchingWithOptions:); these are methods that might be sent to my app delegate (by virtue of its being the app delegate, as discussed in Chapter 11). When I choose one, the entire declaration is filled in for me, including the curly braces:

func application(_ application: UIApplication,
    didFinishLaunchingWithOptions
    launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool {
        <#code#>
}

A placeholder for the code appears between the curly braces, and it is selected, ready for me to start entering the body of the function. If a function needs an override designation, Xcode’s code completion provides it.

What you type in connection with autocompletion doesn’t have to be the literal start of a symbol. In the preceding example, I can get application(_:didFinishLaunchingWithOptions:) to be first in the code completion menu by starting with launch or even appdidf.

Snippets

Autocompletion is supplemented by code snippets. A code snippet is a bit of text with an abbreviation. Code snippets are kept in the Snippets library, which appears when you summon the Library floating window (Command-Shift-L) while editing code. With the Library showing, you can double-click or drag to insert a snippet into your code, but the real point is that a code snippet’s abbreviation is available to code completion, which means you can insert a snippet without showing the library: you type the abbreviation and the snippet’s name is included among the completions.

For example, to enter a class declaration at the top level of a file, I would type class and press Esc to get autocompletion, and choose Class — Subclass. The template for a class declaration appears in my code: the class name and superclass name are placeholders, the curly braces are provided, and the body of the declaration (between the curly braces) is another placeholder.

In the Library, single-click on a snippet to see its details: its expansion, its language, its code completion abbreviation (labeled Completion, new in Xcode 11), and the scope where it applies (labeled Availability). If the details don’t appear, click the Details button at the top right of the Library floating window.

You can add your own snippets to the Snippets library. These will be categorized as user snippets and will appear first. Unlike built-in snippets, user snippets can be edited and deleted.

To create a user snippet, select some text and choose Editor → Create Code Snippet. The Library floating window will appear, with the new snippet’s details ready for editing. Provide a name, a description, and an abbreviation; the Availability pop-up menu lets you narrow the scope in which the snippet will be available through code completion. In the text of the snippet, use the <#...#> construct to form any desired placeholders.

I’ve created an outlet snippet (Chapter 7), with an Availability of Class Implementation, defined like this:

@IBOutlet private var <#name#> : <#type#>!

And I’ve created an action snippet, also with an Availability of Class Implementation, defined like this:

@IBAction private func <#name#> (_ sender: Any) {
    <#code#>
}

My other snippets constitute a personal library of utility functions that I’ve developed. My delay snippet inserts my DispatchQueue.main.asyncAfter wrapper function (see Chapter 11), and has an availability scope of Top Level.

Refactoring and Structure Editing

Refactoring is an intelligent form of code reorganization. To use it, select within your code and then choose from the Editor → Refactor hierarchical menu, or Control-click and choose from the Refactor hierarchical menu in the contextual menu. Here are some of the refactoring commands you’re most likely to use:

Rename

The selected symbol’s declaration and all references to it are changed, throughout your code. This also allows you to change the name of an outlet property or action method without breaking the connection from the nib (Chapter 7).

Extract Method

Creates a new method and moves the selected lines of code into the body of that method, replacing the original lines with a call to that method. The method name and the new call to it are then selected, ready for you to supply a meaningful name.

Extract Variable

Creates a new variable and assigns the selected code expression to that variable, replacing the original expression with a reference to the variable. If the same expression appears multiple times and you choose Extract All Occurrences, they are all replaced with a reference to the variable. The variable name and the new reference(s) to it are then selected, ready for you to supply a meaningful name.

Generate Memberwise Initializer

In a class declaration, when the class name is selected, synthesizes an initializer based on the class’s instance property names. This is a great convenience, because classes, unlike structs, have no implicit memberwise initializer.

Another way to refactor is to Command-click a keyword that introduces curly braces. A popover appears containing menu items that let you edit the structure of your code. The same popover can be summoned by selecting the keyword and then choosing Editor → Selection → Select Structure (Command-Shift-A). Some of the popover menu commands are like the refactoring commands I’ve just listed. Others let you insert templates for standard structural components associated with that keyword. Here are some of the commands that appear, depending on what you Command-clicked:

A class or struct declaration (Command-click class or struct)

Add Method and Add Property.

A method declaration (Command-click func)

Add Parameter and Add Return Type.

An if construct (Command-click if)

Add “else” Statement and Add “else if” Statement.

A switch statement (Command-click switch)

Add “case” Statement and Add “default” Statement.

Fix-it and Live Syntax Checking

Xcode’s Fix-it feature allows the compiler to make and implement positive suggestions on how to avert a problem that has arisen as a warning or compile error. In effect, you’re getting the compiler to edit your code for you.

Here’s an example. Figure 9-6, at the top, shows that I’ve accidentally forgotten the parentheses after a method call. This causes a compile error. But the stop-sign icon next to the error tells me that Fix-it has a suggestion. I click the stop-sign icon, and Figure 9-6, at the bottom, shows what happens: a dialog pops up, not only showing the full error message but also telling me how Fix-it proposes to fix the problem — by inserting the parentheses. If I click the Fix button in the dialog, Xcode does insert the parentheses — and the error vanishes, because the problem is solved.

ios13 0903
Figure 9-6. A compile error with a Fix-it suggestion

Thanks to the intelligence of Fix-it, you can be deliberately lazy and let the compiler do the grunt work. If a switch statement’s tag is an enum and you omit cases, Fix-it will add them. If a type adopts a protocol and fails to implement required members, Fix-it will insert stubs for those members.

Tip

If you’re confident that Fix-it will do the right thing, you can have it implement all suggestions simultaneously: choose Editor → Fix All Issues.

Live syntax checking is like continual compilation, emitting a warning or error even if you don’t actually compile. This feature can be toggled on or off using the “Show live issues” checkbox in Xcode’s General preference pane. I keep it turned off, because I find it intrusive. My code is almost never valid while I’m in the middle of typing, because things are always half-finished; that’s what it means to be typing! Merely typing let and pausing will likely cause the live syntax checker to complain.

Navigation

Developing an Xcode project involves editing code in many files. You’ll need to leap nimbly from file to file, or to see multiple files simultaneously. Fortunately, Xcode provides numerous ways to navigate your code, some of which have been mentioned already in Chapters 6 and 8:

The Project navigator

Lists all your project’s files by name. If you know something about the name of a file, you can find it quickly in the Project navigator by typing into the search field in the filter bar at the bottom of the navigator (Edit → Filter → Filter in Navigator, Command-Option-J). For example, type story to see just your .storyboard files.

The Symbol navigator

If you highlight the first two icons in the filter bar (the first two are blue, the third is dark), the Symbol navigator lists your project’s object types and their members. Click a symbol to navigate to its declaration in the editor. As with the Project navigator, the filter bar’s search field can help get you where you want to go.

The jump bar

Every path component of an editor pane’s jump bar is a menu:

The bottom level

At the jump bar’s bottom level (the rightmost menu, Control-6) is a list of your file’s object and member declarations, in the order in which they appear. Hold Command while choosing the menu to see them in alphabetical order instead. To filter what the menu displays, start typing while the menu is open.

Another useful trick is to inject section titles into this menu; to do so, put a comment in your code whose first word is MARK:, TODO:, or FIXME:, followed by the section title. To make a divider line in the menu, put a hyphen:

// MARK: - View lifecycle
Higher levels

Higher-level path components are hierarchical menus; you can use any of them to work your way down the file hierarchy and reach any file without using the project navigator. These menus can also be filtered.

History

Each editor pane remembers the names of files you’ve edited in it. The Back and Forward triangles are buttons as well as pop-up menus (or choose Navigate → Go Back and Navigate → Go Forward, Command-Control-Left and Command-Control-Right).

Related items

The leftmost button in the jump bar summons the Related Items menu, a hierarchical menu of files related to the current file, such as superclasses and adopted protocols. This list even includes functions that call or are called by the currently selected function.

Editor panes

Using multiple editor panes allows you to work in two places at once.

Tabs and windows

You can also work in two places at once by opening a tab or a separate window.

Jump to definition

Navigate → Jump to Definition (Command-Control-J, Command-Control-click) lets you jump from a selected or clicked symbol in your code to its declaration.

Open quickly

File → Open Quickly (Command-Shift-O) opens a dialog where you can search for a symbol in your code and in the framework headers.

Breakpoints

The Breakpoint navigator lists all breakpoints in your code. Xcode lacks code bookmarks, but you can misuse a breakpoint as a bookmark. Breakpoints are discussed later in this chapter.

Minimap

The minimap, new in Xcode 11, helps you navigate a single large source code file. To see it, choose Editor → Minimap, or use the Editor Options pop-up menu at the top right of the editor pane. The minimap displays the entire file in miniature — too small to read, but displaying the structure of the code, including MARK: comments, along with transients such as warnings and errors, change bars, breakpoints, and Find results. Drag the shaded area to scroll. Hover the mouse to display structure and symbols; hold Command while hovering to display all symbols (Figure 9-7). Click a symbol to navigate to it.

ios13 0903b
Figure 9-7. The minimap

Finding

Finding is a form of navigation. Xcode has both an editor level find (Find → Find, Command-F), using a bar at the top of the editor pane, and a global find (Find → Find in Project, Command-Shift-F), using the Find navigator. You’ll want to configure your search with find options:

Editor level find options

A button at the right end of the search field toggles case-sensitive search; a pop-up menu lets you specify containment, word exact match, word start, word end, or regular expression search.

Global find options

The options appear above and below the search field. Above the search field, you can choose between Text, References (where a symbol is used), Definitions (where a symbol is defined), Regular Expression, and Call Hierarchy (tracing call stacks backward); you can search by word contents, word exact match, word start, or word end. Below the search field, you can toggle case sensitivity, and you can specify a scope determining which files will be searched: click the current scope to see the Search Scopes panel, where you can select a different scope or create a custom scope.

To find and replace:

Editor level find and replace

Next to the magnifying glass icon, click Find and choose Replace to toggle the visibility of the With field. You can perform a Find and then click Replace to replace that instance, or click All to replace all occurrences (hold Option to change it to All in Selection).

Global find and replace

Above the left end of the search bar, click Find and choose Replace. You can replace all occurrences (Replace All), or select particular find results in the Find navigator and replace only those (Replace); you can also delete find results from the Find navigator, to protect them from being affected by Replace All.

Running in the Simulator

When you build and run with a simulator as the destination, you run in the Simulator application. A Simulator window represents a specific type of device. Depending on your app target’s Deployment Target and Targeted Device Family build settings, and on what SDKs you have installed, you may have choices about the device type and system as you specify your destination before running (see Chapter 6).

The Simulator can display multiple windows, representing different devices. You can run different projects simultaneously, in the same Simulator window or different Simulator windows. When you choose from the Simulator’s Hardware → Device hierarchical menu, you switch to the window representing the chosen device, launching that device’s simulator if needed.

A Simulator window can display the bezel surrounding the device’s screen. Choose Window → Show Device Bezels to toggle this feature (for all windows). Displaying the bezel allows you to press hardware buttons (Home button, volume buttons, screen lock button) by clicking the mouse; also, certain gestures, such as swiping from the screen edge, become easier to perform.

A Simulator window can be resized by dragging an edge or corner. You also have a choice of three standard sizes (you might have to uncheck Show Device Bezels to enable them all):

Window → Physical Size

The simulator device screen on your computer monitor is the size of the screen of the physical device (if your monitor is big enough to accommodate it).

Window → Point Accurate

One point on the device screen is one point on the computer monitor. An iPhone 6s screen is 375 points wide, so it occupies 375 points of computer screen width.

Window → Pixel Accurate

One pixel on the device screen is one pixel on the computer monitor. My computer monitor is single-resolution, but an iPhone 6s is double-resolution, so it occupies 750 pixels of computer screen width. If your computer monitor resolution matches the resolution of the device, Pixel Accurate and Point Accurate are the same.

You can interact with the Simulator in some of the same basic ways as you would a device. Using the mouse, you can tap on the device’s screen; hold Option to make the mouse represent two fingers moving symmetrically around their common center, and Option-Shift to represent two fingers moving in parallel. Items in the Hardware menu also let you perform hardware gestures such as rotating the device, shaking it, locking its screen, and clicking the Home button; you can also test your app by simulating certain special situations, such as running low on memory or the arrival of a phone call.

The Debug menu in the Simulator is useful for detecting problems with animations and drawing. Slow Animations, if checked, makes animations unfold in slow motion so that you can see in detail what’s happening. The four menu items whose names begin with Color reveal possible sources of inefficiency in screen drawing.

The Simulator application supports “side-loading” of apps. This means you can get an app onto a simulator without launching it from Xcode. To do so, drag a built .app file from the Finder onto an open simulator window. Various other types of resource, such as images, can be side-loaded as well.

New in Xcode 11, while running your app from Xcode in the Simulator, you can change certain user settings on the simulated device without passing through the Settings app. In the debug bar, click the Environment Overrides button to summon a popover where you can switch between light and dark modes, change the dynamic text size, and alter various accessibility settings.

Debugging

Debugging is the art of figuring out what’s wrong with the behavior of your app as it runs. I divide this art into two main techniques: caveman debugging and pausing your running app.

Caveman Debugging

Caveman debugging consists of altering your code, usually temporarily. Typically, you’ll add code to produce informative messages that you’ll read in the Xcode console in the project window’s Debug pane as your app runs.

The chief Swift command for sending a message to the Xcode console is the print function. You might print a string saying where the path of execution is:

print("view did load")

You might output a value:

print("i is", i)

When you print an object, the output comes from that object’s description property. Cocoa objects generally have a useful built-in description property implementation:

print(self.view)

The output in the console reads something like this (I’ve formatted it for clarity here):

<UIView: 0x79121d40;
  frame = (0 0; 320 480);
  autoresize = RM+BM;
  layer = <CALayer: 0x79121eb0>>

We learn the object’s class, its address in memory (useful for confirming whether two instances are in fact the same instance), and the values of some additional properties. In your own object types, you can adopt CustomStringConvertible and implement the description property as desired (Chapter 4).

Instead of print, you might like to use dump. Its console output describes an object along with its class inheritance and its instance properties, by way of a Mirror object:

dump(self)

If self is a view controller of class ViewController with a didInitialSetup instance property, the console output looks like this:

* ViewController
  - super: UIViewController
    - super: UIResponder
      - super: NSObject
  - didInitialSetup: true

In your own object types, you can adopt CustomReflectable and implement the customMirror property as desired (Chapter 5).

An important feature of print and dump is that they are effectively suppressed when the app is launched independently of Xcode. That’s good, because it means you’re free to pepper your code with print statements and they’ll have no effect on your app in the real world. But what if you want to send yourself messages when you’re running the app independently of Xcode?

If you’re importing Foundation — and in real-life iOS programming, you are — you have access to the NSLog C function. It takes an NSString which operates as a format string, followed by the format arguments. A format string is a string containing symbols called format specifiers, for which values (the format arguments) will be substituted at runtime. See “String Format Specifiers” in Apple’s String Programming Guide in the documentation archive. All format specifiers begin with a percent sign (%), so the only way to enter a literal percent sign in a format string is as a double percent sign (%%). The character(s) following the percent sign specify the type of value that will be supplied at runtime. The most common format specifiers are %@ (an object reference), %d (an int), %ld (a long), and %f (a double):

NSLog("the view: %@", self.view)

In that example, self.view is the first (and only) format argument, so its value will be substituted for the first (and only) format specifier, %@, when the format string is printed in the console:

2015-01-26 10:43:35.314 Empty Window[23702:809945]
  the view: <UIView: 0x7c233b90;
    frame = (0 0; 320 480);
    autoresize = RM+BM;
    layer = <CALayer: 0x7c233d00>>

NSLog is in the process of being superseded by a new unified logging system, OSLog. To use it, import os and create an OSLog object, typically as an instance property or global:

import os
let mylog = OSLog(subsystem: "com.neuburg.matt", category: "testing")

The subsystem and category strings are arbitrary but useful, because you can refer to them to focus on the particular log messages that interest you. To send a log message, call the os_log function; like NSLog, it uses a format string along with format arguments, plus you have to specify an OSLog object:

os_log("%{public}@", log: mylog, "this is a test of os_log")

For more about OSLog and the structure of os_log format strings, see the documentation at https://developer.apple.com/documentation/os/logging. In addition to the extra specifiers such as the subsystem and category, a major reason to prefer os_log over NSLog is that messages from NSLog are truncated at 1024 characters.

Messages from NSLog or os_log work even when the app is running outside of Xcode. To view them, use the Console application. In the Sources pane at the left, under Devices, are shown all running simulators and any devices currently paired with the computer. Select the desired device and exercise the app, while watching the Console output. To eliminate unwanted output, set up a filter in the toolbar search field. You can filter by the name of the process (that is, the name of your app); even better, if you’re using os_log, you can filter by the subsystem and category you configured when creating your OSLog object.

In the Xcode console, NSLog and os_log provide extra information along with each log message: the current time and date, along with the process name, process ID, and thread ID (useful for determining whether two logging statements are called on the same thread). In the Console application, you can see that same information by displaying the Time, Process, and Thread ID columns, plus you can show Category, Subsystem, and Type columns.

Another useful form of caveman debugging is deliberately aborting your app because something has gone seriously wrong. See the discussion of assert, precondition, and fatalError in Chapter 5. precondition and fatalError work even in a Release build. By default, assert is inoperative in a Release build, so it is safe to leave it in your code when your app is ready to ship; by that time, of course, you should be confident that the bad situation your assert was intended to detect has been debugged and will never actually occur.

Purists may scoff at caveman debugging, but I use it heavily: it’s easy, informative, and lightweight. And sometimes it’s the only way. Unlike the debugger, console logging works with any build configuration (Debug or Release) and wherever your app runs (in the Simulator or on a device). It works when pausing is impossible (because of threading issues, for instance). It even works on someone else’s device, such as a tester to whom you’ve distributed your app.

Tip

Swift defines four special literals, particularly useful when logging because they describe their own position in the surrounding file: #file, #line, #column, and #function.

The Xcode Debugger

When Xcode is running your app, you can pause in the debugger and use Xcode’s debugging facilities. The important thing, if you want to use the debugger, is that the app should be built with the Debug build configuration (the default for a scheme’s Run action). The debugger is not very helpful against an app built with the Release build configuration, not least because compiler optimizations can destroy the correspondence between steps in the compiled code and lines in your source code.

Breakpoints

There isn’t a strong difference between running and debugging in Xcode; the main distinction is whether breakpoints are effective or ignored. The effectiveness of breakpoints can be toggled at two levels:

Globally (active vs. inactive)

Breakpoints as a whole are either active or inactive. If breakpoints are inactive, we won’t pause at any breakpoints.

Individually (enabled vs. disabled)

A given breakpoint is either enabled or disabled. Even if breakpoints are active, we won’t pause at this one if it is disabled. Disabling a breakpoint allows you to leave in place a breakpoint that you might need later without pausing at it every time it’s encountered.

To create a breakpoint, select in the editor the line where you want to pause, and choose Debug → Breakpoints → Add/Remove Breakpoint at Current Line (Command-\). This menu item toggles between adding and removing a breakpoint for the current line. Alternatively, a simple click in the gutter adds a breakpoint. The breakpoint is symbolized by an arrow in the gutter (Figure 9-8, first). To remove a breakpoint gesturally, drag the arrow out of the gutter.

ios13 0904
Figure 9-8. A breakpoint

To disable a breakpoint at the current line, click the breakpoint in the gutter to toggle its enabled status. Alternatively, Control-click the breakpoint and choose Disable Breakpoint in the contextual menu. A dark breakpoint is enabled; a light breakpoint is disabled (Figure 9-8, second).

To toggle the active status of breakpoints as a whole, click the Breakpoints button in the debug bar, or choose Debug → Activate/Deactivate Breakpoints (Command-Y). If breakpoints are inactive, they are simply ignored en masse, and no pausing at breakpoints takes place. Breakpoint arrows are blue if breakpoints are active, gray if they are inactive (Figure 9-8, third). The active status of breakpoints as a whole doesn’t affect the enabled or disabled status of any breakpoints.

Once you have some breakpoints in your code, you’ll want to survey and manage them. That’s what the Breakpoint navigator is for. Here you can navigate to a breakpoint, enable or disable a breakpoint by clicking on its arrow in the navigator, and delete a breakpoint.

You can also configure a breakpoint’s behavior. Control-click the breakpoint, in the gutter or in the Breakpoint navigator, and choose Edit Breakpoint; or double-click the breakpoint. You can have a breakpoint pause only under a certain condition or after it has been encountered a certain number of times, and you can have a breakpoint perform one or more actions when it is encountered, such as issuing a debugger command, logging, playing a sound, speaking text, or running a script. A breakpoint whose behavior has been configured is badged (Figure 9-8, fourth).

A breakpoint can be configured to continue automatically after performing its action when it is encountered. A breakpoint that logs and continues can be an excellent alternative to caveman debugging. By definition, such a breakpoint operates only when you’re actively debugging the project; it won’t dump any messages into the console when the app runs independently, because breakpoints exist only in Xcode.

Certain special kinds of breakpoint (event breakpoints) can be created in the Breakpoint navigator — click the Plus button at the bottom of the navigator and choose from its pop-up menu — or by choosing from the Debug → Breakpoints hierarchical menu:

Swift error breakpoint

Pauses when your code says throw.

Exception breakpoint

Pauses when an Objective-C exception is thrown or caught, without regard to whether the exception would crash your app later. An exception breakpoint that pauses on all exceptions when they are thrown gives the best view of the call stack and variable values at the moment of the exception.

Warning

Sometimes Apple’s code will throw an exception and catch it, deliberately. This isn’t a crash, and nothing has gone wrong; but if you’ve created an exception breakpoint, your app will pause at it, which can be confusing. If this happens to you, choose Debug → Continue to resume your app; if it keeps happening, you might need to disable the exception breakpoint.

Symbolic breakpoint

Pauses when a certain method or function is called, regardless of what object called it. The method doesn’t have to be your method! A symbolic breakpoint can help you probe Cocoa’s behavior. A method may be specified in one of two ways:

Using Objective-C method notation

The instance method or class method symbol (- or +) followed by square brackets containing the class name and the method name:

-[UIApplication beginReceivingRemoteControlEvents]
By Objective-C method name

The Objective-C method name alone. The debugger will resolve this for you into all possible class–method pairs, as if you had entered them using the Objective-C notation that I just described:

beginReceivingRemoteControlEvents

If you enter the method specification incorrectly, the symbolic breakpoint won’t do anything; however, you might be assisted by code completion, and in general you’ll know if you got things right, because you’ll see the resolved breakpoint(s) listed hierarchically below yours (though resolution may not take place until you actually run the project).

Runtime Issue breakpoint

New in Xcode 11. Pauses when a runtime issue is encountered. There are four types; for all but the System Frameworks type (useful especially with SwiftUI), you’ll need to enable the corresponding diagnostic in the Scheme editor.

Breakpoints come in three levels of exposure:

Local to you and a project

The default. The breakpoint appears in this project on this machine.

Global to you

Use the contextual menu to say Move Breakpoint To → User. The breakpoint now appears in all your projects on this machine. Symbolic and exception breakpoints are particularly good candidates for this level.

Shared with others

Use the contextual menu to say Share Breakpoint. The breakpoint is now visible in this project to others with whom you share the project.

Paused at a breakpoint

When the app runs with breakpoints active and an enabled breakpoint is encountered (and assuming its conditions are met, and so on), the app pauses. In the active project window, the editor shows the file containing the point of execution, which will usually be the file containing the breakpoint. We are paused at the line that is about to be executed, which is shown by the instruction pointer (Figure 9-9). Depending on the settings for Running → Pauses in the Behaviors preference pane, the Debug navigator and the Debug pane may also appear.

ios13 0906
Figure 9-9. Paused at a breakpoint

Here are some things you might like to do while paused at a breakpoint:

See where you are

One common reason for setting a breakpoint is to make sure that the path of execution is passing through a certain line. Functions listed in the call stack in the Debug navigator with a User icon in a dark blue background are yours; click one to see where you are paused in that function. (Other listings are functions and methods for which you have no source code, so there would be little point clicking one unless you know something about assembly language.) You can also view and navigate the call stack using the jump bar in the debug bar.

Study variable values

In the Debug pane, variable values for the current scope (corresponding to what’s selected in the call stack) are visible in the variables list. You can see additional object features, such as collection elements, properties, and even some private information, by opening triangles.

You can use the search field to filter variables by name or value. If a formatted summary isn’t sufficiently helpful, you can send description (or, if this object adopts CustomDebugStringConvertible, debugDescription) to an object variable and view the output in the console: choose Print Description of [Variable] from the contextual menu, or select the variable and click the Info button below the variables list.

You can also view a variable’s value graphically: select the variable and click the Quick Look button (an eye icon) below the variables list, or press the space bar. For example, in the case of a CGRect, the graphical representation is a correctly proportioned rectangle. You can make instances of your own custom class viewable in the same way; declare the following method and return an instance of one of the permitted types (see Apple’s Quick Look for Custom Types in the Xcode Debugger in the documentation archive):

@objc func debugQuickLookObject() -> Any {
    // ... create and return your graphical object here ...
}

You can also inspect a variable’s value in place in your code, by examining its data tip. To see a data tip, hover the mouse over the name of a variable in your code. The data tip is much like the display of this value in the variables list: there’s a flippy triangle that you can open to see more information, plus an Info button that displays the value description here and in the console, and a Quick Look button for showing a value graphically (Figure 9-10).

ios11 0907
Figure 9-10. A data tip
Set a watchpoint

A watchpoint is like a breakpoint, but instead of depending on a certain line of code it depends on a variable’s value: the debugger pauses whenever the variable’s value changes. You can set a watchpoint only while paused in the debugger. Control-click the variable in the variables list and choose Watch [Variable]. Watchpoints, once created, are listed and managed in the Breakpoint navigator.

Inspect your view hierarchy

You can study the view hierarchy while paused in the debugger. Click the Debug View Hierarchy button in the debug bar, or choose Debug → View Debugging → Capture View Hierarchy. Views are listed in an outline in the Debug navigator. The editor displays your views; this is a three-dimensional projection that you can rotate. The Object inspector and the Size inspector display information about the currently selected view.

Inspect your object graph

Using the View Debugger, you can study the object graph (what objects you’ve created and how they refer to one another) while paused in the debugger. I’ll talk more about that later in this chapter.

Manage expressions

An expression is code to be added to the variables list and evaluated every time we pause. Choose Add Expression from the contextual menu in the variables list. The expression is evaluated within the current context in your code, so be careful of side effects.

Talk to the debugger

You can communicate directly with the debugger through the console. Xcode’s debugger interface is a front end to the real debugger, LLDB (http://lldb.llvm.org); by talking directly to LLDB, you can do everything that you can do through the Xcode debugger interface, and more. Common commands are:

ty loo (short for type lookup)

Followed by a type name, dumps a full declaration for the type, listing all its members (properties and methods). For Cocoa classes, you might get better information by performing the lookup in Objective-C:

$ ty loo -l objc -- ClassName
v (or fr v, short for frame variable)

Alone, prints out all variables locally in scope, similar to the display in the variables list. Alternatively, can be followed by the name of a variable you want to examine. Fast and lightweight because it reaches right into the stack and grabs the value, but it has some limitations; for obvious reasons, it doesn’t work for computed properties.

p (or expression, expr, or simply e)

Compiles and executes, in the current context, any expression in the current language. Be careful of your expression’s side effects! This is more heavyweight than v.

po (meaning “print object”)

Like p, but displays the value of the executed expression in accordance with its description or debugDescription (similar to Print Description). It is actually an alias for expr -O, meaning “object description.” Twice as expensive as p because it has to be compiled and executed twice.

Fiddle with breakpoints

You are free to create, destroy, edit, enable and disable, and otherwise manage breakpoints even while your app is running, which is useful because where you’d like to pause next might depend on what you learn while you’re paused here. Indeed, this is one of the main advantages of breakpoints over caveman debugging. To change your caveman debugging, you have to stop the app, edit it, rebuild it, and start running the app all over again. But to fiddle with breakpoints, you don’t have to be stopped; you don’t even have to be paused! An operation that went wrong, if it doesn’t crash your app, can probably be repeated in real time, so you can just add a breakpoint and try again. If tapping a button produces the wrong results, you can add a breakpoint to the action method and tap the button again; you pass through the same code, and this time you pause and can work out what the trouble is.

Continue or step

To proceed with your paused app, you can either resume running or take one step and pause again. The commands are in the Debug menu, or you can click the convenient buttons in the debug bar:

Continue

Resume running (until a breakpoint is encountered).

Step Over

Pause at the next line.

Step Into

Pause in your function that the current line calls, if there is one; otherwise, pause at the next line.

Step Out

Pause when we return from the current function.

Start over, or abort

To kill the running app, click Stop in the toolbar (Product → Stop, Command-Period). Clicking the Home button in the Simulator (Hardware → Home) or on the device does not stop the running app.

You can make changes to your code while the app is running or paused in the Simulator or on a device, but those changes are not magically communicated to the running app. To see your changes in action, you must stop the running app, build, run, and launch the app all over again.

However, you can inject changes into your code by means of an expr command, given either at the LLDB console or through a custom-configured breakpoint. Moreover, you can skip a line of code by dragging the instruction pointer down; if you combine that with expr, you’ve effectively replaced one line of code with another. So it may be possible to modify your app’s logic and test a proposed change to your code without rebuilding and relaunching.

Warning

Local variable values can exist even if, at the point where you are paused, those variables have not yet been initialized; but such values are meaningless, so ignore them. This applies to the variables list, data tips, and so forth. Forgetting this is a common beginner mistake.

Testing

A test is code that isn’t part of your app target; its purpose is to exercise your app and make sure that it works as expected. Tests can be of two kinds:

Unit tests

A unit test exercises your app internally, from the point of view of its code. A unit test might call some method in your code, handing it various parameters and looking to see if the expected result is returned each time, not just under normal conditions but also when incorrect or extreme inputs are supplied.

Interface (UI) tests

An interface test exercises your app externally, from the point of view of a user. Such a test guides your app through use case scenarios by effectively tapping buttons with a ghost finger, watching to make sure that the interface behaves as expected.

Tests — especially unit tests — should ideally be written and run constantly as you develop your app. It can even be useful to write unit tests before writing the real code, as a way of developing a working algorithm. Having initially ascertained that your code passes your tests, you continue to run those tests to detect whether a bug has been introduced during the course of development.

Tests are bundled in a separate target (see Chapter 6). The application templates give you an opportunity to add test targets at the time you create your project: in the second dialog (“Choose options for your new project”), you can check Include Unit Tests or Include UI Tests, or both. Alternatively, you can easily create a new test target at any time: make a new target and specify iOS → Test → iOS Unit Testing Bundle or iOS UI Testing Bundle.

A test class is a subclass of XCTestCase (which is itself a subclass of XCTest). A test method is an instance method of a test class, returning no value and taking no parameters, whose name starts with test. A test method does not run — indeed, it is not even compiled — until you explicitly ask to run or compile it. The test target depends upon the target to be tested (usually, your app target). This means that before a test class can be compiled and built, the target to be tested must be compiled and built. But building the target to be tested does not build the test target. To build a test target so as to learn whether its code compiles successfully, but without running a test method, choose Product → Build For → Testing.

A test method may call one or more test asserts; in Swift, these are global functions whose names begin with XCTAssert. For a list of these functions, see the XCTest class documentation. Calling a test assert is typically the entire point of writing a unit test; a successful test is one where all asserts succeed.

A test class may contain utility methods that are called by the test methods; their names do not begin with test. In addition, you can override any of four special methods inherited from XCTestCase:

setUp class method

Called once before all test methods in the class.

setUp instance method

Called before each test method.

tearDown instance method

Called after each test method.

tearDown class method

Called once after all test methods in the class.

As an alternative to the tearDown instance method, you can use a teardown block. To do so, call self.addTeardownBlock(_:) with a function (typically an anonymous function) to be called at teardown time; self here is the XCTestCase instance. When the teardown block is called depends on where it is added; if you call addTeardownBlock within a test method, the block is called only on exit from that method, but if you call it in the setUp instance method, the block is called after every test method, because the block was added freshly before every test method.

Running a test also runs the app. The test target’s product is a bundle; a unit test bundle is loaded into the app as it launches, whereas an interface test bundle is loaded into a special test runner app generated for you by Xcode. Resources, such as test data, can be included in the bundle. You might use setUp to load such resources; you can get a reference to the bundle by way of the test class, by saying Bundle(for:Self.self).

Unit Tests

Unit tests need to see into the target to be tested, so the test target must import the target to be tested, as a module. To overcome privacy restrictions, the import statement should be preceded by the @testable attribute; this attribute temporarily changes internal (explicit or implicit) to public throughout the imported module.

As an example of writing and running a unit test method, we can use our Empty Window project. Let’s give the ViewController class a (nonsensical) instance method dogMyCats:

func dogMyCats(_ s:String) -> String {
    return ""
}

The method dogMyCats is supposed to receive any string and return the string "dogs". At the moment, though, it doesn’t; it returns an empty string instead. That’s a bug. Now we’ll write a test method to ferret out this bug.

First, we’ll need a unit test target:

  1. In the Empty Window project, choose File → New → Target and specify iOS → Test → iOS Unit Testing Bundle.

  2. Call the product EmptyWindowTests; observe that the target to be tested is the app target.

  3. Click Finish.

In the Project navigator, a new group has been created, EmptyWindowTests, containing a single test file, EmptyWindowTests.swift. It contains a test class EmptyWindowTests, including stubs for two test methods, testExample and testPerformanceExample. Comment out those two methods. We’re going to replace them with a test method that calls dogMyCats and makes an assertion about the result:

  1. At the top of EmptyWindowTests.swift, where we are importing XCTest, we must also import the target to be tested, which is the app target:

    @testable import Empty_Window
  2. Prepare an instance property in the declaration of the EmptyWindowTests class to store our ViewController instance:

    var viewController = ViewController()
  3. Write the test method. Its name must start with test! Let’s call it testDogMyCats. It has access to the ViewController instance as self.viewController:

    func testDogMyCats() {
        let input = "cats"
        let output = "dogs"
        XCTAssertEqual(output,
            self.viewController.dogMyCats(input),
            "Failed to produce \(output) from \(input)")
    }

We are now ready to run our test. There are many ways to do this. Switch to the Test navigator, and you’ll see that it lists our test target, our test class, and our test method. You can run a test method, or the whole class suite, using the contextual menu or with Run buttons that appear when you hover the mouse over a listing. In addition, in EmptyWindowTests.swift itself, there are diamond-shaped indicators in the gutter to the left of the class declaration and the test method name; when you hover the mouse over one of them, it changes to a Run button. You can click that button to run, respectively, all tests in this class or an individual test. Or, to run all tests, you can choose Product → Test.

Tip

After running a test, to run just that test again, choose Product → Perform Action → Run [Test] Again. To run multiple individual tests, Command-click in the Test navigator to select just those tests; then choose Product → Perform Action → Run [n] Test Methods (or use the contextual menu).

So now let’s run testDogMyCats. The app target is compiled and built; the test target is compiled and built. (If any of those steps fails, we can’t test, and we’ll be back on familiar ground with a compile error or a build error.) The app launches in the Simulator, and the test runs.

The test fails! (Well, we knew that was going to happen, didn’t we?) The error is described in a banner next to the assert that failed in our code; moreover, red X marks appear everywhere — at the top of the project window, in the Test navigator next to testDogMyCats, and in EmptyWindowTests.swift next to the first line of testDogMyCats.

The best place to survey what went wrong is the Report navigator. Initially, you’re shown a summary view. For even more detail, choose Test → Log from the jump bar at the top of the editor (Figure 9-11); by expanding transcripts, you can see the full console output from the test run, including any caveman debugging messages that you may have sent from your test code (to show them, you would click the little horizontal lines icon at the far right).

ios13 0907b
Figure 9-11. The Report navigator describes a test failure

Now let’s fix our code. In ViewController.swift, modify dogMyCats to return "dogs" instead of an empty string. Now run the test again. It passes!

When a test failure occurs, you might like to pause at the point where the assertion is about to fail. To do so, in the Breakpoint navigator, click the Plus button at the bottom and choose Test Failure Breakpoint. This is like an Exception breakpoint, pausing on the assert line in your test method just before it reports failure. You could then switch to the method being tested and debug it, examining its variables and so forth, to work out the reason for the impending failure.

Xcode’s code coverage feature lets you assess how much of your app target’s code is being exercised by your unit tests. To switch it on, edit the Test action in your scheme and check Code Coverage in the Options pane. Run your tests. Afterward, the Report navigator has a Coverage section displaying statistics (Figure 9-12); you can also choose Editor → Code Coverage (or use the Editor Options pop-up menu at the top right of the editor pane) to reveal a gutter at the right of your code calling attention to stretches of code that didn’t run during the tests.

ios12 0907bb
Figure 9-12. The Report navigator displays code coverage statistics

There’s considerably more to learn about unit tests. Asynchronous testing allows a test method to wait for a time-consuming operation to finish. Performance testing lets you check that the speed of an operation has not fallen off by running that operation repeatedly and timing the result; the first time you run a performance test, you establish a baseline measurement, and on subsequent runs, it fails if the standard deviation of the times is too far from the baseline, or if the average time has grown too much. New in Xcode 11, a performance test can exercise launching your app and report whether it launches as quickly as it should.

Interface Tests

Now let’s experiment with interface testing. I’m going to assume that you still have (from Chapter 7) a button in the Empty Window interface with an action connection to a ViewController method that summons an alert. We’ll write a test that taps that button and makes sure that the alert is summoned. Add an iOS UI Testing Bundle to the project; call it EmptyWindowUITests.

Interface test code is based on accessibility, a feature that allows the screen interface to be described verbally and to be manipulated programmatically. It revolves around three classes: XCUIElement, XCUIApplication (an XCUIElement subclass), and XCUIElementQuery. In the long run, it’s best to learn about these classes and write your own UI test code; but to help you get started, accessibility actions are recordable, meaning that you can generate code automatically by performing the actual actions that constitute the test. Let’s try it:

  1. In the testExample stub method, create a new empty line and leave the insertion point within it.

  2. Choose Editor → Start Recording UI Test. (Alternatively, there’s a Record button in the debug bar.) The app launches in the Simulator.

  3. In the Simulator, tap the button in the interface. When the alert appears, tap OK to dismiss it.

  4. Return to Xcode and choose Editor → Stop Recording UI Test. Also choose Product → Stop to stop running in the Simulator.

The following code, or something similar, has been generated:

let app = XCUIApplication()
app.buttons["Hello"].tap()
app.alerts["Howdy!"].buttons["OK"].tap()

The app object, obviously, is an XCUIApplication instance. Properties such as buttons and alerts return XCUIElementQuery objects. Subscripting such an object returns an XCUIElement, which can then be sent action methods such as tap.

Now run the test by clicking in the diamond in the gutter at the left of the testExample declaration. The app launches in the Simulator, and a ghost finger performs the same actions we performed, tapping first the button in the interface and then, when the alert appears, the OK button that dismisses it. The test ends and the app stops running in the simulator. The test passes!

More important, if the interface stops looking and behaving as it does now, the test will not pass. To see this, in Main.storyboard, select the button and, under Control in the Attributes inspector, uncheck Enabled. The button is still there, but it can’t be tapped; we’ve broken the interface. Run the test. The test fails, and the Report navigator explains why (Figure 9-13): when we came to the Tap “OK” Button step, we first had to Find the “OK” Button, and we failed because there was no alert. Ingeniously, the report also supplies lots of information about the view hierarchy, along with a screenshot, so that we can inspect the state of the interface during the test. Under Find the “OK” Button, you can double-click Automatic Screenshot to learn what the screen looked like at that moment: it’s easy to see the disabled interface button (and no alert).

ios13 0907cc
Figure 9-13. The Report navigator displays a failed UI test

Persisting screenshots

Screenshots such as those taken automatically during the UI test I just described can be useful for other purposes. You might like to retain a more permanent record of how your interface looks under various circumstances — as marketing materials, for submission to the App Store, to help with localization, and so on. In fact, you might construct some of your UI tests for no other purpose than to take screenshots for you!

To this end, you can have your UI test deliberately take a screenshot and make it persist. To do so, call the XCUIElement (or XCUIScreen) screenshot method, turn the resulting XCUIScreenshot object into an XCTAttachment, and call the XCTestCase add method to retain the actual screenshot. Along the way, be sure to extend the lifetime of the attachment so that it persists even if the test succeeds; you can also give the screenshot a convenient name:

let screenshot = XCUIApplication().screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.lifetime = .keepAlways
attachment.name = "OpeningScreen"
self.add(attachment)

In the Report navigator, our screenshot is displayed under the name we assigned to it (Figure 9-14).

ios13 0907cccc
Figure 9-14. The Report navigator displays a persisting screenshot

Interface testing and accessibility

During interface testing, your app is in effect being viewed from the outside, as a human being would view it. As I’ve already said, that depends upon accessibility. Standard interface objects are accessible, but other interface that you create might not be. Select an interface element in the nib editor to view its accessibility characteristics in the Identity inspector. Run the app in the Simulator and choose Xcode → Open Developer Tool → Accessibility Inspector to explore in real time the accessibility characteristics of whatever is under the cursor.

Another useful trick is to put a breakpoint in a UI test method, run the test, pause, and tell the debugger to po XCUIApplication() to see the full view hierarchy as accessibility sees it. To see fewer results, form a query specifying the type of entity you’re interested in, such as po XCUIApplication().buttons.

Referring to an interface object by its visible title, as in our code app.buttons["Hello"], is poor practice. If the title changes, or if the app is localized for another language (discussed later in this chapter), the reference breaks. Instead, we should give our button a fixed accessibility identifier, either in code or in the Identity inspector in the nib editor. If the Hello button’s accessibility identifier is GreetingButton, we can refer to it as app.buttons["GreetingButton"] instead.

For more about adding useful accessibility to your interface objects, see Apple’s Accessibility Programming Guide for iOS in the documentation archive.

Test Plans

When you’ve build up several suites of tests, you’ll want a way to configure what tests should be run under what conditions on a given occasion. In the past, such configuration was confined to a scheme. By editing a scheme’s Test action, you could determine the complete set of tests that would run when you chose Product → Test. If you wanted more than one set of tests, you needed multiple schemes.

New in Xcode 11, this scheme-based architecture is superseded by test plans. A test plan is a text file (in JSON format), but you won’t have to deal with it as text; you edit it in a dedicated editor, similar to how an Info.plist is treated. To create a test plan, choose Product → Test Plan → New Test Plan. The test plan should not be part of any target, and you’ll probably put it at the top level of your project. Edit the new test plan. Click the Plus button and select the targets containing the tests you want to use.

In the Tests pane of the test plan, you can specify individual test classes and test methods to be run. In the Configurations pane, you can specify the behavior of your tests, including various choices you would previously have made in the scheme editor and elsewhere: system language, simulated location, screenshot policy, diagnostics, whether to use code coverage, and whether tests should run in random order (which can help unmask hidden dependencies between tests).

You can make configuration choices on two levels, a set of shared settings and individual named configurations that inherit the shared settings and can override them. It’s important to give each named configuration a meaningful name, because this name is the identifier that will be displayed in the test report.

Having created one or more test plans, you still can’t use them for anything until you convert your scheme to use test plans. To do so, choose Product → Scheme → Convert Scheme to Use Test Plans. (I presume that some day test plans will be the default and this step will no longer be needed.) You are offered various ways to add test plans to your scheme. You should add to your scheme every test plan you might want to use with this scheme.

Your scheme may now have multiple test plans, but only one test plan is current at any given moment. When you choose Product → Test, it is the current test plan that runs. Here’s how the current test plan is determined:

In the Test navigator

At the top, the Test navigator has a pop-up menu letting you pick a test plan from among those attached to the current scheme. Whatever test plan is currently displayed here is the current test plan. If the current test plan doesn’t include a test, that test is dimmed and you can’t run it from the Test navigator (though of course you can select it and run it from the test source).

In the Product menu

The Product → Test Plan hierarchical menu lists the test plans attached to the current scheme. Whatever test plan is checked here is the current test plan.

If a test plan has more than one configuration, then when that test plan is current, if you run any test method within that test plan, by default it runs under all configurations successively. If that isn’t what you want, Control-click to summon the contextual menu; it lets you specify a configuration for this run.

Warning

Code coverage in a test plan must be turned on or off at the shared settings level, not at the individual configuration level. Double-click the Code Coverage value to bring up a popover where you can toggle the “Gather coverage” checkbox. This option does not appear unless your scheme is actually using this test plan.

Clean

From time to time, during repeated testing and debugging, and before making a different sort of build (switching from Debug to Release, or running on a device instead of the Simulator), it’s a good idea to clean your target. This means that existing builds will be removed and caches will be cleared, so that all code will be considered to be in need of compilation and you can build your app from scratch.

Cleaning removes the cruft, quite literally. Suppose you have been including a certain resource in your app, and you decide it is no longer needed. You can remove it from the Copy Bundle Resources build phase (or from your project as a whole), but that doesn’t necessarily remove it from your built app. This sort of leftover resource can cause all kinds of mysterious trouble. The wrong version of a nib may seem to appear in your interface; code that you’ve edited may seem to behave as it did before the edit. Cleaning removes the built app, and very often solves the problem.

You can choose Product → Clean Build Folder, which removes the entire build folder for this project. For an even more extensive cleaning, quit Xcode, open your user ~/Library/Developer/Xcode/DerivedData folder, and move all its contents to the trash. This is a complete clean for every project you’ve opened recently — plus the module cache. Removing the module cache can reset Swift itself, causing occasional mysterious compilation, code completion, or syntax coloring issues to go away.

In addition to cleaning your project, you should also remove your app from the Simulator. This is for the same reason as cleaning the project: when a new build of the app is copied to the Simulator, existing resources inside the old build may not be removed (in order to save time), and this may cause the app to behave oddly. To clean out the current simulator while running the Simulator, choose Hardware → Erase All Content and Settings. To clean out all simulators, quit the Simulator and then say, in the Terminal:

$ xcrun simctl erase all

Running on a Device

Eventually, you’ll want to progress from running and testing and debugging in the Simulator to running and testing and debugging on a real device. The Simulator is nice, but it’s only a simulation; there are many differences between the Simulator and a real device. The Simulator is really your computer, which is fast and has lots of memory, so problems with memory management and speed won’t be exposed until you run on a device. User interaction with the Simulator is limited to what can be done with a mouse: you can click, you can drag, you can hold Option to simulate use of two fingers, but more elaborate gestures can be performed only on an actual device. And many iOS facilities, such as the accelerometer and access to the music library, are not present on the Simulator at all, so that testing an app that uses them is possible only on a device.

Running your app on a device requires a Developer Program membership, which in turn requires an annual fee. You may balk initially, but sooner or later you’re going to get over it and accept that this fee is worth paying. (You can obtain a temporary ability to run your app on a device without a paid Developer Program membership, but this ability is very limited and I’m not going to discuss it.)

Obtaining a Developer Program Membership

To obtain a Developer Program membership, go to the Apple Developer Program web page (https://developer.apple.com/programs) and initiate the enrollment process. When you’re starting out, the Individual program is sufficient. The Organization program costs no more, but adds the ability to privilege additional team members in various roles; you do not need the Organization program merely to distribute your built app to other users for testing.

Your Developer Program membership involves two things:

An Apple ID

The user ID that identifies you at Apple’s site (along with the corresponding password). You’ll use your Developer Program Apple ID for all kinds of things. In addition to letting you prepare an app to run on a device, this same Apple ID lets you post on Apple’s development forums, download Xcode beta versions, and so forth.

A team name

You, under a single Apple ID, can belong to more than one team. On each team, you will have one or more roles dictating your privileges. If you are the head (or sole member) of the team, you are the team agent, meaning that you can do everything: you can develop apps, run them on your device, submit apps to the App Store, and receive the money for any paid apps that sell copies there.

Having established your Developer Program Apple ID, you should enter it into the Accounts preference pane in Xcode. Click the Plus button at the bottom left and select Apple ID as the type of account to add. Provide the Apple ID and password. From now on, Xcode will identify you through the team name(s) associated with this Apple ID; you shouldn’t need to tell Xcode this password again.

Signing an App

Running an app on a device is a remarkably complicated business. You will need to sign the app as you build it. An app that is not properly signed for a device will not run on that device (assuming you haven’t jailbroken the device). Signing an app requires two things:

An identity

An identity represents Apple’s permission for a given team to develop, on this computer, apps that can run on a device. It consists of two parts:

A private key

The private key is stored in the keychain on the computer. It identifies a computer where this team can potentially develop device-targeted apps.

A certificate

A certificate is a virtual permission slip from Apple. It contains the public key matching the private key (because you told Apple the public key when you asked for the certificate). With a copy of this certificate, any machine holding the private key can actually be used to develop device-targeted apps under the name of this team.

A provisioning profile

A provisioning profile is a virtual permission slip from Apple, uniting four things:

  • An identity.

  • An app, identified by its bundle identifier.

  • A list of eligible devices, identified by their unique device identifiers (UDIDs).

  • A list of entitlements. An entitlement is a special privilege that not every app needs, such as the ability to talk to iCloud. You won’t concern yourself with entitlements unless you write an app that needs one.

A provisioning profile is therefore sufficient for signing an app as you build it. It says that on this computer it is permitted to build this app such that it will run on these devices.

There are two types of identity, and hence two types of certificate and provisioning profile: development and distribution (a distribution certificate is also called a production certificate). We are concerned here with the development identity, certificate, and profile; I’ll talk about the distribution side later.

The only thing that belongs entirely to you is the private key in your computer’s keychain. Apple is the ultimate keeper of all other information: your certificates, your provisioning profiles, what apps and what devices you’ve registered. Your communication with Apple, when you need to verify or obtain a copy of this information, will take place through one of two means:

The developer member center

A set of web pages at https://developer.apple.com/account. Having logged in with your Apple ID, you can click Certificates, Identifiers & Profiles (or go directly to https://developer.apple.com/account/resources) to access all features and information to which you are entitled by your membership type and role. (This is the area of Apple’s site formerly referred to as the Portal.)

Xcode

Just about everything you would need to do at the developer member center can be done through Xcode instead. When all goes well, using Xcode is a lot simpler! If there’s a problem, you can head for the developer member center to iron it out.

Tip

New in Xcode 11, certificates are unified across all platforms. One development certificate applies to iOS development and to Mac development. These unified certificates are in a new format and are good only with Xcode 11 and later.

Automatic Signing

Apple provides two distinct ways of obtaining and managing certificates and profiles in connection with a project — automatic signing, and manual signing. For new projects, automatic signing is the default. This is indicated by the fact that the “Automatically manage signing” checkbox is checked in the Signing & Capabilities pane when you edit your project’s app target (Figure 9-18). (The Signing & Capabilities pane is new in Xcode 11; previously, the signing interface was the Signing section of the General pane.)

To see just how automatic Xcode’s signing management can be, let’s start at a stage where as yet you have neither a development certificate in your computer’s keychain nor a development profile for any app. But you do have a Developer Program Apple ID, and you’ve entered it into Xcode’s Accounts preference pane. Then, when you create a new project (File → New → Project), you’ll see on the second screen (“Choose options for your new project”) a pop-up menu listing all the teams with which your Apple ID is associated. Specify the desired team here.

When you then create the project on disk and the project window opens, everything happens automatically. Your computer’s keychain creates a private key for a development certificate. The public key is sent to Apple. The actual development certificate is created at the developer member center, and is downloaded and installed into your computer’s keychain. With no conscious effort, you’ve obtained a development identity!

If you’ve never run on any device before, and if you haven’t manually registered any devices at the developer member center, that might be as far as Xcode can go for now. If so, you’ll see some errors in the Signing & Capabilities pane, similar to Figure 9-15.

ios13 0908a
Figure 9-15. Xcode knows of no devices

Now connect a device via USB to your computer and select it as the destination, either under Product → Destination or in the Scheme pop-up menu in the project window toolbar. This causes the error in Figure 9-15 to change: a Register Device button now appears (Figure 9-16). Click it!

ios13 0908aa
Figure 9-16. Xcode offers to register a device

The problem is resolved; the error vanishes. You can switch to the Report navigator to learn what just happened (Figure 9-17).

ios13 0908b
Figure 9-17. Xcode has registered a device for us

As the Report navigator tells us, the device has been registered — and a development provisioning profile has been created and downloaded (and has been stored in your ~/Library/MobileDevice/Provisioning Profiles folder). This is a universal iOS Team Provisioning Profile — and that is all you need in order to run any basic app on any device! Figure 9-18 shows the resulting display in the Signing & Capabilities pane.

ios13 0908
Figure 9-18. Xcode manages signing credentials automatically

You are now almost ready to run this project on this device. There may, however, be one further step: you might have to disconnect the device from USB and connect it again. This is so that Xcode can recognize the device afresh and prepare for debugging on it. This process is rather time-consuming; a progress indication is shown at the top of the project window, and in the Devices and Simulators window.

The good news is that once you already have a development certificate, and once Xcode has already generated and downloaded a universal iOS Team Provisioning Profile, and once your device is already registered with Apple and prepared by Xcode for debugging, none of that will be necessary ever again. When you create a new project, you supply your team name. Xcode now knows everything it needs to know! The development certificate is valid for this computer, the universal iOS Team Provisioning Profile is universal, and the device is registered with Apple and prepared for debugging. Therefore, you should from now on be able to create a project and run it on this device immediately.

You can confirm your possession of a universal development provisioning profile by clicking the “i” button at the right of the Provisioning Profile (Figure 9-18): a popover displays information about the provisioning profile, as shown in Figure 9-19.

ios13 0910
Figure 9-19. A universal development profile

The asterisk (*) in that popover tells you that this is a universal profile, not restricted to one particular app ID. The universal development profile allows you to run any app on the targeted device for testing purposes, provided that the app doesn’t require special entitlements (such as using iCloud). If you turn on any entitlements for an app target (which you would do by adding a capability from the Signing & Capabilities pane when you edit the app target), and if you’re using automatic signing, Xcode will communicate with the developer member center to attach those entitlements to your registered app; then it will create a new provisioning profile that includes those entitlements, download it, and use it for this project.

Manual Signing

If you don’t want a project’s signing to be managed automatically by Xcode, simply uncheck the “Automatically manage signing” checkbox. This causes Xcode to take its hands off completely. Xcode won’t automatically generate or choose a development certificate or a provisioning profile; you will have to do it all yourself.

If you need to obtain a development certificate manually, there are two possible approaches:

The Accounts preference pane

In Xcode’s Accounts preference pane, select your team name and click Manage Certificates to summon the “Signing certificates” dialog. Click the Plus button at the lower left, and choose Apple Development. Xcode will communicate with the developer member center and a development certificate will be created and installed on your computer.

Keychain Access and the developer member center

Go to Certificates at the developer member center, click the Plus button, ask for an Apple Development certificate, click Continue, and follow the instructions that start on the next page:

  1. You begin by generating the private key in your computer’s keychain. Launch the Keychain Access application and choose Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority. Click the “Saved to disk” radio button and save the certificate signing request file onto your computer.

  2. At the develop member center, under Certificates, Identifiers & Profiles, go to Certificates and click the Plus button to start the process. Ask for an Apple Development certificate. You’ll be prompted to upload the certificate signing request file. The actual certificate is generated; download it, and double-click to install it into the keychain. (You can then throw away both the certificate request file and the downloaded certificate.)

Figure 9-20 shows what a valid development certificate looks like in Keychain Access.

ios13 0909
Figure 9-20. A valid development certificate

Once you have a development certificate, you can use the developer member center to create a development profile manually, if necessary:

  1. The device must be registered at the developer member center. Look under Devices to see if it is. If it isn’t, click the Plus button and enter a name for this device along with its UDID. You can copy the device’s UDID from its listing in Xcode’s Devices and Simulators window.

  2. The app must be registered at the developer member center. Look under Identifiers → App IDs to see if it is. If it isn’t, add it: Click Plus. Choose App IDs. Enter a name for this app. Ignore the App ID Prefix field. Copy the Bundle Identifier from the Signing & Capabilities pane and paste it into the bundle identifier field, and register the app.

    (If your app uses special entitlements, this step is also where you’d associate those entitlements manually with the app.)

  3. Under Profiles, click Plus. Ask for an iOS App Development profile. On the next screen, choose the App ID for this app (presumably the one you just created in the previous step). On the next screen, check your development certificate. On the next screen, select the device(s) you want to run on. On the next screen, give this profile a name, and generate the profile. You are now offered a chance to download the profile, but you don’t have to do that, because Xcode can do it for you.

  4. In Xcode, in the Signing & Capabilities pane, in the Provisioning Profile pop-up menu, choose Download Profile. The profile you created at the developer member center is listed here! Select it. The profile is downloaded and development provisioning is enabled for this project (Figure 9-21).

ios13 0910b
Figure 9-21. Manual code signing

Running the App

Once you have a development profile applicable to an app and a device, you can connect the device via USB, choose it as the destination in the Scheme pop-up menu, and build and run the app. (If you’re asked for permission to access your keychain, grant it.)

The app is built, loaded onto your device, and launched. As long as you launch the app from Xcode, everything is just as when running in the Simulator. You can run and you can debug. The running app is in communication with Xcode, so that you can stop at breakpoints, read messages in the console, profile your app with Instruments, and so on. The outward difference is that to interact physically with the app, you use the device, not the Simulator.

You can also configure your device to allow Xcode to build and run apps on it without a USB connection. To do so, start with the device connected via USB; locate the device in the Devices and Simulators window and check “Connect via network.” The device can now be used as a build and run destination wirelessly, provided it is connected via Wi-Fi to the local network or to some other network that your computer can access by its IP address. You can build and run from Xcode, pausing at breakpoints and receiving console messages, even though the device is not physically attached to your computer. This would be useful particularly if the app you’re testing requires the device to be manipulated in ways that are difficult when the device is tethered by a USB cable.

Managing Development Certificates and Devices

You’re allowed to have more than one development certificate, so there should be no problem running your project on a device from another computer. Just do what you did on the first computer! If you’re using automatic signing, a new certificate will be generated for you, and it won’t conflict with the existing certificate for the first computer.

When a device is attached to the computer, it appears in Xcode’s Devices and Simulators window. If this device has never been prepared for development, you can ask Xcode to prepare it for development. You can then build and run onto the device. If the device isn’t registered at the member center, a dialog appears offering to let you register it; click Register Device, and now the device is registered. Your automatically generated provisioning profile is modified to include this device, and you are now able to build and run on it.

The Devices and Simulators window can be used to communicate in other ways with a connected device. Using the contextual menu, you can copy the device’s UDID, and you can view and manage provisioning profiles on the device. In the main part of the window, you can see (and delete) apps that have been installed for development using Xcode, and you can view and download their sandboxes. You can take screenshots. You can view the device’s stored logs. You can open the Console application to view the device’s console output in real time.

Profiling

Xcode provides tools for probing the internal behavior of your app graphically and numerically, and you should keep an eye on those tools. The gauges in the Debug navigator allow you to monitor key indicators, such as CPU and memory usage, any time you run your app. Memory debugging gives you a graphical view of your app’s objects and their ownership chains, and can even reveal memory leaks. And Instruments, a sophisticated and powerful utility application, collects profiling data that can help track down problems and provide the numeric information you need to improve your app’s performance and responsiveness.

Gauges

The gauges in the Debug navigator are operating whenever you build and run your app. Click a gauge to see further detail displayed in the editor. The gauges do not provide highly detailed information, but they are extremely lightweight and always active, so they are an easy way to get a general sense of your running app’s behavior at any time. If there’s a problem, such as a prolonged period of unexpectedly high CPU usage or a relentless unchecked increase in memory usage, you can spot it in the gauges and then use Instruments to help track it down.

There are four basic gauges: CPU, Memory, Disk, and Network. Depending on the circumstances, you may see additional gauges. An Energy Impact gauge appears when running on a device, and for certain devices, a GPU gauge may appear as well.

In Figure 9-22, I’ve been heavily exercising my app for a few moments, repeating the most calculation- and memory-intensive actions I expect the user to perform. These actions do cause some spikes in energy usage, but that’s to be expected; this is a user-initiated action, and the user won’t perform it very often. Meanwhile, my app’s memory usage remains level. So I don’t suspect any issues.

ios13 0911
Figure 9-22. The Debug gauges
Warning

Note that Figure 9-22 is the result of running on a device. Running in the Simulator might give completely different — and misleading — results.

Memory Debugging

Memory debugging lets you pause your app and view a graphical display of your object hierarchy at that moment. This is valuable not only for detecting problems but also for understanding your app’s object structure.

To use memory debugging, run the app and click the Debug Memory Graph button in the debug bar (Figure 9-23). The app pauses, and you are shown a drawing of your app’s objects, linked by their chains of ownership. The Debug navigator lists your objects hierarchically; click an object to see a different part of the graph. Double-click an object in the graph to refocus the graph on that object.

In my app, the root view controller is a ViewController whose view’s subviews include a MyBoard view whose tilesInOrder property is an array of Tile views. Figure 9-23 displays that situation.

ios13 0911a
Figure 9-23. A memory graph

At the cost of some additional overhead, you can enable the malloc stack before running your app: edit the scheme’s Run action and under Diagnostics check Malloc Stack with the pop-up menu set to All Allocation and Free History. When you run the app, selecting an object in the memory graph provides a backtrace in the Memory inspector that tells you how each object came into being. Hover over a line of the backtrace and click the right-arrow button to jump to that line of your code.

Memory debugging also detects memory leaks. Such leaks will cause an error icon to appear, and are listed in the Runtime pane of the Issue navigator. Suppose we run the example from Chapter 5 where I have a Dog class instance and a Cat class instance with strong references to one another and no other references to either instance, so they are both leaking. The leaking Cat and Dog are listed in the Issue navigator, and clicking one them displays a graph of the problem: the Cat and the Dog are retaining one another (Figure 9-24).

ios13 0911b
Figure 9-24. The memory graph displays a leak

Instruments

To get started with Instruments, first set the desired destination in the Scheme pop-up menu in the project window toolbar. The destination should be a device if possible; Instruments on the Simulator does not reflect the reality you’re trying to measure. Now choose Product → Profile. Your app builds using the Profile action for your scheme; by default, this uses the Release build configuration, which is probably what you want. Instruments launches; if your scheme’s Instrument pop-up menu for the Profile action is set to Ask on Launch (the default), Instruments presents a dialog where you choose a template.

Alternatively, click Profile In Instruments in a Debug navigator gauge editor; this is convenient when the gauges have suggested a possible problem, and you want to reproduce that problem under the more detailed monitoring of Instruments. Instruments launches, selecting the appropriate template for you. A dialog offers two options: Restart stops your app and relaunches it with Instruments, whereas Transfer keeps your app running and hooks Instruments into it.

Once the Instruments main window appears, you’ll probably have to click the Record button, or choose File → Record Trace, to get your app running. Now you should interact with your app like a user; Instruments will record its statistics.

Figure 9-25 shows me doing much the same thing in Instruments that I did with the Debug navigator gauges in Figure 9-22. I’ve set the destination to my device. I choose Product → Profile; when Instruments launches, I choose the Allocations template. With my app running under Instruments, I exercise it for a while and then pause Instruments, which meanwhile has charted my memory usage. Examining the chart, I find that there are spikes up to about 14MB, but the app in general settles down to a much lower level (around than 6MB). Those are very gentle and steady memory usage figures, so I’m happy.

ios13 0912
Figure 9-25. Instruments graphs memory usage over time

The Leaks template can help you detect memory leaks (similar to the memory graph leak detection I discussed earlier). In Figure 9-26, I’ve again run the retain cycle code from Chapter 5, profiling the app using the Leaks template. Instruments has detected the leak, and has diagrammed the issue.

ios13 0913
Figure 9-26. Instruments describes a retain cycle

In the next example, I’m curious as to whether I can shorten the time it takes my app to load a photo image. I’ve set the destination to a device, because that’s where speed matters and needs to be measured. I choose Product → Profile. Instruments launches, and I choose the Time Profiler template. When the app launches under Instruments on the device, I load new images repeatedly to exercise this part of my code.

In Figure 9-27, I’ve paused Instruments, and am looking at what it’s telling me. Opening the triangles in the lower portion of the window, I can drill down to my own code, indicated by the user icon.

ios13 0914
Figure 9-27. Drilling down into the time profile

By double-clicking the listing of that line, I can see my own code, time-profiled (Figure 9-28). The profiler is drawing my attention to the call to CGImageSourceCreateThumbnailAtIndex; this is where we’re spending most of our CPU time. That call is in the ImageIO framework; it isn’t my code, so I can’t make it run any faster. It may be, however, that I could load the image another way; at the expense of some temporary memory usage, perhaps I could load the image at full size and scale it down by redrawing it myself. If I’m concerned about speed here, I could spend a little time experimenting. The point is that now I know what the experiment should be. This is just the sort of focused, fact-based numerical analysis at which Instruments excels.

ios13 0916
Figure 9-28. My code, time-profiled

You can inject custom messages into your Instruments graphs in the form of signposts. For instance, based on the first Instruments example, I may suspect that my highest memory spikes are taking place within my newGame method. To confirm this, I’ll add some signposts. I import os and configure an OSLog object called mylog with a .pointsOfInterest category:

let mylog = OSLog(subsystem: "diabelli", category: .pointsOfInterest)

Then I instrument the start and end of my newGame method with os_signpost calls:

private func newGame(imageSource: Any, song: String) {
    os_signpost(.begin, log: mylog, name: "newgame")
    // ...
    os_signpost(.end, log: mylog, name: "newgame")

To prepare my Instruments template, I start with the Allocations template; then I choose View → Show Library to bring up the instrument chooser, and add the Points of Interest instrument to my template. When I run the app, Instruments displays the "newgame" signposts — and sure enough, they surround the memory spikes (Figure 9-29).

ios13 0916ccc
Figure 9-29. Signposts in Instruments

Those examples barely scratch the surface. Use of Instruments is an advanced topic; an entire book could be written about Instruments alone. The Instruments application comes with online help that’s definitely worth studying. Many WWDC videos from current and prior years are about Instruments; look particularly for sessions with “Instruments” or “Performance” in their names.

Localization

A device — or, new in iOS 13, an individual app — can be set by the user to prefer a certain language as its primary language. You might like your app’s interface to respond to this situation by appearing in that language. This is achieved by localizing the app for that language. You will probably want to implement localization relatively late in the development of the app, after the app has achieved its final form, in preparation for distribution.

Localization operates through localization folders with an .lproj extension in your project folder and in the built app bundle (Figure 6-6). When your app obtains a resource, if it is running on a system whose language corresponds to a localization folder, if that localization folder contains a version of that resource, that’s the version that is loaded.

Any type of resource can live in these localization folders; you will be particularly concerned with text that is to appear in your interface. Such text must be maintained in specially formatted .strings files, with special names:

  • To localize your Info.plist file, use InfoPlist.strings.

  • To localize your Main.storyboard, use Main.strings.

  • To localize your code strings, use Localizable.strings.

Fortunately, you don’t have to create or maintain these files manually! Instead, you work with exported XML files in the standard .xliff format. Xcode will generate .xliff files automatically, based on the structure and content of your project; it will also read them and will turn them automatically into the various localized .strings files.

Creating Localized Content

To experiment with localization, our app needs some localizable content:

  1. Edit the target and enter a value in the Display Name text field in the General pane. Our Empty Window app already says “Empty Window” here, but it’s in gray, indicating that this is merely an automatic display name; enter “Empty Window” explicitly (and press Tab), to make this an actual display name. You have now created a “Bundle display name” key (CFBundleDisplayName) in the Info.plist file. That key will be localized.

  2. Edit Main.storyboard and confirm that it contains a button whose title is “Hello.” That title will be localized. (It will help the example if you also widen the button to about 100 points.)

  3. Edit ViewController.swift. The code here contains some string literals, such as "Howdy!":

    @IBAction func buttonPressed(_ sender: Any) {
        let alert = UIAlertController(
            title: "Howdy!", message: "You tapped me!",
            preferredStyle: .alert)
        alert.addAction(
            UIAlertAction(title: "OK", style: .cancel))
        self.present(alert, animated: true)
    }

    That code won’t be localized, unless we modify it. Your code needs to call the global NSLocalizedString function; you’ll usually supply these parameters:

    key (first parameter, no label)

    The first parameter is the key into a .strings file.

    value

    The default string if there’s no .strings file for the current language.

    comment

    An explanatory comment.

    So modify our buttonPressed method to look like this:

    @IBAction func buttonPressed(_ sender: Any) {
        let alert = UIAlertController(
            title: NSLocalizedString(
                "Greeting", value:"Howdy!", comment:"Say hello"),
            message: NSLocalizedString(
                "Tapped", value:"You tapped me!",
                comment:"User tapped button"),
            preferredStyle: .alert)
        alert.addAction(UIAlertAction(
            title: NSLocalizedString(
                "Accept", value:"OK", comment:"Dismiss"),
            style: .cancel))
        self.present(alert, animated: true)
    }

Exporting

Now we’re going to give our project an actual localization, and we’ll export an editable .xliff file expressing the details of that localization. For my localization language, I’ll choose French:

  1. Edit the project. Under Localizations, click the Plus button. In the pop-up menu that appears, choose French. In the dialog, click Finish.

  2. Still editing the project, choose Editor → Export For Localization. In the dialog that appears, check French. You’re about to create a folder, so call it something like Empty Window Localization and save it to the desktop.

The result is an .xcloc bundle called fr.xcloc (for French). It’s an ordinary folder containing subfolders, along with a contents.json file describing the output (Figure 9-30).

ios12 0917cccc
Figure 9-30. An exported xcloc bundle

The heart of this output is an XML file called fr.xliff (inside the Localized Contents subfolder). Examining this file, you’ll observe that our app’s localizable strings have all been discovered:

  • For our Info.plist file in the project, Xcode has created a corresponding <file> element. When imported, this element will be turned into a localized InfoPlist.strings file.

  • For every localized .storyboard and .xib file, Xcode has run ibtool to extract the text, and has created a corresponding <file> element. When imported, these elements will be turned into eponymous localized .strings files.

  • For our code files containing a call to NSLocalizedString, Xcode has run genstrings to parse the file, and has created a corresponding <file> element. When imported, this element will be turned into a localized Localizable.strings file.

Editing

Now let’s pretend that you are the French translator, tasked with creating the French localization of this app. Your job is to modify the fr.xliff file by providing a <target> tag for every <source> tag that is to be translated into French. Your edited file might contain, at the appropriate places, translations like this (note that the id and ObjectID attributes will be different in your actual fr.xliff file):

<trans-unit id="RoQ-mP-swT.normalTitle">
  <source>Hello</source>
  <target>Bonjour</target>
  <note>Class="UIButton"; normalTitle="Hello"; ObjectID="RoQ-mP-swT";</note>
</trans-unit>

<trans-unit id="CFBundleDisplayName">
  <source>Empty Window</source>
  <target>Fenêtre Vide</target>
</trans-unit>
<trans-unit id="CFBundleName">
  <source>Empty Window</source>
  <target>Empty Window</target>
</trans-unit>

<trans-unit id="Accept">
  <source>OK</source>
  <target>OK</target>
  <note>Dismiss</note>
</trans-unit>
<trans-unit id="Greeting">
  <source>Howdy!</source>
  <target>Bonjour!</target>
  <note>Say hello</note>
</trans-unit>
<trans-unit id="Tapped">
  <source>You tapped me!</source>
  <target>Vous m'avez tapé!</target>
  <note>User tapped button</note>
</trans-unit>

Other types of localizable resources may have been exported automatically as well. New in Xcode 11, images in the asset catalog are localizable, and these are exported too (in Localized Contents). If an asset catalog was exported for localization, the translator can edit or replace the localized version of an image directly.

In addition, the original versions of our localizable material were exported (in Source Contents); and, new in Xcode 11, your export can include screenshots taken during interface testing (described earlier in this chapter). These can give the translator valuable context as to the usage of the terms to be translated.

Importing

The French translator, having edited the fr.xliff file and any exported localizable asset catalog images, returns the fr.xcloc folder to us. We proceed to incorporate it back into our project:

  1. Edit the project.

  2. Choose Editor → Import Localizations; in the dialog, locate and open the fr.xcloc folder.

Xcode parses the .xcloc folder, locates the fr.xliff file inside it, opens and reads it, and creates the corresponding files in the project. In particular, there is now a fr.lproj folder containing .strings files in the correct format, namely key–value pairs like this:

/* Optional comments are C-style comments */
"key" = "value";

The .strings files in our fr.lproj include the following:

  • An InfoPlist.strings file, localized for French, corresponding to our Info.plist file. It reads like this:

    /* (No Comment) */
    "CFBundleDisplayName" = "Fenêtre Vide";
    
    /* (No Comment) */
    "CFBundleName" = "$(PRODUCT_NAME)";
  • A Main.strings file, localized for French, corresponding to Main.storyboard. It will be similar to this:

    /* Class="UIButton"; normalTitle="Hello"; ObjectID="RoQ-mP-swT"; */
    "RoQ-mP-swT.normalTitle" = "Bonjour";
  • A Localizable.strings file, localized for French, localizing the strings in our code. It looks like this:

    /* Dismiss */
    "Accept" = "OK";
    
    /* Say hello */
    "Greeting" = "Bonjour!";
    
    /* User tapped button */
    "Tapped" = "Vous m'avez tapé!";

If the translator has edited any exported asset catalog images, they will be incorporated into the appropriate slots in our asset catalog.

Testing Localization

Build and run the project in the Simulator. The project runs in English, so the button title is still “Hello,” and the alert that it summons when you tap it still contains “Howdy!”, “You tapped me!”, and “OK.” Stop the project in Xcode.

Now we’re going to transport ourselves magically to France! In the Simulator, use the Settings app to change the system language to French (under General → Language and Region). Presto! Back in the Springboard, our app’s title has changed to Fenêtre Vide. When we run the app, the button in the interface has the title Bonjour. When we tap it, the alert contains “Bonjour!”, “Vous m’avez tapé!”, and “OK.”

(Instead of changing the Simulator settings, we could edit the scheme. In the scheme’s Run action, under the Options pane, change the Application Language. Then build and run again.)

In real life, preparing your nib files to deal with localization will take some additional work. In particular, you’ll want to use autolayout, configuring your interface so that interface objects containing text have room to grow and shrink to compensate for the change in the length of their text in different languages.

To view your interface under different localizations, you can preview your localized nib files within Xcode, without running the app. Edit a .storyboard or .xib file and choose Editor → Preview (or choose Preview from the pop-up Editor Options menu at the top right of the editor pane). In the preview pane, a pop-up menu at the lower right lists localizations; choose from the menu to switch between them. A “double-length pseudolanguage” stress-tests your interface with really long localized replacement text.

New in Xcode 11, test plans (discussed earlier in this chapter) let you create multiple configurations, each of which can have a different system language and region. When you run a test, it runs under all of the current test plan’s configurations in succession. This provides an automated way to check that your app behaves correctly under all localizations.

Distribution

Distribution means sharing your built app with users (other than members of your team) for running on their devices. There are two primary kinds of distribution:

Ad Hoc distribution

You are providing a copy of your app to a limited set of known users so that they can try it on specific devices and report bugs, make suggestions, and so forth.

App Store distribution

You are providing the app to the App Store. This could be for one of two reasons:

TestFlight testing

You are providing access to the app temporarily to users for testing through TestFlight.

Sale

You are providing the app to the App Store to be listed publicly, so that anyone can download it and run it, possibly for a fee.

Making an Archive

To create a copy of your app for distribution, you need first to build an archive of your app. An archive is basically a preserved build. It has three main purposes:

Distribution

An archive will serve as the basis for subsequent distribution of the app; the distributed app will be exported from the archive.

Reproduction

Every time you build, conditions can vary, so the resulting app might behave slightly differently. But every distribution from a particular archive contains an identical binary and will behave the same way. If a bug report arrives based on an app distributed from a particular archive, you can distribute that archive to yourself and run it, knowing that you are testing exactly the same app.

Symbolication

The archive includes a .dSYM file that allows Xcode to accept a crash log and report the crash’s location in your code. This helps you to deal with crash reports from users.

Here’s how to build an archive of your app:

  1. Set the destination in the Scheme pop-up menu in the project window toolbar to Generic iOS Device. Until you do this, the Product → Archive menu item will be disabled. You do not have to have a device connected; you are not building to run on a particular device, but saving an archive that will run on some device.

  2. If you like, edit the scheme to confirm that the Release build configuration will be used for the Archive action. This is the default, but it does no harm to double-check.

  3. Choose Product → Archive. The app is compiled and built. The archive itself is stored in a date folder within your user ~/Library/Developer/Xcode/Archives folder. Also, it is listed in Xcode’s Organizer window (Window → Organizer) under Archives; this window may open spontaneously to show the archive you’ve just created. You can add a comment here; you can also change the archive’s name (this won’t affect the name of the app).

You’ve just signed your archive with a development profile; that’s good, because it means you can run the archived build directly on your device. However, a development profile can’t be used to make an Ad Hoc or App Store build of your app; therefore, when you export the archive to form an Ad Hoc or App Store build, Xcode will embed the appropriate distribution profile instead. So now, in order to export from your archive, you need a distribution certificate and a distribution profile.

The Distribution Certificate

There are three ways to obtain a distribution certificate, parallel to the three ways of obtaining a development certificate described earlier in this chapter:

Automatic signing

If you’re using automatic signing, and if you have no distribution certificate, then when you first export the archive to the App Store (as I’ll describe later in this chapter), Xcode will offer to create and download a distribution certificate for you, automatically, along with a distribution profile.

The Accounts preference pane

You can request a distribution certificate through Xcode’s Accounts preference pane: select your Apple ID, choose your team, click Manage Certificates to show the “Signing certificates” dialog, click the Plus button at the bottom left, and ask for an Apple Distribution certificate.

Keychain Access and the developer member center

You can obtain a distribution certificate manually using the Keychain Access application and the developer member center, exactly as I described earlier for obtaining a development certificate manually.

Once you’ve obtained a distribution certificate, you’ll see it in your keychain. It will look just like Figure 9-20, except that it will say “Distribution” instead of “Development.”

There is an important difference between distribution certificates and development certificates: your team cannot have multiple distribution identities. Therefore, with distribution certificates, there can be only one.

As a result, distribution from a computer that’s different from the one where you originally obtained the distribution certificate can be tricky. Your distribution certificate is back in the keychain of your old computer. On your new computer, Xcode reports the existence of the distribution certificate (in the Accounts preference pane, under Manage Certificates), but tells you that it isn’t in the keychain of this computer.

What to do? One possibility is to click the Plus button at the bottom left and ask for a new distribution certificate, just as I described a moment ago. However, that might not be a good idea, because this will not be the same distribution certificate as the one on the old computer, and remember: there can be only one. Therefore, creating a new distribution certificate invalidates (revokes) the old distribution certificate sitting on the old computer — and therefore it also invalidates any distribution profiles you already have. The new distribution certificate won’t work with your existing distribution profiles, because they are tied to the old (invalid) distribution certificate.

A better alternative would be to install a copy of the existing distribution certificate; but that’s not trivial either. You can’t simply go to the developer member center and download a copy of the existing distribution certificate, because the existing distribution certificate is matched to a private key, and won’t work without it — and that private key is still sitting in the keychain of the old computer.

The solution is to return to the old computer where the distribution certificate is in the keychain, and, in the Accounts preference pane, under Manage Certificates, Control-click that certificate and choose Export Certificate from the contextual menu. You’ll be asked to save the resulting file, securing it with a password. The password is needed because this file, a .p12 file, contains the private key from your keychain. Now copy the .p12 file to the new computer. (You could email it to yourself.) On that computer, open the exported file, using the same password. The private key and the certificate are imported into the keychain of the new computer. You can then throw away all copies of the .p12 file; it has done its job.

The Distribution Profile

Obtaining a distribution profile is like obtaining a development profile. If you’re using automatic signing for this project, Xcode will probably be able to create an appropriate distribution profile for you automatically when you export your archive.

You can also obtain a distribution profile manually, at the developer member center, under Certificates, Identifiers & Profiles. The procedure is similar to obtaining a development profile manually, with a few slight differences:

  1. If this is to be an Ad Hoc distribution profile, collect the UDIDs of all the devices where this build is to run, and make sure you’ve added each of them at the developer member center under Devices. (For an App Store distribution profile, omit this step.)

  2. Make sure that the app is registered at the developer member center under Identifiers → App IDs, as I described earlier in this chapter.

  3. Under Profiles, click the Plus button to ask for a new profile. Choose an Ad Hoc or App Store profile. On the next screen, choose your app from the pop-up menu. On the next screen, choose your distribution certificate. On the next screen, for an Ad Hoc profile only, specify the devices you want this app to run on. On the next screen, give the profile a name.

    Be careful about the profile’s name, as you might need to be able to recognize it later from within Xcode! My own practice is to assign a name containing the term “AdHoc” or “AppStore” and the name of the app.

  4. Click Done. You should subsequently be able to download the profile from within Xcode (and if not, you can click Download at the developer member center).

Distribution for Testing

There are two ways to distribute your app for testing: Ad Hoc distribution (the old way) and TestFlight distribution (the new way). I’ll briefly describe each of them.

Here are the steps for creating an Ad Hoc distribution file from an archive:

  1. In the Organizer window, under Archives, select the archive and click Distribute App at the upper right. A dialog appears. Here, you are to specify a method; choose Ad Hoc. Click Next.

  2. In the next screen, you may be offered various options:

    App Thinning

    This means that multiple copies of the app can be created, each containing resources appropriate only to one type of device, simulating what the App Store will do when the user downloads the app to a device. There would normally be no need for this, though it might be interesting to learn the size of your thinned app.

    Rebuild from Bitcode

    Bitcode allows the App Store to regenerate your app to incorporate future optimizations. If you’re going to be using bitcode when you upload to the App Store, you might like to use it when you perform your Ad Hoc build. Personally, I avoid bitcode, so I would uncheck this checkbox.

    Strip Swift symbols

    Check this box to reduce the build size somewhat.

  3. In the next screen, you may be offered a choice between automatic and manual signing. An automatically generated Ad Hoc distribution profile will be configured to run on all devices registered for your team at the developer member center. If you choose manual signing, you’ll see another screen where you can specify the certificate and choose an Ad Hoc distribution profile, either from the member center or (if you’ve downloaded the distribution profile already) from your computer.

  4. The archive is prepared, and a summary window is displayed. The name of the provisioning profile is shown, so you can tell that the right thing is happening. Click Export.

  5. You are shown a dialog for saving a folder. The file will be inside that folder, with the suffix .ipa (“iPhone app”), accompanied by .plist and log files describing the export process.

  6. Locate in the Finder the .ipa file you just saved. Provide this file to your users with instructions.

How should a user who has received the .ipa file copy it onto a registered device? The old way was to use iTunes as an intermediary, and it may be that that will still work: attach the device to the computer, locate its listing in iTunes, and drag the .ipa file directly from the Finder onto the device’s name. However, I find that approach unreliable. Here are two alternative suggestions:

Xcode

A developer who has Xcode can attach the device to the computer, find the device in Xcode’s Devices window (Window → Devices and Simulators), click the Plus button under Installed Apps, and choose the .ipa file on the computer. It will be copied onto the device.

Apple Configurator

For other users, the simplest approach is to download the Apple Configurator application from the Mac App Store. Attach the device to the computer and launch Apple Configurator. An image of the device’s screen appears in the Configurator window. Drag the .ipa file from the Finder onto that image, and it will be copied onto the device.

The number of Ad Hoc testers is limited to 100 devices per year per developer (not per app). Devices used for development are counted against this limit. You can work around this limit, and provide your betas more conveniently to testers, by using TestFlight beta testing instead.

TestFlight has many advantages over Ad Hoc testing. It lifts the limit of 100 devices to a limit of 10000 testers. It is far more convenient for your testers than Ad Hoc distribution, because they download and install prerelease versions of your app directly from the App Store onto their devices, through the TestFlight app. Communication between you and your testers is handled seamlessly: TestFlight emails invitations to testers, allows testers to provide feedback comments, collects crash logs, notifies testers when you update the app, and so forth.

Configuration is performed at the App Store Connect site (https://appstoreconnect.apple.com); a prerelease version uploaded to App Store Connect must be exported as if for App Store distribution (see the discussion of App Store submission later in this chapter). See the “Test a beta version” chapter of Apple’s App Store Connect Help document (Help → App Store Connect Help).

Prerelease versions of your app intended for distribution to beta testers require review by Apple. Basically, the rule is that if your app’s minor version number increases, you can expect a delay while Apple performs the review. On the other hand, internal testers (team members who have direct access to your App Store Connect account) can download new versions immediately. That includes you! I often use TestFlight as a way of distributing a build to myself so that I can test on a device under real-world conditions.

Final App Preparations

As the big day approaches when you’re thinking of submitting your app to the App Store, don’t become so excited by the prospect of huge fame and massive profits that you rush the all-important final stages of app preparation. Apple has a lot of requirements, and failure to meet them can cause your app to be rejected. Take your time. Make a checklist and go through it carefully. See Apple’s App Store Connect Help and the “Icons and Images” chapter of the iOS Human Interface Guidelines.

Icons in the app

The best way to provide your app with icons is to use the asset catalog (Figure 9-31). The image sizes needed are listed in the asset catalog itself. To determine which slots should be displayed, use the checkboxes in the Attributes inspector when you select the icon set. To add an image, drag it from the Finder into the appropriate slot.

ios13 0919
Figure 9-31. Icon slots in the asset catalog

An icon file must be a PNG file, without alpha transparency. It should be a full square; the rounding of the corners will be added for you. Apple seems nowadays to prefer simple, cartoony images with a few bright colors and possibly a gentle gradient background.

App icon sizes have changed over the years. If your app is to be backward compatible to earlier systems, you may need additional icons in additional sizes, corresponding to the expectations of those earlier systems. Conversely, new devices can come along, bringing with them new icon size requirements (this happened when the iPad Pro appeared on the scene). Again, this is exactly the sort of thing the asset catalog will help you with.

Optionally, you may elect to include smaller versions of your icon to appear when the user does a search on the device, as well as in the Settings app if you include a settings bundle. However, I never include those icons; the system’s scaled-down versions of my app icons look fine to me.

Marketing icon

To submit an app to the App Store, you will need to supply a 1024×1024 PNG or high-quality JPEG icon to be displayed at the App Store (the marketing icon). Apple’s guidelines say that it should not merely be a scaled-up version of your app’s icon; but it must not differ perceptibly from your app’s icon, either, or your app will be rejected (I know this from bitter experience).

The marketing icon should be included in the asset catalog. There’s a slot for it, along with the slots for the real app icons (Figure 9-32).

ios12 0919b
Figure 9-32. Marketing icon slot in the asset catalog

Launch images

There is a delay between the moment when the user taps your app’s icon to launch it and the moment when your app is up and running and displaying its initial window. To cover this delay and give the user a visible indication that something is happening, a launch image needs to be displayed during that interval.

The launch image needn’t be detailed; in fact, it probably should not be. It might be just a blank depiction of the main elements or regions of the interface that will be present when the app has finished launching. In this way, when the app does finish launching, those elements or regions appear to be filled in as the launch image fades away to reveal the real app.

In iOS 7 and before, the launch image was literally an image (a PNG file). It had to be included in your app bundle, and it had to obey certain naming conventions. As the variety of screen sizes and resolutions of iOS devices proliferated, so did the number of required launch images. The asset catalog, introduced in iOS 7, was helpful in this regard. But with the introduction of more and more devices and screen sizes, the entire situation threatened to become unmanageable.

For this reason, iOS 8 introduced a better solution. Instead of a set of launch images, you now provide a launch nib file — a single .xib or .storyboard file containing a single view to be displayed as a launch image. You construct this view using subviews and autolayout. The view is automatically reconfigured to match the screen size and orientation of the device on which the app is launching, and label and button text can be localized. The launch image is a snapshot of this view.

By default, a new app project comes with a LaunchScreen.storyboard file. This is where you design your launch image. The Info.plist points to this file as the value of its “Launch screen interface file base name” key (UILaunchStoryboardName). You can configure the Info.plist, if necessary, by editing the target and setting the Launch Screen File field (under App Icons and Launch Images).

You should take advantage of this feature — and not merely because it is convenient. The presence of a “Launch screen interface file base name” key in your Info.plist tells the system that your app runs natively on newer device types. Without it, your app is displayed zoomed or letterboxed (or both). Apple says that eventually, for apps that you submit in the future, devices will stop compensating in this way; therefore you must use a launch screen nib, not a launch image or set of launch images.

Custom fonts included in your app bundle cannot be displayed in a launch nib file. This is because they have not yet been loaded at the time the launch screen needs to be displayed. Also, code cannot run in association with the display of the launch screen; by definition, your app is launching and its code has not yet started to run. None of those limitations should be a concern. Keep the launch screen simple and minimal. Don’t try to misuse it as some kind of introductory splash screen. If you want a splash screen, configure a real view controller to display its view when the app has finished launching.

Screenshots and Video Previews

When you submit your app to the App Store, you will be asked for one or more screenshots of your app in action to be displayed at the App Store. These screenshots must demonstrate actual user experience of the app, or your app may be rejected by Apple’s review team. You should take them beforehand and be prepared to provide them during the app submission process. You can provide a screenshot corresponding to the screen size and resolution of every device on which your app can run, or you can reuse a larger-size screenshot for smaller sizes.

You can obtain screenshots either from the Simulator or from a device connected to the computer:

Simulator

Run the app in the Simulator with the desired device type as your destination. Choose File → New Screen Shot. Hold Option if you want to specify the screenshot’s name and location.

Device

In Xcode, in the Devices and Simulators window, locate your connected device under Devices and click Take Screenshot. Alternatively, choose Debug → View Debugging → Take Screenshot of [Device].

You can also take a screenshot directly on a device. If the device has a Home button, click the screen lock button and the Home button simultaneously. If not, click the screen lock button and the Volume Up button simultaneously. Now the screenshot is in the Camera Roll in the Photos app, and you can communicate it to your computer in any convenient way (such as by emailing it to yourself).

You probably don’t have devices with every size you need in order to submit screenshots to the App Store. The Simulator supplies every needed device size. It may be, however, that your app doesn’t run properly on the Simulator, because it uses features that exist only on a device. I frequently solve this problem by supplying artificial data to my app, on the simulator only, so that its interface works sufficiently to let me capture screenshots.

You can also submit to the App Store a video preview showing your app in action; it can be up to 30 seconds long, in H.264 or Apple ProRes format:

  1. Connect the device to the computer and launch QuickTime Player. Choose File → New Movie Recording.

  2. If necessary, set the Camera and Microphone to the device, using the pop-up menu from the down-pointing chevron button next to the Record button that appears when you hover the mouse over the QuickTime Player window.

  3. Start recording, and exercise the app on the device. When you’re finished, stop recording and save.

The resulting movie file can be edited to prepare it for submission to the App Store. For more details, see the “App preview specifications” section of the Reference chapter of Apple’s App Store Connect Help.

Property List Settings

A number of settings in the Info.plist are crucial to the proper behavior of your app. You should peruse Apple’s Information Property List Key Reference for full information. Most of the required keys are created as part of the template, and are given reasonable default values, but you should check them anyway. The following are particularly worthy of attention:

Bundle display name (CFBundleDisplayName)

The name that appears under your app’s icon on the device screen; this name needs to be short in order to avoid truncation. I talked earlier in this chapter about how to localize the display name. You can enter this value directly in the General pane when you edit your app target.

Supported interface orientations (UISupportedInterfaceOrientations)

This key designates the totality of orientations in which the app is ever permitted to appear. I talked earlier in this chapter about the interface for making these settings with checkboxes in the General pane of the target editor, but you get better fine tuning by editing the Info.plist directly. For example, it might be necessary to reorder the orientations (because on an iPhone the first orientation listed may be the one into which the app will actually launch).

Required device capabilities (UIRequiredDeviceCapabilities)

You should set this key if the app requires capabilities that are not present on all devices. But don’t use this key unless it makes no sense for your app to run at all on a device lacking the specified capabilities.

Bundle version

Your app needs a version number. The best place to set it is the General pane of the target editor. Things are a little confusing here because there are two fields:

Version

Corresponds in the Info.plist to “Bundle versions string, short” (CFBundleShortVersionString). This is a user-facing string and needs to be a version string, such as "1.0". It will be displayed at the App Store, distinguishing one release from another. Failure to increment the version string when submitting an update will cause the update to be rejected.

Build

Corresponds in the Info.plist to “Bundle version” (CFBundleVersion). The user never sees this value. I treat it as an independent integer value. It is legal to increment the Build number without incrementing the Version number, and that is what I do when I submit several successive builds of the same prospective release during TestFlight testing, or because I discover a bug and have to withdraw a submitted binary before it appears on the App Store.

The interplay between TestFlight versions and App Store versions is a little tricky. If you’re satisfied with a TestFlight version, you can submit the very same binary, which is already present at App Store Connect, for distribution by the App Store. But once you have submitted a version to the App Store, the next build that you submit, even if it is just for TestFlight, must have a higher version string; upping the build number is not sufficient.

Warning

Version strings don’t work like decimal numbers! Each component of the string is treated as an integer. A short version string "1.4" is not “higher” than a version string "1.32" — because 4 is smaller than 32. As usual, I learned this lesson the hard way.

Submission to the App Store

When you’re satisfied that your app works well, and you’ve installed or collected all the necessary resources, you’re ready to submit your app to the App Store for distribution. To do so, you’ll need to make preparations at the App Store Connect site (https://appstoreconnect.apple.com).

Note

The first time you visit App Store Connect, you should go to the Contracts section and complete submission of your contract. You can’t offer any apps for sale until you do, and even free apps require completion of a contractual form.

I’m not going to recite all the steps you have to go through to tell App Store Connect about your app, as these are described thoroughly in Apple’s App Store Connect Help document, which is the final word on such matters. But here are some of the main pieces of information you will sooner or later have to supply (and see also https://developer.apple.com/app-store/product-page):

Your app’s name

This is the name that will appear at the App Store; it need not be identical to the short name that will appear under the app’s icon on the device, dictated by the “Bundle display name” setting in your Info.plist file. Apple now requires that this name be 30 characters or fewer. You can get a rude shock when you submit your app’s information to App Store Connect and discover that the name you wanted is already taken. There is no reliable way to learn this in advance, and such a discovery can necessitate a certain amount of last-minute scrambling on your part. (Can you guess how I know that?)

Subtitle

A description of the app, 30 characters or fewer, that will appear below the name at the App Store.

Description

You must supply a description of fewer than 4,000 characters; Apple recommends fewer than 580 characters, and the first paragraph is the most important, because this may be all that users see at first when they visit the App Store. It must be pure text, with no HTML or character styling.

Promotional text

Optional; 170 characters or fewer. The significance of the promotional text is that you can change it for an existing app, without uploading a new build.

Keywords

A comma-separated list, shorter than 100 characters. These keywords will be used, in addition to your app’s name, to help users discover your app through the Search feature of the App Store.

Privacy policy

The URL of a web page describing your privacy policy.

Support

The URL of a web site where users can find more information about your app.

Copyright

Do not include a copyright symbol in this string; it will be added for you at the App Store.

SKU number

This is arbitrary, so don’t get nervous about it. It’s just an identifier that’s unique within the world of your own apps. It’s convenient if it has something to do with your app’s name. It needn’t be a number; it can actually be any string.

Price

You don’t get to make up a price. You have to choose from a list of pricing “tiers.”

Availability Date

There’s an option to make the app available as soon as it is approved, and this will typically be your choice.

Tip

As you submit information, click Save often! If the connection goes down and you haven’t explicitly saved, all your work can be lost. (Can you guess how I know that?)

Once your app is initially registered at App Store Connect, and when you have an archived build ready for distribution, you can export and upload it. The export process is similar to what I described earlier for Ad Hoc distribution. Select the archived build in the Organizer and click Distribute App; on the next screen, select App Store. Subsequent options are slightly different from the options for an Ad Hoc distribution: you won’t see anything about app thinning, because that depends on how the user obtains the app; you’ll see the bitcode checkbox; and there’s a checkbox for uploading symbols, which should make it easier to analyze crash reports. Eventually, a screen is displayed summarizing the .ipa content, and you can now upload to App Store Connect or save to disk:

Upload to App Store Connect

The upload is performed within Xcode, and the app will be validated at the far end.

Save to disk

You can perform the upload later using altool. This is a command-line tool built into Xcode. For more information, say xcrun altool --help.

After uploading the archive, you have one final step to perform. Wait for the binary to be processed at Apple’s end. (You should receive an email when processing has completed.) Then return to App Store Connect, where you submitted your app information. You will now be able to select the binary, save, and submit the app for review.

You will subsequently receive notifications from Apple informing you of your app’s status as it passes through various stages: “Waiting For Review,” “In Review,” and finally, if all has gone well, “Ready For Sale” (even if it’s a free app). Your app will then appear at the App Store.

Once your app is registered at the App Store, you do not need to make further preparations merely to upload a new build. Simply increase the build number or version string, as I described earlier, and upload the build. If this build is for TestFlight, and if this version has already been reviewed for TestFlight, the new build becomes available for testing immediately. If this build is for the App Store, you can upload it first and register the new version at App Store Connect later.