Developing Swift for iOS in VSCode


Introduction

Swift has come along away since it was open sourced. Last year, I configured my Linux desktop for developing in Swift and was able to take this pretty far (for command line executables, there is no cross platform Swift UI SDK … yet 😃):

  1. Generate Swift packages from the command line
  2. Develop Swift code from VSCode with autocomplete
  3. Compile Swift code from VSCode
  4. Debug Swift code from VSCode

Although writing Swift code is an absolute joy 💯, writing any code in Xcode is … let’s just say less than ideal.

Now that I have a new MacBook Pro, I really wanted to try using a similar setup on macOS. VScode is such a powerful IDE with many useful extensions that thought of being stuck in Xcode was depressing. In practice, I will still have to use Xcode for app development, but I would like to jump into VSCode when developing anything significant, and switch back into Xcode as needed.

A personal friction point for me is the lack of VIM keybindings. Adding these to Xcode is not trivial either since plugins are not supported and Xcode has to be modified and code signed. On the other hand, the VIM extension in VSCode seems pretty stable meets my needs so far.

Prerequisite

These instructions assume you have already installed a Swift toolchain. If not, head on over to:

https://swift.org/download/#snapshots

and download a macOS pkg (labelled as Xcode).

Warning

The version of the toolchain must match the version of sourcekit-lsp that you will be installing. Detailed instructions can be found here.

Tip

Use a symbolic link to manage multiple copies of installed toolchains by having the link point the currently desired version:

 > pwd
/Library/Developer/Toolchains

> tree . -L 1                                                                                    .
├── swift-5.1-DEVELOPMENT-SNAPSHOT-2019-12-20-a.xctoolchain
├── swift-5.2-DEVELOPMENT-SNAPSHOT-2020-01-06-a.xctoolchain
└── swift-latest.xctoolchain -> swift-5.1-DEVELOPMENT-SNAPSHOT-2019-12-20-a.xctoolchain/

Problem

There are two VSCode extensions that provide Swift autocompletion:

  1. A marketplace published plugin called Maintained Swift Development Environment
  2. An unpublished plugin offered by Apple as part of sourcekit-lsp

Setting up either plugin is relatively straightforward, however neither supports developing against an iOS SDK out of the box. In practice this means most code in a typical app project will appear full of errors since iOS specific types can not be resolved (i.e. those from UIKit).

The Maintained Swift Development Environment states that it can be configured to work with UIKit but I was unable to get this to work, and there are some open issues against this project which seem to indicate it’s support is fragile (it apparently parses the build output YAML to extract compiler flags).

Solution

Create a Swift Package

To walk through a working solution we will first need to create a Swift package which we can build and demo with:

swift package init --type library

Setting Platform to iOS

First, your Swift package should define the platform as iOS. In your Package.swift make sure to add this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let package = Package(
name: "Foo",
platforms: [ .iOS(.v13)],
products: [
    // Products define the executables and libraries produced
    // by a package, and make them visible to other packages.
    .library(
        name: "Foo",
        targets: ["Foo"]),
],

Add an iOS Dependency

Next, update one of your source files to have an iOS dependency. The most obvious one is UIKit:

1
2
3
4
5
6
import UIKit

func testing() {
    let button = UIButton(type: UIButton.ButtonType.system)
    button.tag = 42
}

Build from Command Line

At this point if you build your package with:

swift build

you will immediately get an error about UIKit not being found:

<snip>/Sources/Foo/Foo.swift:1:8: error: no such module 'UIKit'
import UIKit

To make this work, we will need to add some Swift compiler flags which specify the SDK to use and the target output (using fish shell syntax, bash is similar):

swift build -Xswiftc "-sdk" -Xswiftc \
(xcrun --sdk iphonesimulator --show-sdk-path) \
-Xswiftc "-target" -Xswiftc "x86_64-apple-ios13.2-simulator"

Compiling should now be successful 💪!

If you have the sourcekit-lsp executable already installed, you will notice that it too accepts Swift compiler flags:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
> sourcekit-lsp --help
OVERVIEW: Language Server Protocol implementation for Swift and C-based languages

USAGE: sourcekit-lsp [options]

OPTIONS:
  --build-path          Specify build/cache directory
  --configuration, -c   Build with configuration (debug|release) [default: debug]
  --log-level           Set the logging level (debug|info|warning|error) [default: info]
  -Xcc                  Pass flag through to all C compiler invocations
  -Xcxx                 Pass flag through to all C++ compiler invocations
  -Xlinker              Pass flag through to all linker invocations
  -Xswiftc              Pass flag through to all Swift compiler invocations
  --help                Display available options

By providing the same flags to the language sever we should be able to achieve the same results within VSCode.

Modify Apple’s SourceKit-LSP VSCode Extension

Now that we know how to make the project compile, we can carry over the same compiler flags to the extension provided by Apple which allows VSCode to integrate with their language server.

To do this, clone the sourcekit-lsp repo:

git clone https://github.com/apple/sourcekit-lsp.git

Add Configuration Parameters

Next we need to add some configuration options to the extension. Modify the file Editors/vscode/package.json to have these additional configuration parameters:

{
            "sourcekit-lsp.iosSdk": {
                "type": "string",
                "default": "",
                "description": "The path to the desired iOS SDK (i.e. xcrun --sdk iphonesimulator --show-sdk-path)"
            },
            "sourcekit-lsp.iosTarget": {
                "type": "string",
                "default": "",
                "description": "The name of target to generate code for"
            }
}

Use the Configuration Parameters

The last modification to the extension is to read the values for the new configuration parameters and pass these to the language server. To do this, modify the file Editors/src/extension.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function activate(context: vscode.ExtensionContext) {

const config = vscode.workspace.getConfiguration('sourcekit-lsp');
const iosSdk = config.get<string>('iosSdk', 'sourcekit-lsp');
const iosTarget = config.get<string>('iosTarget', 'sourcekit-lsp');
let iosFlags: string[] = [];

if (iosSdk) {
    iosFlags = iosFlags.concat(['-Xswiftc', '-sdk', '-Xswiftc', iosSdk])
}

if (iosTarget) {
    iosFlags = iosFlags.concat(['-Xswiftc', '-target', '-Xswiftc', iosTarget])
}

console.log("iosFlags set to: "  iosFlags)

    const sourcekit: langclient.Executable = {
        command: config.get<string>('serverPath', 'sourcekit-lsp'),
        args: iosFlags
    };

<snip> 
}

The changes are straightforward:

  1. Read the configuration parameters for iOS
  2. Pass them along to the invocation of the language server

Build the extension

From the root of the sourcekit-lsp repo, simply run:

npm run createDevPackage

Install the VSCode Extension

First you will need to have code in your path (below is for fish shell on macOS, just modify $PATH if using bash):

set -U fish_user_paths \
/Applications/Visual\ Studio\ Code.app/Contents/Resources/app/bin/ $fish_user_paths

Next, run:

code --install-extension out/sourcekit-lsp-vscode-dev.vsix

Alternatively, you can install the locally built extension by menu clicking in VSCode:

  1. Code
  2. Preferences
  3. Extensions
  4. Click the ... in the top right of the pane
  5. Select Install from VSIX ...

Install of local extension

Configure the SourceKit-LSP extension

Finally, make sure the sourcekit-lsp extension is configured. This can be done through menus:

  1. Code
  2. Preferences
  3. Settings
  4. Select Extensions
  5. Select SourceKit-LSP

Configuring sourcekit-lsp extension

Alternatively, you can edit your $HOME/vscode/settings.json directly:

{
"sourcekit-lsp.serverPath": "/Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin/sourcekit-lsp",
"sourcekit-lsp.toolchainPath": "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk",
"sourcekit-lsp.trace.server": "messages",
"sourcekit-lsp.iosSdk": "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.2.sdk",
"sourcekit-lsp.iosTarget": "x86_64-apple-ios13.2-simulator"
}

Testing

Now open your Swift package which had the dependency to UIKit. Before this had error and no autocompletion:

UIKit not working in VSCode

Now, the import of UIKit is not flagged as an error, and autocompletion works for types from this framework (such as UIButton).

UIKit working in VSCode

Happy coding!

swift  ios  devenv