Modularize Xcode Projects using local Swift Packages
By: Philip Niedertscheider
Tags: Swift, iOS, Swiftui, Macos

Swift Package Manager… SPM…It is everywhere, many use it and it is most likely the future of working with Swift dependencies. A single file to fetch all the sweet Open Source packages. And with higher acceptance of the community, even more packages will get available without installing any more tools, such as Cocoapods or Carthage.

But how can we leverage this dependency structure even further? Is external code the only reason for using a package manager?

Our code bases are growing with every single new file. First we create a folder structure to organize our .swift files, but then even the slightest code requires Xcode to recompile everything. Our build process becomes slower… and slower… and ….… *goes away to grab a coffee while waiting for Xcode to finish compilation* ….… slower.

Even worse when working with feature-rich, large-scale apps. They become clunky and you spend a lot of time waiting for rebuilding unchanged parts when you just want to iterate your own new, fresh feature.

Example:

A feature-rich receipt tracking app, which connects to your bank account for matching transactions, uses a cloud for live-synchronization, sharing accounts with friends etc.

You want to add a scanning feature, which takes a photo and converts it into the receipt data your app uses.

SPM to the rescue!

Swift Package Manager allows us to create small, reusable code packages. One the one hand this allows to isolate unchanged code during the build process, and on the other hand it gives us the opportunity to simply create a spin-off demo version of the app, with only the necessary parts to improve a single feature.

Continuing the example above:

Using local SPM packages, you can create a small prototyping-app which only shows the scan feature. When the feature is done, it can be used in the main app.

Let me give you a quick overview on how we are going to build our own multi-platform Calculator as an iOS app and a command line tool (the guide for creating the iOS app can be applied for macOS too):

Create a starter SPM command line tool Moving logic code into own SPM library Create the iOS project using the library Create more local libraries to build a dependency graph

If you more interested in the final solution, checkout this GitHub repository for the final code.

iOS app and command line executable offering the same functionality

Creating a SPM command line tool

Start off launching your Terminal of choice (for me it’s iTerm2). Then go ahead and create a new folder called Calculator and afterwards change the working directory into that folder:

$ mkdir Calculator

$ cd Calculator

Next step is initializing our Swift package. The Swift Command Line Interface (CLI) allows us to create multiple types of packages. To figure which ones, run swift package init --help for a list:

$ swift package init --help

OVERVIEW: Initialize a new package OPTIONS:

--name Provide custom package name

--type empty|library|executable|system-module|manifest

Our main focus is on library and executable . If you are just creating a library package, run swift package init --type library , but in our case we want to start with an executable (leading $ means it is a command):

$ swift package init --type executable

Creating executable package: Calculator

Creating Package.swift

Creating README.md

Creating .gitignore

Creating Sources/

Creating Sources/Calculator/main.swift

Creating Tests/

Creating Tests/LinuxMain.swift

Creating Tests/CalculatorTests/

Creating Tests/CalculatorTests/CalculatorTests.swift

Creating Tests/CalculatorTests/XCTestManifests.swift

Awesome! You created your first Swift package 🔥

Our folder structure now looks like the following:

Calculator

├── Package.swift

├── README.md

├── Sources

│ └── Calculator

│ └── main.swift

└── Tests

├── CalculatorTests

│ ├── CalculatorTests.swift

│ └── XCTestManifests.swift

└── LinuxMain.swift

To start working, simply open/double-click the Package.swift file and Xcode will recognize it as a package(-project).

Swift Packages can be opened directly in Xcode by double clicking the Package.swift file

As this blog post is not so much of an tutorial about building a calculator in Swift, I am providing you only simple implementation steps in the comments (let me know on Twitter if you want a more detailed tutorial).

Place the following code in your main.swift file :

import Foundation



// CommandLine gives us access to the given CLI arguments

let arguments = CommandLine.arguments



// We expect three parameters: first number, operator, second number

func printUsage(message: String) {

let name = URL(string: CommandLine.arguments[0])!.lastPathComponent

print("usage: " + name + " number1 [+ | - | / | *] number2")

print(" " + message)

}



// The first one is the binary name, so in total 4 arguments

guard arguments.count == 4 else {

printUsage(message: "You need to provide two numbers and an operator")

exit(1);

}

// We expect the first parameter to be a number

guard let number1 = Double(arguments[1]) else {

printUsage(message: arguments[1] + " is not a valid number")

exit(1);

}

// We expect the second parameter, to be one of our operators

enum Operator: String {

case plus = "+"

case minus = "-"

case divide = "/"

case multiply = "*"

}

guard let op = Operator(rawValue: arguments[2]) else {

printUsage(message: arguments[2] + " is not a known operator")

exit(1);

}

// We expect the third parameter to also be a number

guard let number2 = Double(arguments[3]) else {

printUsage(message: arguments[3] + " is not a valid number")

exit(1);

}

// Calculation function using our two numbers and the operator

func calculate(number1: Double, op: Operator, number2: Double) -> Double {

switch op {

case .plus:

return number1 + number2

case .minus:

return number1 - number2

case .divide:

return number1 / number2

case .multiply:

return number1 * number2

}

}

// Calculate the result

let result = calculate(number1: number1, op: op, number2: number2)

// Print result to output

print("Result: \(result)")

To use your new calculator, go back to the terminal and inside the package folder, use the swift run command to test the implementation:

$ swift run Calculator 13 + 14

Result: 27.0

Moving logic code into own SPM library

We got the first of our two applications up and running. Before we continue to create the iOS app, lets review the code and figure out, which parts should be shared by all the applications.

Two parts of the code are relevant:

The enum Operator which is our collection of math operators

which is our collection of math operators The calculate function which is taking two numbers and an operator to perform the actual math.

So let’s start off by creating a new library. First off we clean up the default Package.swift manifest file by removing all the comments and unused arguments:

// swift-tools-version:5.3



import PackageDescription



let package = Package(

name: "Calculator",

targets: [

.target(name: "Calculator"),

.testTarget(name: "CalculatorTests",

dependencies: ["Calculator"]),

]

)

Now create a new folder inside the Sources folder called CalculatorCore which is our shared application core logic.

Creating a new library starts with creating a folder with the same name

Inside this newly created folder we are creating two new Swift files:

First create the file Operator.swift and move the enum Operator {...} declaration in there:

enum Operator: String {

case plus = "+"

case minus = "-"

case divide = "/"

case multiply = "*"

}

Second create another file calculate.swift and move the calculate(...) function into there:

// Calculation function using our two numbers and the operator

func calculate(number1: Double, op: Operator, number2: Double) -> Double {

switch op {

case .plus:

return number1 + number2

case .minus:

return number1 - number2

case .divide:

return number1 / number2

case .multiply:

return number1 * number2

}

}

After moving out the code, your main.swift file should now look like this:

import Darwin

import Foundation



// CommandLine gives us access to the given arguments

let arguments = CommandLine.arguments



// We expect three parameters: first number, operator, second number

func printUsage(message: String) {

let name = URL(string: CommandLine.arguments[0])!.lastPathComponent

print("usage: " + name + " number1 [+ | - | / | *] number2")

print(" " + message)

}



// The first one is the binary name, so in total 4 arguments

guard arguments.count == 4 else {

printUsage(message: "You need to provide two numbers and an operator")

exit(1);

}

// We expect the first parameter to be a number

guard let number1 = Double(arguments[1]) else {

printUsage(message: arguments[1] + " is not a valid number")

exit(1);

}

// We expect the second parameter, to be one of our operators

guard let op = Operator(rawValue: arguments[2]) else {

printUsage(message: arguments[2] + " is not a known operator")

exit(1);

}

// We expect the third parameter to also be a number

guard let number2 = Double(arguments[3]) else {

printUsage(message: arguments[3] + " is not a valid number")

exit(1);

}

// Calculate the result

let result = calculate(number1: number1, op: op, number2: number2)

// Print result to output

print("Result: \(result)")

If you try to run your application once again, it will greet you with an error saying it can’t find Operator nor calculate anymore.

When moving classes outside of the scope, errors will occur.

This is expected, so now we have to finish creating the CalculatorCore library and add it as an dependency to our Calculator . To do so, all we need is to declare the library in our Package.swift :

// swift-tools-version:5.3



import PackageDescription



let package = Package(

name: "Calculator",

targets: [

.target(name: "CalculatorCore"),

.target(name: "Calculator",

dependencies: ["CalculatorCore"]),

.testTarget(name: "CalculatorTests",

dependencies: ["Calculator"]),

]

)

If you try to run the application once more, you will still see the same errors. The reason behind this behavior is the missing import CalculatorCore in the main.swift :

import Foundation

import CalculatorCore



// CommandLine gives us access to the given arguments

...

Additionally the (in my opinion great) isolation capabilities of Swift packages require us to declare both Operator and calculate as public , or otherwise they won’t be available outside the package:

// in Operator.swift:

public enum Operator { ... }

// in calculate.swift:

public func calculate(...) -> Double { ... }

Run your application using swift run and it should be working once again.

Create the iOS project using the library

Good job so far! You already created a command line executable and a SPM library. Now we expand it even further and create an iOS app using our SPM library calculation logic.

For this tutorial we will be using a SwiftUI app, as it is the future of iOS/macOS app development, and allows us to create a simple calculator way faster than using traditional UIKit.

Open up Xcode and click on File/New/Project

Now select App in the iOS tab, name it Calculator_iOS and select SwiftUI for all the settings.