🐮 iOS App w/ VisualStudio Code. Or not?

In SwiftUI Tools w/ VisualStudio Code we explored how to use
VisualStudio Code to build macOS apps using the new Swift extension. But can we also build an iOS application using that?

This is an updated version of SwiftUI Tools w/ VisualStudio Code using the new Swift support for Visual Studio Code, which is an updated version of SwiftUI Tools w/ just SwiftPM.

It is well known that building iOS apps doesn’t work using Swift Package Manager only, or is it? In a (very inconvenient) way it now is!

Apple recently introduced the new Swift Playgrounds 4 which features the new ability to build “real” iOS applications. Guess what? This is using a Swift package under the hood. Aaron Sky revisted the mechanism early on and blogged about it over here: Swift Playgrounds App Projects.

In short, Xcode 13.2 adds a new iOSApplication product to the builtin SPM, it looks like this:

import PackageDescription
import AppleProductTypes

let package = Package(
    name: "Tows",
    platforms: [ .iOS("15.2") ],
    products: [
        .iOSApplication(
            name: "Tows",
            targets: ["AppModule"],
            displayVersion: "1.0",
            bundleVersion: "1",
            supportedDeviceFamilies: [ .pad, .phone ],
            supportedInterfaceOrientations: [ .portrait ],
            capabilities: []
        )
    ],
    targets: [ .executableTarget(name: "AppModule", path: ".") ]
)

Let’s see whether can get our Hello World SwiftUI going from within VSCode:

import SwiftUI

@main
struct HelloWorld: App {
  
  var body: some Scene {
    WindowGroup { 
      Text("Hello World!").padding()
    }
  }
}

Installing VisualStudio Code w/ Swift

This is really easy, download VisualStudio Code from Microsofts Download Page, and drag it to the /Applications folder.

One may also want to link up the code tool, so that VSC can be started from the shell:

$ ln -s "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code" \
        /usr/local/bin/code

On startup, VSCode is going to show a little tutorial, though most things are self explanatory. The basics:

  • They call the dock on the left the “activity bar”. It is roughly comparable to the sidebar tab selector in Xcode. Relevant shortcuts:
    • ⌘⇧ E: Jump into the “Explorer” (the file navigation pane) and if the focus is on the Explorer, back to the editor (thanks @pfriedrich_).
    • ⌘⇧ F: Open search pane.
    • ⌘⇧ D: Run & Debug pane (also seems to be used for tests).
    • ^⇧ G: Git pane.
  • A terminal can be brought up using Ctrl-Backtick (similar to M-x shell)
  • More shortcuts:
    • Navigate forward/backward: ⌘⌥ < / ⌘⌥ >
    • F5: Start in debugger, ^ F5: just run
    • F12: Jump to definition of a symbol
    • ⌘⇧ N: Open a new VSCode window
    • ⌘⇧ P: Open “command palette”, this is similar to M-x in Emacs, one can invoke all the available functions.
  • Usually you seem to open just one “folder” in a VSCode window (similar to Xcode), which is your project root (though you can also create workspaces, again similar to Xcode).

Afterwards one can install the “Swift Language Support for Visual Studio Code” extension using the extensions button in the activity bar. Search for “Swift”, should be the top hit:

So far so gut, the ARI ran into no issues.

Setting up the App Package and Build

The new Swift extension for VisualStudio Code doesn’t yet support creating new Swift projects. So that part still has to be done in the shell.

First make sure that you have Xcode 13.2 installed and that Swift 5.5 is active:

$ swift --version
swift-driver version: 1.26.21 Apple Swift version 5.5.2 (swiftlang-1300.0.47.5 clang-1300.0.29.30)
Target: arm64-apple-macosx11.0

If it is not: sudo xcode-select -s /Applications/Xcode.app.

Next create the package boilerplate:

$ mkdir Tows && cd Tows
$ swift package init --type executable
Creating executable package: Tows
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/Tows/main.swift
Creating Tests/
Creating Tests/TowsTests/
Creating Tests/TowsTests/TowsTests.swift

This needs to be massaged a little more. First we need to rename the main.swift to something else. main.swift is a special file which essentially wraps the whole content in a big function (i.e. you can run statements like print("Moo!") at the top level). This clashes with how @main works.

$ mv Sources/Tows/main.swift \
     Sources/Tows/Tows.swift

This can be opened in VisualStudio Code now, to do so, just type:

$ code .

It should come up like this:

Replace the contents (the print("Hello")) with our app as shown above:

import SwiftUI

@main
struct HelloWorld: App {
  
  var body: some Scene {
    WindowGroup { 
      Text("Hello World!").padding()
    }
  }
}

VSCode is going to show a set of errors:

Next we need to adjust the Package.swift to build an iOS product:

// swift-tools-version:5.5

import PackageDescription
import AppleProductTypes

let package = Package(
    name: "Tows",
    platforms: [ .iOS("15.2") ],
    products: [
        .iOSApplication(
            name: "Tows",
            targets: ["Tows"],
            displayVersion: "1.0",
            bundleVersion: "1",
            supportedDeviceFamilies: [
                .pad,
                .phone
            ],
            supportedInterfaceOrientations: [
                .portrait,
                .landscapeRight,
                .landscapeLeft,
                .portraitUpsideDown(.when(deviceFamilies: [.pad]))
            ],
            capabilities: []
        )
    ],
    dependencies: [],
    targets: [
        .executableTarget(
            name: "Tows",
            dependencies: []),
        .testTarget(
            name: "TowsTests",
            dependencies: ["Tows"]),
    ]
)

This is going to show an error that the AppleProductTypes (which contains the iOSApplication product) is not available:

Why is this? It is because the SwiftPM one can invoke in Terminal (e.g. using swift build) is different (🙄) to the SwiftPM built into Xcode! Not sure anything can be done about, maybe? We’ll ignore the error for now.

The other thing is that Tows.swift still says that macOS BS is required. Looks like the Swift extension still has a bug w/ refreshing things when the platform changes. This can be worked around by renaming the Tows.swift file to Tows2.swift (just click on the filename in the explorer), and et voilà, the source looks proper now:

The answer is kinda obvious, but can we run the app in the simulator by pressing F5? Unfortunately not:

OK, but VSCode lets us “configure” the swift build “task” (we intentionally didn’t reindent the generated JSON to visualize how wrong it is to used tabs for formatting …):

{
	"version": "2.0.0",
	"tasks": [
		{
			"type": "swift",
			"command": "swift",
			"args": [
				"build",
				"--build-tests"
			],
			"group": "build",
			"problemMatcher": [],
			"label": "swift: Build All",
			"detail": "swift build --build-tests"
		}
	]
}

We know we can’t use the Terminal SwiftPM. But we can invoke the Xcode SwiftPM by the means of the xcodebuild tool. Let’s try that:

{
	"version": "2.0.0",
	"tasks": [
		{
			"type": "swift",
			"command": "xcodebuild",
			"args": [
				"-scheme",
        "Tows",
        "-destination",
        "platform=iOS Simulator,OS=15.2,name=iPhone 13 Pro"
			],
			"group": "build",
			"problemMatcher": [],
			"label": "swift: Build All",
			"detail": "xcodebuild"
		}
	]
}

Doesn’t help when pressing F5. Unfortunatly ARI’s VSCode skillz are too weak on how to invoke such a task. But presumably, by reconfiguring all those, one could get it running automagically?

But we can invoke the shell using Ctrl-Backtick and run the build within that:

$ xcodebuild -scheme Tows \
  -destination 'platform=iOS Simulator,OS=15.2,name=iPhone 13 Pro'

This prints out a lot of errors we love to ignore, but fails with:

xcodebuild: error: Unable to find a destination matching the provided destination specifier:
                { platform:iOS Simulator, OS:15.2, name:iPhone 13 Pro }

and suggests only macOS destinations:

Available destinations for the "Tows" scheme:
        { platform:macOS, arch:arm64, id:00008103-000E618A3662001E }
...

Very weird. The setup is almost identical to what Swift Playgrounds produce 🧐 But there is one difference to those, the extension of the package folder is .swiftpm 💡

$ cd .. && mv Tows Tows.swiftpm && cd Tows.swiftpm && code .

And then within the VSCode terminal (Ctrl-Backtick):

$ xcodebuild -scheme Tows \
  -destination 'platform=iOS Simulator,OS=15.2,name=iPhone 13 Pro'

Nice! 🍻 Inconvenient, but works. It built and put the Tows.app into the DerivedData folder configured in Xcode.

Running the App in the Simulator

But how do we run that app? Using a small tool called simctl. First we need to figure out the ID of the simulator we want to use:

$ xcrun simctl list devices "iPhone 13 Pro" available | grep iPhone
    iPhone 13 Pro (9932C857-414E-4D9A-90DB-4E64A0B72B83) (Shutdown) 
    iPhone 13 Pro Max (759D3A4B-248E-4BC5-98C0-5CDABEC5E606) (Shutdown) 

Note how it says the simulator is “Shutdown”. We need to start it:

$ alias sim=/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator
$ export ID=9932C857-414E-4D9A-90DB-4E64A0B72B83
$ sim -CurrentDeviceUDID $ID >/dev/null &

Then we can install our app:

$ alias sc="xcrun simctl"
$ sc install $ID \
  /tmp/DerivedData/Tows-bsyramamusgwwvgucsqrnwzhfvyb/Build/Products/Debug-iphonesimulator/Tows.app

And launch it:

$ sc launch $ID Tows

Splendid! 🍻🍻 Very manual, but works.

But can we write more complex apps with that? Something cowtastic? Yes we can!

          (__)
        /  .\/.     ______
       |  /\_|     |      \
       |  |___     |       |
       |   ---@    |_______|
    *  |  |   ----   |    |
     \ |  |_____
      \|________|
CompuCow Discovers Bug in Compiler

🐮 A Cowtastic App 🐮

This thing, as a SwiftPM tool, in 82 lines of code (including support for search, selection and dragging):

We are going to use the Swift cows package, let’s add it as a dependency to Package.swift (remember to add both, the package and the target dependency):

// swift-tools-version:5.5

import PackageDescription
import AppleProductTypes

let package = Package(
    name: "Tows",
    platforms: [ .iOS("15.2") ],
    products: [
        .iOSApplication(
            name: "Tows",
            targets: ["Tows"],
            displayVersion: "1.0",
            bundleVersion: "1",
            supportedDeviceFamilies: [
                .pad,
                .phone
            ],
            supportedInterfaceOrientations: [
                .portrait,
                .landscapeRight,
                .landscapeLeft,
                .portraitUpsideDown(.when(deviceFamilies: [.pad]))
            ],
            capabilities: []
        )
    ],
    dependencies: [ // Add this!
        .package(url: "https://github.com/AlwaysRightInstitute/cows",
                 from: "1.0.10")
    ],
    targets: [
        .executableTarget(
            name: "Tows",
            dependencies: [ "cows" ]) // Add this!
    ]
)

Since the Swift VSCode extension can’t properly run the Xcode SPM, the new “Package Dependencies” section will NOT pop up in the sidebar 😢

Lets update the application code (Tows2.swift):

import SwiftUI
import cows // @AlwaysRightInstitute

struct ContentView: View {
  
  @State var searchString  = ""
  @State var matches       = allCows
  @State var selectedCow   : String?
    
  var body: some View {
    VStack {
      TextField("Search", text: $searchString)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .padding(8)
        .onChange(of: searchString) { nv in
          matches = nv.isEmpty
                  ? cows.allCows
                  : cows.allCows.filter { $0.contains(searchString) }
        }
      
      Divider()
      
      VStack(spacing: 0) {
        if matches.isEmpty {
          Text("Didn't find cows matching '\(searchString)' 🐮")
            .padding()
            .font(.title)
          Divider()
        }
        
        ScrollView {
          ForEach(matches.isEmpty ? allCows : matches, id: \.self) { cow in
            Text(verbatim: cow)
              .font(.body.monospaced())
              .onDrag { NSItemProvider(object: cow as NSString ) }
              .padding()
              .background(
                RoundedRectangle(cornerRadius: 16)
                  .strokeBorder()
                  .foregroundColor(.accentColor)
                  .padding(4)
                  .opacity(selectedCow == cow ? 1 : 0)
              )
              .frame(maxWidth: .infinity)
              .contentShape(Rectangle())
              .onTapGesture {
                selectedCow = selectedCow == cow ? nil : cow
              }
          }
        }
      }
    }
  }
}

@main
struct Tows: App {
  
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
  }
}

And back to our manual cycle:

  1. compile
  2. install
  3. run
$ xcodebuild -scheme Tows \
  -destination 'platform=iOS Simulator,OS=15.2,name=iPhone 13 Pro'
$ sc install $ID \
  /tmp/DerivedData/Tows-bsyramamusgwwvgucsqrnwzhfvyb/Build/Products/Debug-iphonesimulator/Tows.app
$ sc launch $ID Tows

Cowtastic! 🍻🍻🍻

Closing Notes

So can you develop iOS apps w/o opening Xcode and with just SwiftPM? Yes, you can.
Do you want to? Probably not, too much manual work to get going.

Though we think for some VSCode wizard it might actually be possible to just tweak the tasks to do the right things and get F5 working? Let us know if you know how!

The Swift for Visual Studio Code extension is still pretty nice and a good starting point. It has some limitations, but Adam Fowler and Steven Van Impe did a great job.

Want to have a readymade 🐮 app that is properly reviewed by Apple’s AppStore team?

There is CodeCows. Which (amongst other things) features a (language aware) Xcode extension (yes!) and support for macOS services (i.e. automatic ASCII Cows support in any Cocoa text field).

And for iOS there is ASCII Cows. Includes a Messages app and proper Markdown support, so that you can paste the cows into WhatsApp properly.

Contact

Feedback is warmly welcome: @helje5, wrong@alwaysrightinstitute.com. GitHub.

Want to support my work? Buy an app! You don’t have to use it! 😀

Written on December 29, 2021