Migrating Your Server Side App to Swift 4

Written by Tim on Thursday, September 21, 2017. Last edited on Monday, October 9, 2017.

I have noticed a lot of people on the Vapor slack asking about how to migrate to Swift 4 so I thought I would write a short post about it. Whilst some of this may be relevant to iOS, for the most part it is concentrating on Swift Package Manager so mainly applies to Server-Side Swift. I also won't talk much about the language changes - there are far better posts that already do!

Swift 4

Swift 4 is out! With the release of Xcode 9 this week, most people are now probably running Swift 4. The highlight is that the list of things you need to do for the most part to get your app working in Swift 4 is....NOTHING!

Remember the major upheaval when Swift 3 came out? There were breaking changes galore and you had to do two or three runs of the migrator in Xcode and even then you were still left with hundreds of errors. Now that Swift is another year older and another year maturer, the migration is a lot smoother.

Swift 3.2

To begin with, the Swift 4 toolchain actually comes with two different compilers - a Swift 4 compiler and a Swift 3.2 compiler. Swift 3.2 is a small 'backwards compatible' version of Swift that mainly adds some goodies for compatibility. When you build your project, the compiler will work out what toolchain your Package.swift is using and choose the right compiler. It was also scan all of your dependencies and use the correct compiler for those. This means that you can write your app in Swift 4 and pull in dependencies that haven't been migrated yet and it will compile your app with Swift 4, and the dependencies will be compiled with Swift 3.2 and they will all be linked together.

Yes you read that right, you can use different versions of the language in the same application and they will automagically be linked together! This was briefly mentioned at WWDC this year, but I honestly don't know why this wasn't a bigger deal. No more waiting for dependencies to be updated, so no more migration hell - it just works!

(This is mostly true - in typical Apple 'backwards compatibility' style I have already spotted an issue in Swift Soup (the HTML parser I use for SteamPress) which fails to compile on Linux (naturally) on Swift 3.2 which is blocking me getting SteamPress ready for Swift 4. So I will be trying to fix that ASAP!)

Package.swift

The first step to migrating your application to use the Swift 4 compiler so you can take advantage of all of the great improvements is to change your Package.swift file. This is what the compiler uses to determine the toolchain to use (and is the first step to stop Xcode complaining at your asking you to update your project!). By default, the compiler will select the 3.2 toolchain so the whole world doesn't implode when you upgrade. To specify you want to use Swift 4, you need to add a comment at the top of the file:

// swift-tools-version:4.0

Once you do that, you need to update your code as some of the syntax has changed. Previously a lot of stuff was inferred, such as target names and tests would only be linked if they were in a directory with the same name appended with Tests. Now it is a lot easier to have things in different places and link targets properly. Have a look at the documentation if you want the full breakdown.

To migrate your manifest, the first thing you need to do, which is new is to define your products. Previously these would have been inferred from the directories in Sources but you can now name them explicitly. So for a standard Vapor app, with a Run target and an App target, your products would look like:

let package = Package(
    name: "My Awesome App",
		products: [
		    .library(name: "App", targets: ["App"]),
        .executable(name: "Run", targets: ["Run"])
    ],
		...
		]
)

Next you define your dependencies. This is pretty similar to Swift 3, but .package is now lowercase and the way of specifying versions has changed slightly. The documentation link above goes into way more detail, but you want to do something like:

dependencies: [
    .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "2.1.0")),
],

Finally you need to define each target. These are the targets you referenced in your products section and anyone coming from a recent Vapor template will want to define three targets:

  • App - this is where your code lives
  • Run - the very thin wrapper that actually runs your code and makes it easier to test
  • AppTests - the tests for your App

For each target, you have to define you dependencies - this is important. If you don't define your dependencies then you will find that they won't actually get pulled into the project. Once it is all put together, your new Swift 4 Package.swift should look something like:

// swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "MyAwesomeApp",
    products: [
        .library(name: "App", targets: ["App"]),
        .executable(name: "Run", targets: ["Run"])
    ],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", .upToNextMajor(from: "2.1.0")),
        .package(url: "https://github.com/vapor/fluent-provider.git", .upToNextMajor(from: "1.0.0")),
        .package(url: "https://github.com/vapor/leaf-provider.git", .upToNextMajor(from: "1.0.0")),
        .package(url: "https://github.com/vapor/auth-provider.git", .upToNextMajor(from: "1.0.0")),
    ],
    targets: [
        .target(name: "App", dependencies: ["Vapor", "FluentProvider", "LeafProvider", "AuthProvider"],
                exclude: [
                    "Config",
                    "Database",
                    "Localization",
                    "Public",
                    "Resources",
                ]),
        .testTarget(name: "AppTests", dependencies: ["App"]),
        .target(name: "Run", dependencies: ["App"])
    ]
)

If you have any other dependencies, you'll need to obviously add these in as well. For example, if you are using any of the Vapor test helpers, as the default template uses, you will need to add the Testing dependency.

Libraries

The final thing to note is for those writing libraries - whether it be open source packages for the community or internal modules for your company. You may want to keep some backwards compatibility whilst people migrate. This is relatively easy to do in the code, as you can use macros such as:

#if swift(>=4)
  // Do some Swift 4 stuff
#else
  // Do some Swift 3 stuff
#endif

But what do you do if you want people who are still on Swift 3.1 to be able to use your libraries. The new Package.swift syntax won't work as it isn't backwards compatible. Well, it turns out that there is some handy tricks Swift has! You can keep your original Swift 3 Package.swift as normal and then add a new manifest with the name Package@swift-4.swift. Whilst terminal tends to get confused by the @, the Swift compiler will be able to see the new manifest file and use that instead of the Swift 3 one, providing you with backwards compatibility - pretty neat!

Hopefully that quick overview should answer some of the common questions about moving to Swift 4, but if I've missed anything, just ask in the comments!