Build, run and debug iOS and Mac apps in Zed instead of Xcode
2025-08-04
There's now a followup article: Now you can test Xcode apps and Swift packages in Zed.
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 your apps on the simulator, devices and the Mac
- build and run plain Swift packages
- run tests right from your code, just like in Xcode (see the followup article for this)
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.
Used in the real world
This isn't just an article about what you could do in theory – I built my new app, DelayDrop, mainly in Zed. It's a pleasure to work in a genuinely good code editor, and switching into Xcode for things like previews really isn't an imposition.
DelayDrop is an iOS and Mac app for sending anything to your other Apple devices – even if they're locked, off or elsewhere – in two taps. It beats AirDrop because your other device can be anywhere in the world; and even if it is nearby, you're still winning because you don't have to get up and unlock it.
And if that sounds like a plug for DelayDrop, that's because it is!
Get started
- Open your project's folder in Zed.
- 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.
Using a workspace? This article and the tool generally assume you're using a single Xcode project, with SPM for dependencies and perhaps with other nested packages. If you're still using Cocoapods or have a workspace for other reasons, things might not go to plan. Feel free to raise an issue to discuss your use case. (Also note that
xcedeaccepts an--xcodebuildargsargument, which might be helpful.)
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 MySchemein 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 --scheme MyScheme --platform device --device MyPhonexcede run --scheme MyScheme --platform device --device MyPhone- or both at once:
xcede buildrun --scheme MyScheme --platform device --device MyPhone
It works for the iOS simulator and for Mac apps too (and note I'm using the short version of argument names here):
xcede buildrun -s MyScheme -p sim -d "iPhone 16 Pro"xcede buildrun -s MyScheme -p 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 (although some are created automagically by language plugins).
You have a choice:
-
Global tasks:
- define build and run tasks once, and they'll available to all projects
- project-specific parameters (like scheme) are handled in a small config file:
.xcrc - I favour this because it's less setup for each project
-
Or, project tasks:
- define build and run tasks in every project you work on
- project-specific parameters like scheme are specified in the task definition, as command line args
- no need for a config file
Build and run using global tasks
Run the open tasks command (hopefully you know how to run Zed commands already) and add:
Notice we haven't said what scheme to build or anything! You can specify this on the command line, but we don't want to do that here because this is a generic task that works in any project.
Instead, we create a project-specific xcede config file. In your project directory, create .xcrc (or .zed/xcrc if you prefer), containing something like:
scheme=MyApp
# platform can be device, sim or mac
platform=device
# device name (not required if platform = mac)
device=Tarquin's iPhone 17
Now choose Run → Spawn task from the menu and select SwiftBuild. Zed will show your build output in a terminal pane. Nice. (Sure it's not as convenient as a list of errors, but at least there are error indicators in the editor as well.)
You'll probably want to target different devices and platforms. To do this, alter the config file. You can make this a bit less painful with this sort of thing:
# Note you can have multiple settings per line:
platform=device; device=Tarquin's iPhone 17
# platform=sim; device="iPhone 16 Pro"
# platform=sim; device="iPad Air"
# platform=mac
scheme = MyApp # spaces if you like
which makes it easier to switch by uncommenting a different line. You can see the file format is pretty flexible.
(Your other option is creating project-specific tasks (see below) and possibly editing task definitions as you go. Up to you. By the way: Zed doesn't let extensions have UI, so there's no possibility of an Xcode-like scheme and destination selector.)
More beautiful output: xcodebuild's output is pretty hard to read. But if you have
xcbeautify(install it with brew),xcedewill 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.
Run
Running is a task too. You can buildrun or just plain run:
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": ,
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": ,
// and the key you actually want:
"f9":
Project-specific tasks
If you prefer, you can define tasks for each project, in which case you can do without a .xcrc file. xcede
accepts options on the command line too (see xcede --help, xcede build --help, etc).
Run the Zed command open project tasks and add this sort of thing:
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
Bonus: Swift package support
If you've opened a Swift package rather than an Xcode project, xcede still supports building
and running, so your global tasks will still work. No .xcrc is required for this.
It just runs plain swift build and swift run, so if you need anything more elaborate,
define a project task.
Debugging
First you need to configure zed to use xcede's DAP wrapper. Add this your zed settings file (Settings → Open Settings):
"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: without this, IDEs would have to implement a different protocol for every debugger they talked to. Communication looks like this: IDE (Zed) → lldb-dap → lldb.
When launched without arguments, xcede runs as a DAP server. This adds another layer, so then communication looks like this: IDE (Zed) → xcede → lldb-dap → lldb. Mostly xcede passes commands straight through to lldb-dap, but it adds some useful functionality:
- With
lldb-dapthere's no obvious way to attach to a process on a device without hackery with python scripts. xcede takes care of that for you.- Even so, attaching kind of sucks because you'd have two separate manual steps to do: first launch the app, then attach with the debugger. Also, its console output would be in a separate zed terminal panel. What I wanted was launch-like behaviour (like you get in Xcode). lldb can do that with a macOS executable, but not on devices/simulators. xcede takes care of the launching, and merges the app's console output into debugger output as you'd expect. Plus it can use
xcrc, which is convenient.
Now define a debug task. Again, you can create global debug tasks or project debug tasks.
Global debug task (assumes you have a .xcrc file as described above; zed command: open debug tasks):
Or, a project debug task (zed command: open project debug tasks):
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.
Testing
BREAKING NEWS: With a new update, you can now run tests right from your code, just like in Xcode. Read about that in the next article: Now you can test Xcode apps and Swift packages in Zed.
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.
There's now a followup article: Now you can test Xcode apps and Swift packages in Zed.