Oct 15, 2018

Sharing modules using the Swift package manager

Tools like CocoaPods and Carthage work great when you share code with other developers, but they are not so good for local work. We often have code which should be reused between multiple apps. Or we may want to split business logic into separate modules for a massive app. Mentioned dependency managers can work with local frameworks, but their workflow is not optimized for this. Luckily, Swift 4 added support for local packages as dependencies. I will show how to edit multiple packages in a single Xcode project.

Shared Modules

So we’re building a Swift ‘Project’ with two dependencies: ‘ModuleA’ and ‘ModuleB’.

Launch Terminal and go to the working directory, for example by running ‘cd Documents’. Then perform the following commands:

$ mkdir Project && cd Project

$ mkdir ModuleA && cd ModuleA && swift package init
$ git init && git add . && git commit -a -m "Init ModuleA" && cd ..

$ mkdir ModuleB && cd ModuleB && swift package init
$ git init && git add . && git commit -a -m "Init ModuleB" && cd ..

You have just created two shared modules in two separate repositories for the future project. Their code is almost the same. We’ll change it later.

Parent Mac App

While we’re in the folder Project, let’s create a client for the modules above:

$ mkdir MacApp && cd MacApp && swift package init --type executable
$ open Package.swift

The second line opens the file ‘Package.swift’ describing dependencies for the Mac app. You should change its implementation a bit:

// swift-tools-version:4.2
import PackageDescription

let package = Package(
  name: "MacApp",
  dependencies: [
    .package(path: "../ModuleA"),
    .package(path: "../ModuleB"),
  ],
  targets: [
    .target(name: "MacApp", dependencies: ["ModuleA", "ModuleB"]),
    .testTarget(name: "MacAppTests", dependencies: ["MacApp"]),
  ]
)

Notice how ‘ModuleA’ and ‘ModuleB’ are referenced using local, relative directories, using an API named ‘path:’. Time to save changes and go back to the Terminal:

$ git init && git add . && git commit -a -m "Init MacApp"

Now the ‘Project’ module is committed in git, just like its dependencies.

Xcode Project

For more convenient editing, let’s create an “umbrella” Xcode project:

$ swift package generate-xcodeproj && open ./MacApp.xcodeproj

Now locate the file ‘MacApp/Dependencies/ModuleA/ModuleA.swift’ in the Project navigator and change it to something like this:

public struct ModuleA {
  public static let text: String = "Module A"
}

Then change the file ‘MacApp/Dependencies/ModuleB/ModuleB.swift’ from the second module:

public struct ModuleB {
  public static let text: String = "Module B"
}

Finally, the Mac app itself needs to import and use everything we wrote above, so find and edit the file ‘MacApp/Sources/MacApp/main.swift’:

import ModuleA
import ModuleB

print(ModuleA.text)
print(ModuleB.text)

It’s done, hit ‘Command-R’ and check your console in Xcode:

Module A
Module B
Program ended with exit code: 0

Next up is the best part.

Change Management

Now your IDE shows three nice “M”-icons next to all edited files. And if we check the Terminal, something amazing exposes:

git status
  modified: Sources/MacApp/main.swift

cd ../ModuleA && git status
  modified: Sources/ModuleA/ModuleA.swift

cd ../ModuleB && git status
  modified: Sources/ModuleB/ModuleB.swift

To be more specific, by editing and compiling those files in one Xcode workspace, we introduced changes in separate git repositories. Which means that if you make any changes in the child modules, you can commit and push them separately. No source code duplication, no rebuilding, it works like a charm.

I found only two flaws with this approach. First, in order to add Tests to the module, you need to generate and open a separate Xcode project. Fortunately, this is not a big problem. Second, the parent app always uses the latest version of the module’s code. To build an older version of the app, you should remember original commits of its modules and rewind, which is not very convenient.

Let me know if you have any questions!