Xcode Projects, Targets, Schemes and Previews

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Getting Started with Apple Platforms created FieldNotes from Xcode’s multiplatform template and ran it. This page opens up what the template produced: the project file, its targets, the schemes that decide what Run and Test do, the build configurations and settings behind every build, and the canvas where SwiftUI previews and playgrounds run without launching the app. Everything here applies to Xcode 27 building Swift 6.4 code for iOS, iPadOS, macOS, watchOS and visionOS 27, with FieldNotes's deployment minimum kept at 26. Xcode 27-only features (Device Hub, argument grids in previews, the JSON project format) are called out as they come up.

The Swift toolchain behind Xcode (the compiler, swift build, SwiftPM) is covered in Swift Build and Tooling; this page is about the Xcode side only.

Workspace, Project, Target and Product

Four nouns describe every Xcode build:

  • A project (FieldNotes.xcodeproj) is a bundle directory holding the list of source files, the targets, the build configurations and the project-level build settings. It is the unit you open, and it is what goes into source control.

  • A target is one recipe for one product. FieldNotes has FieldNotes (the multiplatform app), FieldNotesWatch (the watch app), and later FieldNotesWidgets (a widget extension) and FieldNotesTests (a test bundle). Each target has its own build phases and its own build settings, which override the project’s.

  • A product is what a target produces: an .app bundle, an .appex extension, a framework, a test bundle. Products of one target can be embedded in another; the widget extension ends up inside FieldNotes.app/PlugIns/.

  • A workspace (.xcworkspace) groups several projects so their targets can depend on each other. Every project already contains an implicit workspace; you only create an explicit one when you combine projects. FieldNotes does not need one, because its shared code lives in local Swift packages (see Swift Packages and Modularizing an App), which a single project can reference directly.

On disk, FieldNotes looks like this:

FieldNotes/
├── FieldNotes.xcodeproj/          # the project bundle
│   ├── project.pbxproj            # targets, files, settings (or project.xcproj, see below)
│   └── xcshareddata/xcschemes/    # shared schemes, committed
│       ├── FieldNotes.xcscheme
│       └── FieldNotesWatch.xcscheme
├── Config/                        # .xcconfig files (see below)
├── FieldNotes/                    # app sources, Assets.xcassets, FieldNotes.entitlements
├── FieldNotesWatch/               # watch app sources
└── Packages/
    ├── FieldNotesKit/             # FieldNotesModel, FieldNotesServices
    └── FieldNotesUI/

The JSON Project Format (Xcode 27.2)

For twenty years the project’s configuration file has been project.pbxproj, an old-style property list full of generated identifiers that is notoriously hard to review and merge. Xcode 27 adds a second format: a smaller, hierarchical, self-describing JSON file named project.xcproj, still inside the .xcodeproj bundle. From Xcode 27.2 new projects use it by default, and Xcode 27 opens both. Changes in the JSON file map to what you did in Xcode, so diffs are readable, merge conflicts are rarer, and coding agents can edit it safely. To convert an existing project, select it in the Project navigator and choose JSON in the File inspector’s Project Format pop-up; the change is just the removal of one file and the addition of the other, so it can be reverted from source control. A converted project can no longer be opened by Xcode 26, which matters if part of a team, or a CI machine, lags behind.

# After switching FieldNotes to the JSON format, git shows one file swapped for another.
git status --short FieldNotes.xcodeproj
#  D FieldNotes.xcodeproj/project.pbxproj
# ?? FieldNotes.xcodeproj/project.xcproj

Build Phases

A target’s Build Phases tab lists, in order, what happens to its inputs: target dependencies first (so the watch app and packages are built before they are embedded), then Compile Sources, Link Binary With Libraries, Copy Bundle Resources, Embed phases for extensions and frameworks, and any Run Script phases you add. Xcode runs independent tasks in parallel and signs the product last. A typical custom phase in FieldNotes is a script that fails the build when the watch and phone apps disagree on a version number:

# Run Script phase "Check version parity" in the FieldNotes target.
# Input file:  $(SRCROOT)/Config/Base.xcconfig   (declared so the phase can run in the script sandbox)
set -euo pipefail
if [ "${MARKETING_VERSION}" != "$(awk -F' = ' '/^MARKETING_VERSION/ {print $2}' "${SCRIPT_INPUT_FILE_0}")" ]; then
  echo "error: MARKETING_VERSION does not match Config/Base.xcconfig"
  exit 1
fi

Figure: Project Anatomy

classDiagram class Workspace { projects packages } class Project { FieldNotes.xcodeproj buildConfigurations: Debug, Release projectBuildSettings } class Target { name: FieldNotes, FieldNotesWatch buildPhases targetBuildSettings dependencies } class Product { FieldNotes.app FieldNotesWatch.app } class BuildPhase { Compile Sources Link Binary With Libraries Copy Bundle Resources Embed Extensions Run Script } class Scheme { FieldNotes.xcscheme targetsToBuild } class Action { Run Test Profile Analyze Archive } class Configuration { Debug Release xcconfig file } Workspace "1" o-- "*" Project : contains Project "1" *-- "*" Target : defines Target "1" --> "1" Product : produces Target "1" *-- "*" BuildPhase : runs in order Workspace "1" o-- "*" Scheme : shares Scheme "1" --> "*" Target : builds Scheme "1" *-- "5" Action : has Action "*" --> "1" Configuration : uses Project "1" *-- "*" Configuration : declares

The left half of the diagram is what gets built (project, targets, phases, products); the right half is how it gets built for a given purpose (scheme, actions, configurations). Keeping those two apart is what makes the rest of this page easy to follow.

Schemes and Their Actions

A scheme is the thing chosen in the toolbar next to the Run button. It names the targets to build and defines five actions, each with its own build configuration and options:

Action Default configuration What it does for FieldNotes

Run (Command-R)

Debug

Builds, installs on the run destination, launches and attaches the debugger. Options: launch arguments, environment variables, simulated location, app language and region.

Test (Command-U)

Debug

Builds the test bundles and runs them, optionally through a test plan and with code coverage. See Unit Testing with Swift Testing.

Profile (Command-I)

Release

Builds an optimized app and opens it in Instruments. See Performance and Instruments.

Analyze (Shift-Command-B)

Debug

Runs the Clang static analyzer over C and Objective-C code, which matters only for mixed-language targets (see Mixing Swift, Objective-C and C++ in an App).

Archive

Release

Builds a distribution-ready .xcarchive and opens it in the Organizer for upload. See App Store Connect, TestFlight and App Review.

Xcode creates one scheme per app target and stores it in xcshareddata so the whole team shares it. The scheme file is plain XML; this is the part of FieldNotes.xcscheme that matters, including an environment variable that makes the Run action start with sample data:

<Scheme LastUpgradeVersion = "2700" version = "1.7">
   <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES">
      <!-- BuildActionEntries: FieldNotes, and FieldNotesWatch as a dependency -->
   </BuildAction>
   <TestAction buildConfiguration = "Debug" codeCoverageEnabled = "YES">
   </TestAction>
   <LaunchAction buildConfiguration = "Debug" launchStyle = "0" debugDocumentVersioning = "YES">
      <EnvironmentVariables>
         <EnvironmentVariable key = "FIELDNOTES_SAMPLE_DATA" value = "1" isEnabled = "YES">
         </EnvironmentVariable>
      </EnvironmentVariables>
   </LaunchAction>
   <ProfileAction buildConfiguration = "Release">
   </ProfileAction>
   <AnalyzeAction buildConfiguration = "Debug">
   </AnalyzeAction>
   <ArchiveAction buildConfiguration = "Release" revealArchiveInOrganizer = "YES">
   </ArchiveAction>
</Scheme>

The app reads that variable like any other process environment, which keeps the switch out of the Release build entirely because only the Run action sets it:

import Foundation

extension NoteStore {
    static func makeForLaunch() -> NoteStore {
        let store = NoteStore()
        if ProcessInfo.processInfo.environment["FIELDNOTES_SAMPLE_DATA"] == "1" {
            store.notes = Note.samples
        }
        return store
    }
}

Every scheme action also exists on the command line, which is how CI drives the same scheme (see CI/CD with Xcode Cloud and the Command Line):

xcodebuild -project FieldNotes.xcodeproj -list                      # targets, configurations, schemes
xcodebuild -scheme FieldNotes -destination 'platform=iOS Simulator,name=iPhone 17' test
xcodebuild -scheme FieldNotes -destination 'generic/platform=iOS' analyze
xcodebuild -scheme FieldNotes -destination 'generic/platform=iOS' \
  -archivePath build/FieldNotes.xcarchive archive

Debug and Release Configurations

A build configuration is a named set of build-setting values. Every new project has two:

  • Debug: no optimization (SWIFT_OPTIMIZATION_LEVEL = -Onone), DEBUG defined as a compilation condition, testability enabled so tests can use @testable import, and fast incremental builds.

  • Release: whole-module optimization (-O), no DEBUG condition, debug symbols moved into a separate dSYM for crash symbolication.

Schemes pick one per action, which is why Run is fast to build but Profile measures the code users will actually get. You can add configurations (a Staging build pointing at a test server is common) in the project editor’s Info tab. Code that must only exist in Debug uses the DEBUG condition, which is Swift conditional compilation (see Attributes and Compiler Control):

extension NoteStore {
    func logState() {
        #if DEBUG
        print("NoteStore: \(notes.count) notes in \(notebooks.count) notebooks")
        #endif
    }
}

.xcconfig Files for FieldNotes

Build settings edited in the Build Settings tab end up buried in the project file. Build configuration files (.xcconfig) keep them in plain text instead, where they are easy to review. FieldNotes keeps one shared base file and one file per configuration, assigned in the project editor’s Info tab under Configurations:

// Config/Base.xcconfig -- shared by every configuration of every FieldNotes target
MARKETING_VERSION = 2.2.0
CURRENT_PROJECT_VERSION = 42
PRODUCT_BUNDLE_IDENTIFIER = com.example.fieldnotes

IPHONEOS_DEPLOYMENT_TARGET = 26.0
MACOSX_DEPLOYMENT_TARGET = 26.0
WATCHOS_DEPLOYMENT_TARGET = 26.0
XROS_DEPLOYMENT_TARGET = 26.0

SWIFT_VERSION = 6
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_APPROACHABLE_CONCURRENCY = YES

// The host only: "//" would start a comment in an .xcconfig file, so the scheme lives in Info.plist.
FIELDNOTES_API_HOST = api.fieldnotes.example.com

// Optional, untracked file with each developer's DEVELOPMENT_TEAM (see Getting Started).
#include? "Signing.xcconfig"
// Config/Debug.xcconfig
#include "Base.xcconfig"

SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DEBUG
SWIFT_OPTIMIZATION_LEVEL = -Onone
ENABLE_TESTABILITY = YES
FIELDNOTES_API_HOST = staging-api.fieldnotes.example.com

// Settings can be conditioned on SDK, architecture or configuration.
SWIFT_ACTIVE_COMPILATION_CONDITIONS[sdk=iphonesimulator*] = $(inherited) DEBUG SIMULATOR
// Config/Release.xcconfig
#include "Base.xcconfig"

SWIFT_OPTIMIZATION_LEVEL = -O
SWIFT_COMPILATION_MODE = wholemodule
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym

$(inherited) appends to the value from the level below instead of replacing it, and #include? includes a file only if it exists. The Levels view of the Build Settings tab shows where each final value came from. From lowest to highest precedence, Xcode layers: system defaults, the .xcconfig mapped to the project, the project’s own settings, the .xcconfig mapped to the target, and the target’s own settings. Values passed to xcodebuild on the command line override all of them. A value typed into the Build Settings tab therefore silently beats the same setting in an .xcconfig; teams that adopt .xcconfig files usually delete the settings from the tab so the file is the single source of truth.

Build Settings That Matter

Xcode has hundreds of build settings; a handful decide most of an app’s behaviour. The first group below is the concurrency set, which decides how strictly the compiler checks data races and what code is main-actor isolated by default. The language rules behind them are in Actors, Isolation and Sendable; how they play out in an app is in Swift Concurrency in Apps.

Setting FieldNotes value Meaning

SWIFT_VERSION

6

The Swift language mode, not the compiler version. Xcode 27 ships the Swift 6.4 compiler, which also compiles Swift 5 mode code. Swift 6 mode turns data-race checks into errors.

SWIFT_STRICT_CONCURRENCY

(ignored)

minimal, targeted or complete: how many concurrency diagnostics to emit in Swift 5 mode, as warnings. In Swift 6 mode it is always complete, so FieldNotes does not set it; it is the migration knob for Swift 5 targets.

SWIFT_DEFAULT_ACTOR_ISOLATION

MainActor

Infers @MainActor on unannotated code in the target, so ordinary UI code needs no annotations and does not trigger false-positive data-race errors. Code that must run off the main actor opts out with nonisolated or @concurrent. New app projects created by Xcode set it to MainActor.

SWIFT_APPROACHABLE_CONCURRENCY

YES

Turns on a bundle of upcoming features, including NonisolatedNonsendingByDefault (a nonisolated async function runs on the caller’s actor unless marked @concurrent) and InferIsolatedConformances.

PRODUCT_BUNDLE_IDENTIFIER

com.example.fieldnotes

The app’s unique identity on the system and in App Store Connect; extensions append a suffix (com.example.fieldnotes.widgets).

MARKETING_VERSION / CURRENT_PROJECT_VERSION

2.2.0 / 42

The user-visible version and the build number, written into CFBundleShortVersionString and CFBundleVersion.

*_DEPLOYMENT_TARGET

26.0

The oldest OS the app installs on, per platform; any newer API needs if #available.

SWIFT_ACTIVE_COMPILATION_CONDITIONS

DEBUG in Debug

The flags tested by #if in Swift code.

The effect of SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor is easiest to see in code. In the FieldNotes app target, this view model needs no annotation to touch NoteStore (which is itself explicitly @MainActor, because it lives in a package where the default does not apply), while the import parser explicitly leaves the main actor:

import Foundation

// Implicitly @MainActor in the app target.
final class ImportController {
    let store: NoteStore
    init(store: NoteStore) { self.store = store }

    func importFile(at url: URL) async throws {
        let imported = try await NoteImporter().parse(url)   // runs off the main actor
        for item in imported {                               // back on the main actor
            store.add(Note(title: item.title, body: item.body, tags: item.tags))
        }
    }
}

nonisolated struct ImportedNote: Decodable, Sendable {
    var title: String
    var body: String
    var tags: [String]
}

nonisolated struct NoteImporter {
    @concurrent
    func parse(_ url: URL) async throws -> [ImportedNote] {
        let data = try Data(contentsOf: url)
        return try JSONDecoder().decode([ImportedNote].self, from: data)
    }
}

See Apple Developer Documentation — Build settings reference. Local packages set the same defaults in Package.swift rather than in build settings, as shown on Swift Packages and Modularizing an App. The Decodable conformance that decode relies on is covered in Codable and Serialization.

Info.plist Values vs Build Settings

Every bundle carries an information property list, Info.plist, which the system reads at install and launch time: the bundle identifier, the version, the display name, the privacy usage strings, supported orientations, URL schemes. Build settings, by contrast, are read only by the build. The two meet in three ways:

  1. Generated Info.plist. New targets set GENERATE_INFOPLIST_FILE = YES, and Xcode writes the plist from build settings named INFOPLIST_KEY_<Key>. The Info tab of the target editor edits these.

  2. Variable substitution. A hand-written plist (pointed to by INFOPLIST_FILE, merged with the generated one) can reference any build setting as $(NAME), which is how a value from an .xcconfig reaches the running app.

  3. Values from build settings. Some keys are always filled from settings, such as CFBundleIdentifier from PRODUCT_BUNDLE_IDENTIFIER and CFBundleShortVersionString from MARKETING_VERSION.

FieldNotes uses the generated plist for standard keys and a small plist file for its own:

// Config/Base.xcconfig (continued)
GENERATE_INFOPLIST_FILE = YES
INFOPLIST_FILE = FieldNotes/FieldNotes-Info.plist
INFOPLIST_KEY_CFBundleDisplayName = Field Notes
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = FieldNotes records where each note was taken.
INFOPLIST_KEY_NSCameraUsageDescription = FieldNotes attaches photos you take to your notes.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>FNAPIBaseURL</key>
    <string>https://$(FIELDNOTES_API_HOST)</string>
</dict>
</plist>

At runtime the value comes back from the bundle, already substituted for the configuration that was built:

import Foundation

enum AppConfiguration {
    static var apiBaseURL: URL {
        guard let string = Bundle.main.object(forInfoDictionaryKey: "FNAPIBaseURL") as? String,
              let url = URL(string: string) else {
            fatalError("FNAPIBaseURL missing from Info.plist")
        }
        return url
    }
}

The rule of thumb: if the system or the App Store needs to read it, it is an Info.plist key; if only the compiler, linker or signer needs it, it is a build setting; and if the app needs a value that varies by configuration, define it as a build setting and pass it through the plist. Secrets never belong in either, because both end up readable inside the shipped bundle (see Security and Privacy).

The Signing & Capabilities Tab

The Signing & Capabilities tab of each target does two jobs. The Signing section picks the team and turns on Automatically manage signing, which is enough for development (see the account discussion in Getting Started with Apple Platforms). The capabilities section adds features that need Apple’s permission, such as App Groups, iCloud, Push Notifications or HealthKit, with the + Capability button. Each capability writes a key into the target’s .entitlements file and, with automatic signing, registers it on the App ID in the developer account. FieldNotes's app and widget share data through an App Group:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.application-groups</key>
    <array>
        <string>group.com.example.fieldnotes</string>
    </array>
</dict>
</plist>

The file’s path is itself a build setting, CODE_SIGN_ENTITLEMENTS = FieldNotes/FieldNotes.entitlements. Certificates, provisioning profiles, manual signing and distribution are covered in Membership, Signing, Capabilities and Info.plist.

Figure: The Build Pipeline

flowchart LR Deps[Build dependencies: packages and FieldNotesWatch] --> Compile subgraph Compile[Compile] Swift[swift-frontend: .swift to .o] Assets[actool: Assets.xcassets to Assets.car] Strings[String Catalogs to .strings] end Compile --> Link[ld: link .o files and frameworks into the executable] Link --> Copy[Copy bundle resources and generated Info.plist] Copy --> Embed[Embed watch app, extensions and frameworks] Embed --> Sign[codesign: sign nested code, then the app with its entitlements] Sign --> App[FieldNotes.app]

Every Run, Test, Profile and Archive goes through the same pipeline; only the configuration and the signing identity differ. When a build fails, the Report navigator shows which of these steps failed.

SwiftUI Previews

A preview renders one view in the canvas next to the editor, rebuilding only what changed as you type. You declare it with the #Preview macro (the macro mechanism itself is explained in Macros); the canvas shows one tab per preview in the file. Show the canvas with Editor > Canvas (Option-Command-Return). In Xcode 27 code inside #Preview runs on the main actor, so it can call main-actor APIs such as NoteStore directly. The older PreviewProvider protocol is deprecated in Xcode 27; convert any remaining ones to #Preview.

Sample Data for FieldNotes

Previews need data that does not come from disk or the network. FieldNotes keeps it next to the model:

extension Note {
    static let samples: [Note] = [
        Note(title: "Kestrel over the ridge", body: "Hovering at 20 m, 07:40.",
             tags: ["birds", "raptors"], isPinned: true),
        Note(title: "Spring at the old quarry", body: "Flow noticeably lower than in March.",
             tags: ["water"]),
        Note(title: "Untitled", body: ""),
    ]
}

#Preview, Names and Traits

The simplest preview returns a view. A name labels its tab, and preview traits change the context it is rendered in: .sizeThatFitsLayout for a small component, .landscapeLeft for orientation, .fixedLayout for an exact size:

import SwiftUI

#Preview("Pinned") {
    NoteRow(note: Note.samples[0])
}

#Preview("Row, compact", traits: .sizeThatFitsLayout) {
    NoteRow(note: Note.samples[1])
        .padding()
}

#Preview("Detail, landscape", traits: .landscapeLeft) {
    NavigationStack {
        NoteDetail(note: Note.samples[0])
    }
}

#Preview("Capture button", traits: .fixedLayout(width: 120, height: 120)) {
    CaptureButton { }
}

A view that needs live, mutable state gets it with @Previewable, which declares state inside the preview without a wrapper view. The editor reads its note from the store, so the preview owns a store seeded with the sample notes and injects it:

#Preview("Editor") {
    @Previewable @State var store = NoteStore(notes: Note.samples)
    NoteEditor(noteID: Note.samples[0].id)
        .environment(store)
}

Xcode 27 adds argument grids: pass an array with arguments: and the canvas renders one preview per element, side by side, which is ideal for checking every state of a row at once. Clicking one of them opens it in interactive mode:

#Preview("Every row state", arguments: Note.samples) { note in
    NoteRow(note: note)
        .padding()
}

Sharing Expensive Setup with PreviewModifier

Views that read NoteStore from the environment need one injected, and building a realistic store (later, a SwiftData container) for every preview is slow. A PreviewModifier builds a shared context once, which the preview system caches and reuses across every preview that applies the modifier through the .modifier trait:

import SwiftUI

struct SampleNotes: PreviewModifier {
    static func makeSharedContext() async throws -> NoteStore {
        let store = NoteStore()
        Note.samples.forEach(store.add)
        return store
    }

    func body(content: Content, context: NoteStore) -> some View {
        content.environment(context)
    }
}

extension PreviewTrait where T == Preview.ViewTraits {
    @MainActor static var sampleNotes: Self { .modifier(SampleNotes()) }
}

#Preview("Split view", traits: .sampleNotes) {
    ContentView()
}

#Preview("Note list", traits: .sampleNotes, .landscapeLeft) {
    NavigationStack {
        NoteList(notebookID: nil, selection: .constant(nil))
    }
}

When FieldNotes adopts SwiftData, this modifier stays as it is, because views still read Note values from NoteStore. Only the few screens that use @Query on SwiftData records get a second preview modifier whose makeSharedContext() returns an in-memory ModelContainer, applied in body with .modelContainer(context).

Working in the Canvas

The canvas has three modes: Live (the preview is interactive, taps and scrolling work), Selectable (clicking an element selects its code), and a static snapshot. The canvas’s Variants button renders the same preview across color schemes, orientations or Dynamic Type sizes; the overrides picker changes appearance, text size, and (new in Xcode 27) localization, increased contrast and control borders without touching code. iOS previews also get a Resizable Canvas mode in Xcode 27, for checking how a view behaves at arbitrary widths. Previews run in a simulator process for their platform; choose the device in the canvas’s device picker, or My Mac for a macOS preview.

The #Playground Macro

#Playground is the non-UI counterpart of #Preview: a block of code in any Swift file that the canvas runs and annotates line by line with the resulting values, like a playground document but living next to the code it exercises. It is ideal for trying out model logic. Import the Playgrounds module, wrap the code, and click Resume in the canvas:

import Foundation
import Playgrounds

#Playground {
    let notes = Note.samples

    let tagCounts = Dictionary(grouping: notes.flatMap(\.tags), by: { $0 })
        .mapValues(\.count)
    let pinned = notes.filter(\.isPinned).map(\.title)
    let untitled = notes.filter { $0.title == "Untitled" }.count
}

Playground tabs show a beaker icon and preview tabs an eye icon; in Xcode 27 each tab can be pinned so it stays visible while you edit another file. Both #Preview and #Playground blocks are compiled out of the shipping app.

Device Hub

In Xcode 27, Device Hub is the one app that manages every run destination, taking over the jobs of the former Devices and Simulators window. Simulators run inside it, and paired physical devices can be mirrored in it, so an iPhone on the desk can be driven from the Mac. From Device Hub you:

  • create, rename and delete simulators, and install additional simulator runtimes;

  • pair devices by cable or (new with iOS, iPadOS and watchOS 27) over the network with Pair Nearby Device;

  • configure a simulated device’s environment: appearance, text size, location, network conditions;

  • capture screenshots and screen recordings;

  • inspect a device’s identifiers, installed apps and app data containers.

The last point replaces the .xcappdata workflow: download FieldNotes's container from a device, then attach it to the scheme’s Run action (Options > App Data) to start every debugging session from the same data. Everything Device Hub does is scriptable through simctl for simulators and devicectl for devices:

# Create a dedicated simulator for FieldNotes UI tests.
xcrun simctl create "FieldNotes iPhone" "iPhone 17" com.apple.CoreSimulator.SimRuntime.iOS-27-0

# Give it a fixed simulated location and dark mode.
xcrun simctl location "FieldNotes iPhone" set 51.5007,-0.1246
xcrun simctl ui "FieldNotes iPhone" appearance dark

# Physical devices: list them and install the app built for one.
xcrun devicectl list devices
xcrun devicectl device install app --device <device-identifier> \
  build/Build/Products/Debug-iphoneos/FieldNotes.app

Sidebar: Coding Intelligence and Agents in Xcode 27

Xcode 27 builds coding agents into the IDE. You choose a model or agent in Settings > Intelligence (Claude, OpenAI’s models and Codex, and, new in Xcode 27, Google Gemini), then work in the coding assistant, which now lives in the editor area: conversations run side by side, their artifacts (diffs, plans, preview snapshots) sit next to the transcript, and every change can be reviewed, annotated, undone or rolled back. Useful capabilities for a project like FieldNotes:

  • Plan mode (/plan): the agent writes an editable Markdown plan that you review and approve before any code changes.

  • Built-in specialists for localization, accessibility and UIKit resizing, and a Generate Translations button in the String Catalog editor.

  • Verification: agents can boot simulators, install and launch the app, tap through it, take screenshots and render previews in several variants to check their own work.

  • Generate a preview or playground from a selected symbol through the coding tools popover.

  • Plug-ins adding skills (invoked as slash commands), MCP servers and agent configurations.

Agents that run outside Xcode can use the same tools through Xcode’s MCP server, once Allow external agents to use Xcode tools is turned on in Intelligence settings:

# Give a terminal agent access to the open FieldNotes project through Xcode's MCP bridge.
claude mcp add --transport stdio xcode -- xcrun mcpbridge
codex mcp add xcode -- xcrun mcpbridge

The JSON project format described above exists partly for this reason: agents (and people) can change a target’s settings in a readable file rather than in project.pbxproj. As with any generated code, review the diff before committing; the build settings and entitlements an agent touches are exactly the ones this page describes.

References