[ Documentation ]
Running Swift apps

Running Swift apps

peach swift opens a Swift app in Peach and keeps it running while you edit. It runs in the foreground: it compiles your app, opens a preview, watches your sources, and stops everything it started when you press Ctrl+C.

terminal
peach swift ./MyApp # a SwiftPM package
peach swift ./MyApp.xcodeproj # an Xcode project's app target
peach swift ./MyApp.xcworkspace --scheme MyApp

Swift support is in beta. See the iOS overview for what compiles today and what does not.

What you can open

A SwiftPM package

Pass a directory that contains Package.swift. Its executable target is the app; library targets it depends on compile as their own modules. A minimal SwiftUI app is one file beside the manifest:

// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "Counter",
platforms: [.iOS(.v26)],
targets: [.executableTarget(name: "Counter", path: ".")]
)
import SwiftUI
@main
struct CounterApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}
struct ContentView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 16) {
Text("Tapped \(count) times").font(.title)
Button("Increment") { count += 1 }
.buttonStyle(.borderedProminent)
}
.padding()
}
}

An Xcode project or workspace

Pass an .xcodeproj, an .xcworkspace, or a directory that holds exactly one of them. A directory with a workspace opens the workspace. A directory with two workspaces or two projects is refused, and the error lists them so you can pass one. Peach reads both the classic project.pbxproj format and the JSON project.xcproj format that Xcode 27 writes.

Peach reads the project as data. It never runs Xcode, xcodebuild or your build phases, so a run script that generates sources must have run already. From the project it builds what Xcode builds for your app target:

  • +
    the target’s Swift sources and copied resources, including synchronized folders
  • +
    framework and library targets in any project of the workspace that the app depends on or links, each as its own module
  • +
    local Swift packages the workspace references
  • +
    remote Swift packages, compiled from source at the revisions Package.resolved pins. Peach fetches them into .build/peach under the workspace. A remote package with no pin is refused; resolve packages in Xcode once to create Package.resolved.

If you open a project that belongs to a workspace in a parent directory, Peach uses that workspace, because it holds the local packages and the pins.

Choosing a target or scheme

A project with one application target needs no flag. With more than one, Peach refuses and lists them; pick one:

terminal
peach swift ./MyApp.xcworkspace --target MyApp

To build the way a scheme’s Run action does, pass the scheme instead. Peach uses the scheme’s runnable target and its build configuration. It finds shared schemes (xcshareddata/xcschemes) and your own (xcuserdata/*/xcschemes) in the workspace and each project.

terminal
peach swift ./MyApp.xcworkspace --scheme "MyApp Staging"

An unknown scheme is refused with the list of schemes that exist. So is a scheme that runs no application target. --scheme and --target cannot be combined, and neither applies to a SwiftPM package.

Build configuration and settings

Without a scheme, Peach builds Debug when the project declares it, as Xcode’s Run action does, and otherwise the project’s default configuration. Settings resolve the way Xcode resolves them: project level, then target level; each level’s base .xcconfig (with its #includes) before its own settings; $(inherited); and conditional keys such as [sdk=iphonesimulator*] and [config=Debug].

  • +
    SWIFT_ACTIVE_COMPILATION_CONDITIONS and -D flags in OTHER_SWIFT_FLAGS set your Swift #if conditions, so #if DEBUG follows your project.
  • +
    Swift packages build in debug for Debug and in release for any other configuration, as Xcode builds them.
  • +
    $(SETTING) references in Info.plist expand against the same settings. An undefined setting expands to nothing, as in Xcode.
  • +
    A base configuration file the project names must exist. If your repository asks you to create one in a setup step, do that before opening the project.
  • +
    Editing an .xcconfig or project file rebuilds.

Where it compiles

On your machine (default)

By default your source never leaves your machine. Peach compiles with the swift.org toolchain release it pins (Swift 6.4.0) and its WebAssembly SDK. If either is missing, peach swift stops and prints the exact commands that install them for your platform. On macOS the toolchain installs into your home directory beside Xcode without replacing it.

An installed Peach also needs the prebuilt Peach Swift distribution, which it downloads, verifies and caches on first use. During the beta that distribution is available by invitation: set PEACH_SWIFT_ENDPOINT to the endpoint you were given. The distribution ships for macOS on Apple silicon and Linux on x64 and arm64.

terminal
PEACH_SWIFT_ENDPOINT=<your endpoint> peach swift ./MyApp

On Contrast (--upload-source)

With --upload-source, Peach sends your app’s Swift sources to the Contrast compiler instead, and sends each watched edit as you save. You need no local Swift toolchain. Passing the flag is your consent to that upload, and it requires a signed-in session (peach login) or a PEACH_API_KEY.

terminal
peach login
peach swift ./MyApp --upload-source

Uploaded compiles accept Swift sources only. A project with C or C++ targets, or a vendored native library, needs the local toolchain. Results are cached by content, so an unchanged set of files never compiles twice.

Editing while it runs

Save any watched file and Peach compiles the change and swaps it into the running app. The terminal prints Swift artifact: <hash> for each new build.

  • +
    View state carries over. A view whose position in the view tree did not change keeps its @State: counters, toggles, text, navigation paths, the selected tab and a presented sheet. State a property cannot carry (for example a custom type) rebuilds from its initial value, as SwiftUI does after a structural change.
  • +
    Pushed screens stay pushed when you change the views above the link that pushed them.
  • +
    Literal edits skip the compiler. When a save only changes a string, number or bool literal inside a view body, Peach applies the new value to the running app without compiling, the way Xcode Previews does. The terminal prints Swift literals: N changed. Any other change compiles. Uploaded compiles always compile.
  • +
    Errors keep the session alive. A failed compile prints the compiler’s file, line and column against your original source, and Peach keeps watching. Fix the error and the next save resumes the app with its state.

The first compile must succeed before the preview opens. If it fails, peach swift exits with the error and closes everything it started. The first build compiles every dependency, so it takes longer than later edits. After that, an edit recompiles only the changed file and relinks.

Reading the missing API report

When your app imports modules Peach does not provide, peach swift prints one list of every such module, the files that import it, and for a package Peach cannot compile, the reason. When compilation fails, it ends with the APIs Peach lacks, each with its error count and the files that use it. That report is the most useful thing to send us when you tell us what your app needs.

Flags

FlagEffect
--target <name>Run this Xcode application target when the project has more than one.
--scheme <name>Run the target and configuration of this scheme’s Run action. Not combinable with --target.
--upload-sourceCompile on Contrast instead of this machine. Uploads the project’s Swift sources and watched edits. Needs peach login or PEACH_API_KEY.
--device <model>Open on this simulated device, for example iphone-17-pro or ipad-pro-13. An unknown model is refused with the list of known ones.
--headlessOpen the preview without a visible browser window.
--no-openKeep compiling and serving the app without opening a preview. A running Peach lists it in the Swift app on the dock.
--host-port <port>Pin the local port the Swift preview host listens on. Otherwise Peach picks a free one.
--driver <playwright|desktop|electron>Choose which browser driver opens the preview.
--cdp-port <port>Pin the Playwright debugging port.

peach swift serves your app only to your own machine; the preview host answers loopback requests only.

Once it is running

The app runs in a normal Peach simulator, so every other command works on it: take a screenshot with peach screenshot, record with peach record, change location or permissions, or drive it in a test. See Testing, Device state and Sharing.