Quick Start
Create an oe-swift.json file in your Xcode project root (next to .xcodeproj):
{
"project": "MyApp.xcodeproj",
"scheme": "MyApp",
"simulator": "iPhone 16 Pro",
"bundleId": "com.example.myapp",
"launchArgs": ["-hasCompletedOnboarding", "YES"],
"views": [
{ "name": "WelcomeView", "file": "MyApp/Onboarding/WelcomeView.swift", "requiresLogout": true, "launchArgs": [], "verifyLabels": ["Welcome"] },
{ "name": "HomeView", "file": "MyApp/Views/HomeView.swift", "tapLabel": "Home", "verifyLabels": ["Your Home"] }
]
}
OpenEditor will auto-detect your project, build it, install on any booted simulators, and capture screenshots for each view on the canvas.
Project Configuration
| Field | Type | Description |
|---|---|---|
project Optional |
string |
Path to .xcodeproj or .xcworkspace, relative to the config
file. Auto-detected if omitted. |
scheme Optional |
string |
Xcode build scheme. Defaults to the project name. |
simulator Optional |
string |
Default simulator device name. Default: iPhone 16 Pro. |
bundleId Optional |
string |
App bundle identifier. Auto-detected from the built .app if omitted. |
launchArgs Optional |
array |
Launch arguments passed every time the app is started for captures. Useful when one project should boot into the logged-in app while specific onboarding screens override the launch args per view. |
urlScheme Optional |
string |
URL scheme for deep link navigation (e.g. myapp). Enables per-view
screenshots via xcrun simctl openurl. |
views Optional |
array |
List of SwiftUI views to display on the canvas. If omitted, views are auto-discovered by
scanning .swift files for struct ... : View. |
View Configuration
Each entry in the views array describes a SwiftUI view to preview:
| Field | Type | Description |
|---|---|---|
name Required |
string |
The SwiftUI struct name (e.g. ContentView, ProfileView). |
file Optional |
string |
Relative path to the .swift file from the project root. |
label Optional |
string |
Display label on the canvas. Defaults to the struct name with "View" stripped. |
device Optional |
string |
Override the simulator device for this view (e.g. iPad Air 11-inch (M3)).
|
deepLink Optional |
string |
Deep link URL to navigate to this view before capturing (e.g.
myapp://profile). |
tapLabel Optional |
string |
Accessibility label to tap to navigate to this view (e.g. a tab bar label like
Profile). |
tapId Optional |
string |
Accessibility identifier to tap to navigate to this view. |
tapXY Optional |
object |
Tap coordinates in points {"x": 120, "y": 832} to navigate (for tab bars
not exposed via accessibility in iOS 18+). |
requiresLogout Optional |
boolean |
Marks screens that should be captured before demo/login bootstrap, such as welcome and onboarding flows. |
launchArgs Optional |
array |
Overrides project-level launch arguments for this screen. Use an empty array [] to clear the project defaults for onboarding or logged-out captures. |
verifyLabels Optional |
array |
Accessibility labels that confirm the app really reached this screen before a capture is accepted. |
Per-View Screenshots
OpenEditor can capture a unique screenshot for each view by navigating the app in the simulator before taking the screenshot. Three strategies are available, tried in order:
Deep Links
If your app registers a URL scheme, add urlScheme at the project level and
deepLink per view. The engine uses xcrun simctl openurl to navigate.
{
"urlScheme": "myapp",
"views": [
{ "name": "HomeView", "deepLink": "myapp://home" },
{ "name": "ProfileView", "deepLink": "myapp://profile" }
]
}
Accessibility Tap
For apps with a TabView or navigation bar, use tapLabel to tap the
matching accessibility label. This works with standard SwiftUI Label components.
{
"views": [
{ "name": "FeedView", "tapLabel": "Feed" },
{ "name": "ExploreView", "tapLabel": "Explore" },
{ "name": "ProfileView", "tapLabel": "Profile" }
]
}
Accessibility-based navigation requires the
axe CLI tool. Install via: brew install cameroncooke/axe/axe
Coordinate Tap
iOS 18+ changed how TabView exposes its tabs to the accessibility tree — individual
tab buttons are no longer visible to automation tools. Use tapXY with point coordinates
to tap specific UI elements like tab bar items.
{
"views": [
{ "name": "FeedView", "tapXY": { "x": 40, "y": 832 } },
{ "name": "ExploreView", "tapXY": { "x": 120, "y": 832 } },
{ "name": "MapView", "tapXY": { "x": 280, "y": 832 } }
]
}
Use axe describe-ui --udid <UDID> to inspect the accessibility tree and find the
frame positions of UI elements. Tab bar center Y is typically around 832 points
on iPhone. Divide pixel coordinates by the device scale factor (3x for Pro devices).
Welcome and Onboarding Screens
If your app normally skips onboarding, keep the project-level launchArgs set for the main
app flow and override specific welcome/onboarding screens with launchArgs: [] plus
requiresLogout: true. Add verifyLabels so OpenEditor only accepts a capture
once the simulator has landed on the intended onboarding step.
{
"launchArgs": ["-hasCompletedOnboarding", "YES"],
"views": [
{
"name": "WelcomeIntroScreen",
"requiresLogout": true,
"launchArgs": [],
"verifyLabels": ["Welcome to Brunchclub"]
},
{
"name": "FeedView",
"tapXY": { "x": 40, "y": 832 },
"verifyLabels": ["Curated Selection"]
}
]
}
Fallback
When no navigation strategy is configured, OpenEditor captures whatever is currently visible on the simulator. iPhone and iPad get separate screenshots if both simulators are booted.
How It Works
- OpenEditor reads
oe-swift.jsonand resolves the Xcode project and views. - It runs
xcodebuildto build the app for the iOS Simulator. - The built
.appis installed and launched on all booted simulators. - For each view with navigation config, the engine navigates to it (deep link or tap), then captures a screenshot.
- Screenshots are cached to
/tmp/oe-preview-cache/for instant reload on next session. - A file watcher monitors
.swiftfiles — on save, it triggers an incremental rebuild and re-capture.
Full Example
{
"project": "BrunchclubApp.xcodeproj",
"scheme": "BrunchclubApp",
"simulator": "iPhone 16 Pro",
"bundleId": "com.lisbonbrunchclub.app",
"launchArgs": ["-hasCompletedOnboarding", "YES"],
"views": [
{
"name": "WelcomeIntroScreen",
"file": "Sources/Views/OnboardingView.swift",
"label": "Welcome",
"requiresLogout": true,
"launchArgs": [],
"verifyLabels": ["Welcome to Brunchclub"]
},
{
"name": "FeedView",
"file": "Sources/Views/FeedView.swift",
"label": "Feed",
"tapXY": { "x": 40, "y": 832 },
"verifyLabels": ["Curated Selection"]
},
{
"name": "ExploreView",
"file": "Sources/Views/ExploreView.swift",
"label": "Explore",
"tapXY": { "x": 120, "y": 832 }
},
{
"name": "MenuView",
"file": "Sources/Views/MenuView.swift",
"label": "Menu",
"tapXY": { "x": 200, "y": 832 }
},
{
"name": "MapView",
"file": "Sources/Views/TabViews.swift",
"label": "Map",
"tapXY": { "x": 280, "y": 832 }
},
{
"name": "FavoritesView",
"file": "Sources/Views/TabViews.swift",
"label": "Saved",
"tapXY": { "x": 360, "y": 832 }
}
]
}
Auto-Discovery
If views is omitted or empty, OpenEditor scans the project directory for
.swift files and finds all structs conforming to View. Preview provider
structs (ending in _Previews) are excluded.
If project is omitted, the first .xcworkspace or .xcodeproj
found in the directory is used.
Auto-discovery is convenient for getting started, but defining views explicitly gives you control over which views appear on the canvas and enables per-view navigation for unique screenshots.
Prerequisites
- Xcode installed with command line tools (
xcode-select --install) - iOS Simulator booted — boot via Xcode or
xcrun simctl boot "iPhone 16 Pro" - OpenEditor Desktop (macOS) — the Tauri runtime is required for simulator
interaction. Download the current
.pkg,.dmg, or tarball from the download page. - AXe CLI (optional) — for accessibility-based navigation:
brew install cameroncooke/axe/axe
Best Practices
- Place
oe-swift.jsonnext to your.xcodeprojor.xcworkspace. - Keep one
oe-swift.jsonper Swift project. OpenEditor regenerates the canvas per workspace, so Pitcher, Brunchclub, and other local iOS projects can be switched independently. - Boot both an iPhone and iPad simulator for side-by-side responsive previews.
- Add
tapLabelto views that correspond to tab bar items for automatic per-view screenshots. - Add
.oe-buildto your.gitignore— this is where incremental build artifacts are stored. - For apps with deep linking, set
urlSchemeanddeepLinkper view for the most reliable navigation.
Testing with oe-tests.json
Define screenshot-based test specs alongside your project config. Each test is a sequence of actions (tap, navigate, wait) followed by screenshot captures. Run them from the Tests tab in OpenEditor.
{
"project": "MyApp",
"simulator": "iPhone 16 Pro",
"bundleId": "com.example.myapp",
"tests": [
{
"name": "All tabs load",
"description": "Navigate through all tabs and verify each renders",
"steps": [
{ "action": "tapXY", "x": 40, "y": 832, "name": "Tap Feed tab" },
{ "action": "screenshot", "name": "Feed" },
{ "action": "tapXY", "x": 200, "y": 832, "name": "Tap Map tab" },
{ "action": "screenshot", "name": "Map" }
]
}
]
}
Test Spec Fields
| Field | Type | Description |
|---|---|---|
project Optional |
string |
Project name for display. |
simulator Optional |
string |
Default simulator device. Default: iPhone 16 Pro. |
bundleId Optional |
string |
App bundle identifier for the test target. |
tests Required |
array |
Array of test cases, each with a name and steps. |
Step Actions
| Action | Fields | Description |
|---|---|---|
tapXY |
x, y, wait? |
Tap at screen coordinates (points). Waits wait ms after (default 800). |
tapLabel |
label, wait? |
Tap an accessibility label. Requires AXe or XcodeBuildMCP. |
deepLink |
url, wait? |
Open a deep link URL via simctl openurl. |
screenshot |
name? |
Capture a screenshot. Displayed inline in the test results. |
wait |
wait? |
Pause for wait ms (default 1000). Use between actions that need settling time. |
navigate |
url, wait? |
Open a URL in the simulator (same as deepLink). |
Full Example
{
"project": "Brunchclub",
"simulator": "iPhone 16 Pro",
"bundleId": "com.lisbonbrunchclub.app",
"tests": [
{
"name": "All tabs load",
"description": "Navigate all 5 tabs and verify each renders",
"steps": [
{ "action": "tapXY", "x": 40, "y": 832, "name": "Tap Feed" },
{ "action": "screenshot", "name": "Feed" },
{ "action": "tapXY", "x": 120, "y": 832, "name": "Tap Explore" },
{ "action": "screenshot", "name": "Explore" },
{ "action": "tapXY", "x": 200, "y": 832, "name": "Tap Menu" },
{ "action": "screenshot", "name": "Menu" },
{ "action": "tapXY", "x": 280, "y": 832, "name": "Tap Map" },
{ "action": "screenshot", "name": "Map" },
{ "action": "tapXY", "x": 360, "y": 832, "name": "Tap Saved" },
{ "action": "screenshot", "name": "Favorites" }
]
},
{
"name": "Venue detail flow",
"description": "Open a venue card from Explore",
"steps": [
{ "action": "tapXY", "x": 120, "y": 832, "name": "Go to Explore" },
{ "action": "wait", "wait": 600 },
{ "action": "tapXY", "x": 100, "y": 500, "name": "Tap venue card" },
{ "action": "wait", "wait": 1000 },
{ "action": "screenshot", "name": "Venue detail" }
]
}
]
}
The Tests tab includes a Record button that captures a video of the simulator
while tests run. Requires xcodebuildmcp:
brew install getsentry/xcodebuildmcp/xcodebuildmcp
Place oe-tests.json in the same directory as oe-swift.json, next to your
.xcodeproj. OpenEditor auto-discovers it when you switch to the Tests tab.