Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .claude/skills/switching-xcode-scheme/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
name: switching-xcode-scheme
description: Use when you need to change the active scheme of the running Xcode app (typically before invoking the Xcode MCP BuildProject/RunSomeTests/RunAllTests tools, which always operate on Xcode's currently-selected scheme), or when you need to discover what schemes or run destinations a workspace defines. Listing-only for run destinations — Xcode 26.x's AppleScript cannot persistently change the active destination, so destination switching must be done manually in the Xcode toolbar.
---

# Switching Xcode Scheme

## Overview

The Xcode MCP tools (`BuildProject`, `RunSomeTests`, `RunAllTests`) always act on the active scheme and active destination of the focused Xcode workspace. They take no scheme or destination arguments. To build a different target or run on a different simulator, you must change Xcode's selection first.

This skill wraps an AppleScript helper that:
- **Sets the active scheme** of the workspace document Xcode currently has focused (works reliably; verified by read-after-write).
- **Lists schemes and run destinations** for that workspace, with optional substring filtering.
- **Cannot change the active run destination** — see the limitation section below.

## When to use

- Before calling Xcode MCP `BuildProject` / `RunSomeTests` / `RunAllTests` tools and the active scheme isn't the target you want.
- The user asks to "build/test/run scheme X" and you don't know whether X is currently selected.
- The user asks "what schemes are in this project?" or "what simulators can I run on?" and you want a definitive list straight from Xcode (rather than parsing project files).

## When NOT to use

- Xcode is not running, or no workspace/project is open. The script will fail with a clear message — don't use it as a way to launch Xcode.
- You need to switch the active run destination (simulator/device). AppleScript silently no-ops on Xcode 26.x. Ask the user to pick the destination in Xcode's toolbar instead.
- You're working with `xcodebuild` (CLI) rather than driving the Xcode app. `xcodebuild` takes `-scheme` and `-destination` flags directly — this skill is for the IDE, not the CLI.

## Quick reference

The helper lives at `xcode-state.sh` in this skill's directory. Run it directly via its absolute path.

| Need to do | Command |
|---|---|
| See which workspace is focused | `xcode-state.sh workspace` |
| Read the current scheme | `xcode-state.sh scheme` |
| Switch the active scheme | `xcode-state.sh scheme "WordPress"` |
| List all schemes | `xcode-state.sh schemes` |
| Find schemes matching a pattern | `xcode-state.sh schemes Jetpack` |
| List all run destinations | `xcode-state.sh destinations` |
| Find destinations matching a pattern | `xcode-state.sh destinations "iPhone 17 Pro"` |

Run with no arguments or `help` to print full usage. Exit code 1 means Xcode is not running / no workspace open / item not found; exit code 2 means usage error.

The scheme `<name>` for `scheme <name>` must match exactly (use the listing command first if unsure). Destination names already include the OS version in parens, e.g. `iPhone 17 Pro (26.4.1)`.

## The run destination limitation

Xcode's AppleScript dictionary defines `active run destination` as a settable property of the workspace document. In Xcode 26.x this property is broken in both directions:
- **Reads** always return `missing value`, even immediately after a successful set and even when Xcode visibly has a destination selected.
- **Writes** don't error, but the underlying `xcuserstate` doesn't update and there's no observable change in subsequent Xcode behavior.

UI scripting via System Events can drive Xcode's toolbar dropdown, but it requires the invoker to have Accessibility permission, which Claude Code typically doesn't have. So this skill doesn't attempt destination writes. If destination switching is essential, ask the user to change it in the Xcode toolbar before running an MCP build.

## Common mistakes

- **Forgetting that scheme set is a precondition for the Xcode MCP tools.** If you call `BuildProject` without first switching scheme, it builds whatever was selected — possibly not what the user asked for. Read the current scheme first if you're unsure.
- **Passing a substring to `scheme <name>`.** That command requires an exact match. For substring search, use `schemes <pattern>` to find the full name first.
- **Treating "set destination" as a no-op safely.** The AppleScript `set` will not error, but the destination will not change. Don't claim success based on the absence of an error.
- **Operating on the wrong workspace.** The helper always targets `active workspace document`. If the user has multiple Xcode windows open, confirm with `xcode-state.sh workspace` before switching schemes.
192 changes: 192 additions & 0 deletions .claude/skills/switching-xcode-scheme/xcode-state.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#!/bin/bash
# Read or change the active scheme of the workspace currently focused in
# the running Xcode application, and list the run destinations available
# to that workspace.
#
# Operates on Xcode's `active workspace document`. Xcode must already be
# running with at least one workspace/project open — this script does not
# launch Xcode and does not open files.
#
# Subcommands:
# workspace Print the path of the active workspace document.
# schemes [substring] List schemes (newline-delimited), optionally
# filtered by case-insensitive substring.
# scheme Print the active scheme's name.
# scheme <name> Set the active scheme. <name> must match exactly.
# destinations [substring]
# List run destinations (newline-delimited),
# optionally filtered by case-insensitive
# substring. Destination names already include
# the OS version in parens, e.g.
# "iPhone 17 Pro (26.4.1)".
#
# This script intentionally does NOT support reading or setting the
# active run destination. Xcode's AppleScript dictionary advertises
# `active run destination` as a property of the workspace document, but
# in Xcode 26.x reads always return `missing value` and writes appear to
# silently no-op. Until Apple fixes this, change the destination by hand
# in Xcode's toolbar, or use UI scripting via System Events (which
# requires Accessibility permission).
#
# Exit codes:
# 0 success
# 1 Xcode not running, or no workspace open, or item not found
# 2 usage error

set -euo pipefail

usage() {
awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"
exit 2
}

# Run an AppleScript snippet against Xcode after first verifying that
# Xcode is running and that it has a workspace document open. The wrapper
# converts those preconditions into clean, single-line error messages
# instead of the noisy AppleScript stack traces you'd otherwise get.
run_osa() {
local script="$1"
osascript <<APPLESCRIPT
if application "Xcode" is not running then
return "ERR:not_running"
end if
tell application "Xcode"
if (count of workspace documents) = 0 then
return "ERR:no_workspace"
end if
set ws to active workspace document
${script}
end tell
APPLESCRIPT
}

handle_error() {
local result="$1"
case "$result" in
ERR:not_running)
echo "error: Xcode is not running" >&2
exit 1
;;
ERR:no_workspace)
echo "error: Xcode has no workspace or project open" >&2
exit 1
;;
ERR:scheme_not_found)
echo "error: no scheme matching the requested name" >&2
echo "hint: run 'xcode-state.sh schemes' to list available schemes" >&2
exit 1
;;
ERR:no_active_scheme)
echo "error: workspace has no active scheme" >&2
exit 1
;;
esac
}

cmd_workspace() {
local result
result=$(run_osa 'return (path of ws as string)')
handle_error "$result"
echo "$result"
}

# Newline-delimited list. AppleScript's text item delimiters are the
# standard way to join a list into a string with a custom separator.
list_named() {
local elementName="$1"
local filter="${2:-}"
local result
result=$(run_osa "
set savedTID to AppleScript's text item delimiters
set AppleScript's text item delimiters to linefeed
set out to (name of every $elementName of ws) as string
set AppleScript's text item delimiters to savedTID
return out
")
handle_error "$result"
if [[ -n "$filter" ]]; then
echo "$result" | grep -i -- "$filter" || {
echo "error: nothing matching '$filter'" >&2
exit 1
}
else
echo "$result"
fi
}

cmd_schemes() {
list_named "scheme" "${1:-}"
}

cmd_destinations() {
list_named "run destination" "${1:-}"
}

cmd_get_scheme() {
local result
result=$(run_osa '
try
return name of active scheme of ws
on error
return "ERR:no_active_scheme"
end try
')
handle_error "$result"
echo "$result"
}

cmd_set_scheme() {
local name="$1"
local escaped
escaped=$(printf '%s' "$name" | sed 's/"/\\"/g')
local result
result=$(run_osa "
try
set s to first scheme of ws whose name is \"$escaped\"
on error
return \"ERR:scheme_not_found\"
end try
set active scheme of ws to s
return name of active scheme of ws
")
handle_error "$result"
echo "$result"
}

if [[ $# -eq 0 ]]; then
usage
fi

cmd="$1"
shift || true

case "$cmd" in
workspace)
[[ $# -eq 0 ]] || usage
cmd_workspace
;;
schemes)
[[ $# -le 1 ]] || usage
cmd_schemes "${1:-}"
;;
scheme)
if [[ $# -eq 0 ]]; then
cmd_get_scheme
elif [[ $# -eq 1 ]]; then
cmd_set_scheme "$1"
else
usage
fi
;;
destinations)
[[ $# -le 1 ]] || usage
cmd_destinations "${1:-}"
;;
-h|--help|help)
usage
;;
*)
echo "error: unknown command '$cmd'" >&2
usage
;;
esac
3 changes: 3 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ control_statement:
discarded_notification_center_observer:
severity: error

opening_brace:
ignore_multiline_statement_conditions: true
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

swift-format puts the opening brace in a new line when there are multiple lines of conditions in the if. There is no option to tweak that configuration in swift-format. So, I updated the swift-lint rule configuration to make swiftlint and swift-format to be compatible.


operator_usage_whitespace:
skip_aligned_constants: false

Expand Down
31 changes: 5 additions & 26 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,33 +38,12 @@ WordPress-iOS uses a modular architecture with the main app and separate Swift p
- **Accessibility**: Use proper accessibility labels and traits
- **Localization**: follow best practices from @docs/localization.md

## Build & Test
## Xcode Schemes
- `WordPress` builds the WordPress iOS app and runs `WordPressUnitTests.xctestplan` — default for builds and the full unit test suite.
- `Jetpack` builds the Jetpack iOS app — switch to it for Jetpack-only work.
- Some test targets (e.g. `WordPressDataTests`) have their own scheme and aren't in `WordPressUnitTests.xctestplan`. If the `WordPress` scheme fails to build because of an unrelated target, fall back to the target's dedicated scheme.

**Always check for the Xcode MCP server first.**
If it is connected, use it to build and test — no exceptions.

If the Xcode MCP fails (e.g. build errors from unrelated targets), fall back to the Fastlane `test` lane:

```bash
bundle exec fastlane test
bundle exec fastlane test only_testing:TargetName/Class/method
```

If Fastlane also fails, fall back to `xcodebuild` directly:

```bash
xcodebuild \
-workspace WordPress.xcworkspace \
-scheme "${SCHEME}" \
-destination "platform=iOS Simulator,name=${DEVICE}" \
test \
| xcbeautify
```

Some test targets (e.g. `WordPressDataTests`) have their own scheme and are not part of the main `WordPress` scheme's test plan.
When the `WordPress` scheme build fails due to an unrelated target, try using the target's dedicated scheme instead.

### Simulator Sign-In
## Simulator Sign-In

To automatically sign in to the app on an iOS simulator, see @docs/simulator-sign-in.md.

Expand Down