Tryswift jp simard cover?fm=jpg&fl=progressive&q=75&w=300

Practical Cross-Platform Swift

With Swift now available on non-Apple platforms, you can set your code free to roam beyond just your iOS app. In this presentation from try! Swift, you will discover practical ways to write, test, debug, and deploy cross-platform Swift code without sacrificing the Cocoa and Objective-C functionality where it’s available.


The Goal of Cross Platform Swift Code (1:58)

Today, we’ll learn how one would write cross-platform Swift. We’ll go over:

  1. Setting up a development environment that will get you off the ground running
  2. Using the Swift Package Manager
  3. Proper testing
  4. Continuous integration

In order to get started with cross-platform Swift, it’s important to note the number of platforms development is occurring against. On one hand, it’s two platforms: Darwin and 64-bit Linux. Darwin includes Mac OS, iOS, Watch OS and TV OS. However, because Darwin is fragmented between Swift binaries - the one that Apple ships with Xcode, and the open source version - you can also see at it as three distinct platforms.

Expectation Vs. Reality (2:42)

The aim is to write supercharged Swift code that can run everywhere! My expectation was that I could write pure Swift code and port it over using scripts, but in reality, things don’t work quite as well. For example, you don’t have Grand Central Dispatch (GCD), and you don’t even have Xcode to build for these other platforms.

The Setup (5:05)

You can have something set up on three distinct machines, but ideally, you want to be able to do all of these from the Mac. The three sets of development environment I recommend are:

  1. Xcode for the pre-shipped and pre-built version of Swift that Apple releases with its major toolchains.
  2. The open source toolchain, in combination with Xcode.
  3. Use a Docker to run a virtualized instance of Linux on your Mac, in addition to terminal, and a text editor to edit the code.

Get more development news like this

With the first option, that’s just plain old Xcode, which you’ve already done while writing any sort of Swift code. The second option is part of Xcode 7.3 (still in beta), which allows you to specify a custom version of Swift toolchain to use with your IDE. As a result, you get both SourceKit and LLDB integrated.

The third option is more complex. I recommend you use a tool called Docker, which is a virtualization environment that can be installed from Homebrew. By following these commands from the terminal, you get a Linux environment running on your Mac that’s completely isolated from the system.

$ brew install docker docker-machine
$ docker-machine create --driver virtualbox default
$ eval $(docker-machine env default)
$ docker pull swiftdocker/swift
$ docker run -it -v `pwd`:/project swiftdocker/swift bash

root@445ab1838149 $
cd /project
root@445ab1838149 $ swift build & swift test

Suppose you’re working on a module/framework you want to share across platforms. By running the last command, Docker allows you mount the directory into the environment, allowing you to continue using the shell that you’re familiar with in terminal. It also allows you to continue using your favorite text editor like Sublime, Atom, Textmate, or even Xcode.

Swift Package Manager (9:00)

The Swift Package Manager is a great tool for pulling in Swift dependencies, and it can coexist with both CocoaPods and Carthage, but in the case of cross-platform Swift, using Swift Package Manager is really the only way to build it because both CocoaPods and Carthage relies on Xcode to do the actual building. I really encourage you to use SPM even if you’re just building a single executable. If you manage to architect your code in a way that you can use frameworks and modules and unit tests, you’ll end up thanking yourself later.

Things You Would Expect to Work (10:36)

Unfortunately, some things that seem simple have some hidden dependencies on the Objective-C runtime. For example, the keyword dynamic is a Swift language feature, but it depends on the Objective-C runtime, which means you’ll have to port that.

Casting is also something you’d expect to work (casting is when you convert one type to different type). Casting also heavily depends on the Objective-C runtime.

Then, there are frameworks that are Darwin only, such as Foundation and Grand Central Dispatch. Thankfully, Apple has made the decision to porting these frameworks to Linux, and the lack of support here will only be temporary. The automatic import of frameworks is also an issue, as that’s something Xcode tends to do for you.

The following function is pure Swift, and it should work while porting it over to Linux, but there’s a casting error because the top part of the diff relies on the Objective-C runtime, and that type of casting would not work in Linux.

public func materialize<T>(@autoclosure f: () throws -> T) -> Result<T, NSError> {
   do {
     return .Success(try f())
-  } catch {
-    return .Failure(error as NSError)
+  } catch let error as NSError {
+    return .Failure(error)
   }
 }

To work around the fragmentation, you’ll use a lot of these defined if everywhere in preprocessor definitions. You may need to check for the usage of the Swift Package Manager, or where you don’t have access to all of the system libraries and frameworks. This is something you’ll have to get used to, but we’ll see these limitations reduced as the Swift team and outside contributors help with improving Swift portability.

#if
 SWIFT_PACKAGE
import
 SomeModuleOtherwiseAvailable
#endif

#if os(Linux)
// some arcane hack
#else
// something more reasonable
#endif

Testing (10:36)

Until a few weeks ago, testing with the Swift Package Manager using XCTest required you to clone the Apple-provided library because it didn’t have the SPM required tags you need to expose it as an external dependency. So, you needed to fork it, and then build it as your module dependency. You also needed a target that’s built as part of your actual package.

import PackageDescription

let
 package = Package(
  name: "MyPackage",
  targets: [
    Target(name: "MyPackage"),
    Target(name: "MyPackageTests", // build tests as a regular target...
      dependencies: [.Target(name: "MyPackage")]), // ...that depend on the main one
  ],
  dependencies: [
#if !os(Linux) // XCTest is distributed with Swift releases on Linux
    .Package(
      // no version tags at apple/swift-corelibs-xctest, so fork it
      url: "https://github.com/username/swift-corelibs-xctest.git",
      majorVersion: 0
    ),
#endif
  ]
)

There was a fairly major rewrite recently, and this is available in the latest Swift Package Manager snapshot. The Swift Package Manager will just look at the source layout of your project on disk in the file system and automatically create test targets for this. It’ll use Darwin’s XCTest framework, if it’s available; otherwise it’ll use the snapshot-provided XCTest version (the one that’s built-in in Swift). By doing this, you have access to @testable and you can actually only build your packages when you’re distributing them, rather than also building your tests along with them, and all of XCTest to go with it.

SPM TESTING

Package.swift # can be empty in simplest configuration
Sources/
  MyPackage/
    file.swift
Tests/
  LinuxMain.swift # needs `@testable import MyPackagetest` & `XCTMain()`
  MyPackage/ # *must* be named after package being tested
    test.swift # can `import XCTest` and `@testable import MyPackage`

With Linux, because the Swift XCTest version doesn’t have runtime reflection capabilities, as opposed to the Darwin XCTest version, you have to explicitly state what your test functions are.

Continuous Integration (17:42)

What would it take to set up continuous integration tests with all of our three platforms using something like Travis CI? On Mac, you’ll need to ensure your tests and external dependencies are in order, and for SPM, you’ll have to download the Swift snapshot and tell your path to actually start using that version of Swift as opposed to the one that came with Xcode.

It’s essentially the same on Linux: you download the snapshot, and set up your path.

Travis Configuration

matrix:
  include:
    - env: JOB=OSX_Xcode
    - env: JOB=OSX_SPM
    - env: JOB=Linux

Travis Xcode

script: xcodebuild test
env: JOB=OSX_Xcode
os: osx
osx_image: xcode7.2
language: objective-c
before_install: pod install / carthage update / etc.

Travis OS X SPM

script:
  - swift build
  - .build/Debug/MyUnitTests
env: JOB=OSX_SPM
os: osx
osx_image: xcode7.2
language: objective-c
before_install:
  - export SWIFT_VERSION=swift-DEVELOPMENT-SNAPSHOT-2016-02-25-a
  - curl -O https://swift.org/builds/development/xcode/$(SWIFT_VERSION)/$(SWIFT_VERSION)-osx.pkg
  - sudo installer -pkg $(SWIFT_VERSION)-osx.pkg -target /
  - export PATH=/Library/Developer/Toolchains/$(SWIFT_VERSION).xctoolchain/usr/bin:"${PATH}"

Travis Linux

script:
  - swift build
  - .build/Debug/MyUnitTests
env: JOB=Linux
dist: trusty
sudo: required
language: generic
before_install:
  - DIR="$(pwd)"
  - cd ..
  - export SWIFT_VERSION=swift-DEVELOPMENT-SNAPSHOT-2016-02-25-a
  - wget https://swift.org/builds/development/ubuntu1404/$SWIFT_VERSION/$SWIFT_VERSION-ubuntu14.04.tar.gz
  - tar xzf $SWIFT_VERSION-ubuntu14.04.tar.gz
  - export PATH="${PWD}/${SWIFT_VERSION}-ubuntu14.04/usr/bin:${PATH}"
  - cd "$DIR"

We’re Still in the Early Days (19:58)

One of the reasons I want to share this is to document how this works as of today, but I also want to get an early start. In the meantime, I encourage you to contribute back to the Swift open source projects, whether that’s the Standard Library, the Foundation spin-off, the Swift XCTest, or the compiler. Thank you.

Q&A (21:00)

Q: Do you do foresee Xcode running on non-Darwin OSs in the future?

Probably not, and if it were to happen, it would only happen internally at Apple. First of all, Xcode is not open source. The Swift team has done a great job trying to extract out the sets of functionality, like SourceKit, so most of the IDE support lives outside of Xcode when it comes to Swift. Things like LibIDE or libparse are sets of tools that are portable across platforms, but SourceKit is Darwin only. You can port SourceKit to Linux and have the IDE support such things as code completion and highlighting, and this can be done without porting Xcode itself.

Q: From your slides, what part of the cast depended on the Objective-C runtime?

I don’t know. I’m actually having a difficult time debugging the compiler parts and everything that lives in the Swift project distribution. I know it’s certainly possible to use LLDB with the compiler, but ultimately, it’s not something that I’ve been able to do.

One of the reasons I think it depends on the Objective-C runtime is that’s the mechanism that’s used for runtime type resolution. So, for a dynamic cast that is done at runtime, I would expect that that is somehow linked. But, it’s quite possible it’s due to something else.

About the content

This talk was delivered live in March 2017 at try! Swift Tokyo. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

JP Simard

JP works at Realm on the Objective-C & Swift bindings, creator of jazzy (the documentation tool Apple forgot to release) and enjoys hacking on Swift tooling.

4 design patterns for a RESTless mobile integration »

close