Skip to content

iOS guide

Basic steps to build and run an iOS project generated by pn.

What gets generated

pn run ios unpacks the bundled iOS template (a Swift Xcode project plus the PythonNativeKit Swift package) into build/ios/ios_template and stages three things at the project root:

  • Python.xcframework: the embedded CPython runtime for your app.python_version (downloaded once, checksum-verified, and cached under build/ios/ios_runtime/).
  • app/: your project's Python sources.
  • app_packages.iphoneos/ and app_packages.iphonesimulator/: the pythonnative package plus [requirements].packages resolved for that SDK (device or Simulator), so binary wheels such as numpy land in the right slice. pn run ios against a Simulator stages only the Simulator slice; pn build ios stages both. The "Install Python runtime" build phase copies the slice matching the SDK being built into the app bundle.

PythonNativeKit/ holds the native rendering core (component managers, gesture recognizers, animations, native modules) and is linked as a local Swift package. Native plugins from [plugins].paths and installed packages are copied into PythonNativeKit/Sources/PythonNativeKit/Plugins/ before xcodebuild runs.

The Xcode project links the framework at build time and embeds CPython through its C API (PythonRuntime.swift); an "Install Python runtime" build phase installs the standard library and converts binary modules into signed frameworks. The built .app is therefore complete as soon as xcodebuild finishes, with no post-build patching. If Python fails to start or your root component fails to mount, the app shows a full-screen error report with the Python traceback instead of failing silently.

Component model

The staged yoga/ directory supplies the YogaCore Swift package, which PythonNativeKit depends on. Xcode compiles its C++ source for the selected device or simulator architecture during the app build. The desktop PythonNative wheel's host extension isn't used by the app.

Your app/ directory contains @pn.component function components. The native iOS template boots Python with pythonnative.bootstrap.start() and asks the Host native module to mount your root component inside a ViewController (a thin subclass of PythonNativeKit's PNViewController). You don't call anything for this; just export your component and configure the entry point in pythonnative.toml (app.entry_point).

Run / Prepare

pn run ios

To target a specific Simulator, or a physical device, list the available targets and pass one to --device:

pn devices ios
pn run ios --device "iPhone 15 Pro"
pn run ios --device 00008120-001234567890ABCD

Running on a physical device requires [ios].development_team in pythonnative.toml (for code signing), a paired device, and Developer Mode enabled on the device (Settings > Privacy & Security > Developer Mode).

Or prepare without building:

pn run ios --prepare-only

You can then open build/ios/ios_template/ios_template.xcodeproj in Xcode.

Viewing logs

After building and installing into the Simulator, pn run ios launches the app with xcrun simctl launch --console-pty, which attaches your terminal to the app's stdout/stderr. Python print() calls and exception tracebacks appear inline until you press Ctrl+C, at which point the app is terminated cleanly.

SIMCTL_CHILD_PYTHONUNBUFFERED=1 is forwarded to the launched process so output is line-buffered and doesn't get stuck behind Python's stream buffers.

Pass --no-logs to skip the console attach and use the legacy fire-and-exit launch instead. That's useful when you want to continue interacting with the app from Xcode or Console.app, or when running in a non-interactive context like CI (see the e2e.yml workflow for an example).

To re-attach to a running app later without rebuilding, use:

pn logs ios

For a physical device, use Console.app or Xcode's Devices and Simulators window; simctl console attach is Simulator-only.

Requirements

  • iOS 13.0 or newer on the device or Simulator ([ios].deployment_target can't go lower; it's the floor for BeeWare's CPython and for iOS wheels on PyPI).
  • Xcode 14 or newer. simctl launch --console-pty was added in Xcode 14; on older toolchains either upgrade Xcode or pass --no-logs.
  • Python print() output on the Simulator is routed through the app's stderr by the pythonnative._ios_log module. This runs automatically when the embedded Python bootstraps on iOS, so you don't need to configure it.

Clean

Remove the build directory safely:

pn clean

Notes

  • Building and running for Simulator via the CLI is best-effort. Opening the generated project in Xcode is recommended for iterative development.

See Troubleshooting for the most common errors and their fixes.

Next steps