Build, run and debug iOS and Mac apps in Zed instead of Xcode
2025-08-04
This article will get you building iOS and Mac apps in the Zed editor, with a proper build / run / debug cycle, on devices and the iOS simulator. With the help of some new tools I've developed, this makes Zed genuinely usable for building apps for Apple platforms. This is something of a first from what I can tell.
With not too much setup, you can:
- use code completion and navigation (not just the built-in Swift syntax highlighting)
- build Xcode projects
- run iOS apps on the simulator and real devices
- run Mac apps
- debug on the simulator, devices and the Mac
You will, of course, need to jump back to Xcode for things like SwiftUI previews.
Out of the box, Zed comes with a Swift extension which handles plain Swift projects, but it doesn't understand Xcode projects. There are a couple of articles that get you as far as code completion but no further. So, I've put some work into tools that enable a real development process.
Why Zed? I've been using the Zed editor for writing iOS and Mac apps for a while now. I won't try and convince you to use Zed; suffice to say I find pretty much any other modern editor is better than Xcode for actually writing code. So I've been through AppCode (used it for years although it often didn't work properly; now decommissioned), briefly looked into VSCode (meh), used Fleet for a bit (it doesn't seem to quite know what it's about), and now Zed.
Get started
- open your project's folder in Zed (best to pick a fairly straightforward project at this stage – nothing too gnarly)
- Open a Swift file. Zed will offer to install the Swift extension. Do that.
Now after a minute you should have basic syntax highlighting. Most of your imports won't work yet though – so you'll see unresolved symbols, and code completion and command-clicking to navigate around will be pretty limited.
Code completion and navigation
To get Zed to understand your how your code fits together you need to install xcode-build-server. (I won't get into explaining what language servers and build servers are in this article. Read about those elsewhere if you care.)
brew install xcode-build-server
- Run
xcode-build-server config -scheme MyScheme
in your project directory (there are also arguments for workspace and project if you have multiple candidates) - Do a build in Xcode
- Restart Zed
(This created a buildServer.json
file in your project directory. You probably won't look at it again, but it's good to know it's there. See xcode-build-server's repo if you want to know more.)
You should now be able to command-click symbols to navigate around, and code completion should work. (This can be a little slow at first.)
In future, if your symbols stop resolving at some point, run another Xcode build. You shouldn't need to do this often though.
Building and running
Install xcede
xcede
helps you build, run and debug Apple platform apps. It also comes with a debug adapter, which we'll get into soon. Follow xcede's installation instructions and restart Zed.
You can try out xcede
from the command line:
xcede build MyScheme device MyPhone
xcede run MyScheme device MyPhone
- or both at once:
xcede buildrun MyScheme device MyPhone
It works for the iOS simulator and for Mac apps too:
xcede buildrun MyScheme sim "iPhone 16 Pro"
xcede buildrun MyScheme mac
You need tasks
Building and running aren't really concepts in Zed: they're just examples of tasks. Zed's idea of a task is very generic: it's just a way to run a command in a shell. You define tasks yourself (some are created automatically – but not for Swift). There are project tasks and global tasks.
Build
Define a task to build your app. We'll use project tasks for now. Run the Zed command open project tasks
.
(If you don't like the idea of defining tasks for every project, you can skip straight down to the "Use global tasks" section below.)
Zed commands: Hopefully you know this already, but many of Zed's commands are only available from the Command Palette – in the Run menu, or press cmd-shift-P.
Add something like:
{
"label": "Build MyApp",
// Other options here are:
// "command": "xcede build MyScheme sim SomeSimName",
// "command": "xcede build MyScheme mac",
"command": "xcede build MyScheme device 'My Phone'",
"allow_concurrent_runs": false,
"reveal": "no_focus",
"hide": "never"
}
Now choose Run -> Spawn task
from the menu and select your task. Zed will show your build output in a terminal pane. (Sure it's not as convenient as a list of errors, but at least zed has error indicators in the editor as well.)
More beautiful output: xcodebuild's output is pretty hard to read. But if you have
xcbeautify
(install it with brew),xcede
will see it and use it, and your build output will indeed be more beautiful. If you have a different beautifier (or want to invoke xcbeautify with some flags), specify the command in$XCODEBUILD_BEAUTIFIER
. The output will be piped through this command.
Run
Running is a task too. You can buildrun
or just plain run
:
{
"label": "Run MyApp",
// Other options here are:
// "command": "xcede buildrun MyScheme sim SomeSimName",
// "command": "xcede buildrun MyScheme mac",
//
// or without building:
// "command": "xcede run MyScheme device 'My Phone'",
"command": "xcede buildrun MyScheme device 'My Phone'",
"allow_concurrent_runs": false,
"reveal": "no_focus",
"hide": "never"
}
Define more tasks?
In these tasks we've specified a platform and a device. You'll either want to define multiple tasks like "Run on phone", "Run on iPad simulator", etc – or alter the tasks file each time you want to switch. But before you do that, read the next section so you know what your options are. You might decide not to define tasks for every project!
Or, use global tasks
You don't have to define tasks for every project. xcede
offers an alternative: you can supply its parameters in a file rather than as arguments, in either
(ProjectDir)/.xcrc
, or(ProjectDir)/.zed/xcrc
(note no leading dot)
It looks like this:
device=MyPhone
platform=device
scheme=MyApp
Or, because it's just bash code, this:
device=MyPhone; platform=device; scheme="Scheme with spaces"
# device="16 Pro"; platform=sim; scheme="MyApp"
# device="MyPad"; platform=sim; scheme="MyApp"
# platform=mac; scheme="MyAppMac"
which makes it easy to switch by uncommenting a different line. Don't forget to quote values containing spaces!
Now when you run just plain xcede build
or xcede run
(with no other arguments) in your project directory, it'll use the device and scheme in the file. (You can still provide arguments on the command line – they take precedence over what's in the xcrc
.)
This means you can define generic global tasks that work in any project with an xcrc
. No need to define tasks separately in each project! Run the open tasks
command and add something like this:
{
"label": "Build",
"command": "xcede build",
"allow_concurrent_runs": false,
"reveal": "no_focus",
"hide": "never"
}
As you can see, there are lots of ways to slice this depending on what you prefer. You can even go with global tasks in general, and override them at the project level in special cases.
Add keyboard shortcuts
Obviously it's a faff to choose things from menus every time. If you're using global tasks, you can create yourself a keyboard shortcut (run the open keymap
command):
"f9": [ "task::Spawn", { "task_name": "Build" }],
Or, to autosave before building (because who remembers to do that?) you can do some hackery to work around the fact that Zed doesn't let you bind a key to a sequence of tasks:
// some key combo you won't want for anything else:
"cmd-shift-alt-ctrl-fn-f9": [
"task::Spawn",
{ "task_name": "Build" }
],
// and the key you actually want:
"f9": [
"workspace::SendKeystrokes",
"cmd-s cmd-shift-alt-ctrl-fn-f9"
]
Keyboard shortcuts for project tasks
There are no project-specific key bindings, but you could:
- define your task with a generic label like "Build"
- stick with this label in all your projects
- make a keyboard shortcut to run the task called Build as above
Speed up your builds
Building with xcodebuild is often slower than with Xcode. A coupe of things can help:
(1) You can usually get a pretty massive
performance gain by adding this line to your /etc/hosts
:
0.0.0.0 developerservices2.apple.com
Now obviously that's going to stop some stuff working, so don't forget to comment it out when you need to. When you forget (and you will) you'll see what I mean.
(There's a VSCode plugin that actually does this for you, but xcede doesn't go as far as gaining root permission to modify your hosts file 🙀. Not yet, anyway.)
(2) You'll see from your build output (especially if you've beautified it as above) that
the "resolve package versions" step can be unnecessarily slow. You might look into some extra
xcodebuild flags; you can supply these to xcede by setting $XCODEBUILD_FLAGS
.
Bonus: Swift package support
If you define global tasks for build and run, they'll work in a basic way in when you open a
plain Swift project (i.e. not an Xcode project) – they'll run swift build
and swift run
respectively. No xcrc
is required for this.
Debugging
First you need to configure zed to use xcede's DAP wrapper. Add this your settings file (Settings -> Open Settings):
"dap": {
"Swift": {
"binary": "/path/to/xcede-dap"
}
}
What's going on here? If you're interested: the DAP (debugger adapter protocol) layer sits between the IDE and lldb. It provides a standard way for the IDE to talk about debugging, instead of implementing a lot of (in this case) lldb-specific stuff. Now, there are two main reasons for
xcede-dap
:
- There's no obvious way to attach to a process on a device without hackery with python scripts.
xcede-dap
takes care of that for you.- Even so, attaching kind of sucks because you have to launch the app first – and its output is separate. What I wanted was launch-like behaviour. lldb can do that with a local executable, but not on devices/simulators.
xcede-dap
takes care of the launching, and merges the app's console output into debugger output as you'd expect. Plus it can usexcrc
, which is convenient.
Now define a debug task. Again, you can create global debug tasks (zed command: open debug tasks
) or project debug tasks (zed command: open project debug tasks
).
If you went with global build and run tasks above, a global debug task will probably suit you:
{
"label": "Debug",
"adapter": "Swift",
"request": "launch",
// This says we want xcede to manage the process.
// You could pass the path to an executable here, and that
// would launch in the usual way, as if you weren't using
// xcede-dap.
"program": "xcede:",
// This will build your app first, and is optional.
// Or, you can instead refer to an existing task that
// you've defined, like this:
// "build": "Build"
"build": {
"command": "xcede build"
}
}
Now start the debugger (Run -> Start Debugger) and choose your task. Set some breakpoints and make sure it works! You should be able to step through your code, see variables' values and the call stack, and do all the usual things.
A project debug task, if you prefer that, might be more specific:
{
"label": "MyApp on iPhone 16",
"adapter": "Swift",
"request": "launch",
"program": "xcede:",
"args": ["device", "iPhone 16"],
// build is optional.
// And again, task name or command/args json:
"build": {
"command": "xcede",
// Note how even though we're passing an args array
// we still need to put extra quotes around values with
// spaces. This seems like a bug in Zed.
"args": ["build", "'My scheme'", "device", "'iPhone 16'"]
}
}
Testing
The more gaps you fill, the more obvious the remaining gaps become! For now, xcede has no support for running tests, so write yourself a task to run a suitable xcodebuild command. xcbeautify does a nice job with the output.
At some point I'll look into this more. I suspect it might require mods to Zed's Swift extension. We'll see.
That's it!
And there you have it. Perhaps not quite as convenient as the purpose-specific UI in Xcode, but totally usable I find.
If you have any feedback, join the discussion on Zed's github, or add an issue to the xcede repo.
@lxmn@mastodon.social.