BLOG ABOUT

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:

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

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 xcede accepts an --xcodebuildargs argument, 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.)

(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:

It works for the iOS simulator and for Mac apps too (and note I'm using the short version of argument names here):

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:

Build and run using global tasks

Run the open tasks command (hopefully you know how to run Zed commands already) and add:

{
    "label": "SwiftBuild",
    "command": "xcede build",
    "allow_concurrent_runs": false,
    "reveal": "no_focus",
    "hide": "never"
}

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), 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.

Run

Running is a task too. You can buildrun or just plain run:

{
    "label": "SwiftRun",
    "command": "xcede buildrun",
    "allow_concurrent_runs": false,
    "reveal": "no_focus",
    "hide": "never"
}

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": "SwiftBuild" }],

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": "SwiftBuild" }
],
// and the key you actually want:
"f9": [
	"workspace::SendKeystrokes",
	"cmd-s cmd-shift-alt-ctrl-fn-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:

{
    "label": "Build",
    // Other options here are:
    // "command": "xcede build --scheme MyScheme --platform sim --device SomeSimName",
    // "command": "xcede build --scheme MyScheme --platform mac",
    "command": "xcede build --scheme MyScheme --platform device --device 'My Phone'",
    "allow_concurrent_runs": false,
    "reveal": "no_focus",
    "hide": "never"
}

There are no project-specific key bindings, but you could:

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": {
    "Swift": {
        "binary": "/path/to/xcede"
    }
}

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-dap there'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):

{
    "label": "SwiftDebug",
    "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 just launch lldb on that executable, as if you
    // weren't using xcede.
    "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": "SwiftBuild"
    "build": {
        "command": "xcede build"
    }
}

Or, a project debug task (zed command: open project debug tasks):

{
    "label": "MyApp on iPhone 16",
    "adapter": "Swift",
    "request": "launch",
    "program": "xcede:",
    "args": ["--scheme", "My scheme", "--platform", "device", "--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 to be a bug in Zed.
        "args": ["build", "--scheme", "'My scheme'", "--platform", "device", "--device", "'iPhone 16'"]
    }
}

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.