Using Swift Local Package Manager to Group Your Code

​The Swift package manager is an official third framework dependency manager like CocoaPods. Not only can we use it to add third-party dependencies, but we can also use it to separate our project into multiple modules with individual naming spaces.

First, we create a new folder MyApp for our project.

Then, we add a local package for our project


As you can see, we have added a local package Modules, then we first remove the original Tests folder and the Modules folder. Our Modules package will contain multiple targets and we should give them a meaningful name.

Assuming that our project has three modules, Network, Api and CommonUI.

The Network module is a low-level network layer that we cannot use directly in our MyApp project.
The Api module is the real network request tool out of the box in our business modules. It depends on the Network module.
The CommonUI module contains a variety of reusable UI components.

We have three modules, so we create three folders to store the code.

We take the Api module as an example, creating two folderssrc and test. The src folder places our module’s code or assets, and the test folder places our module’s unit tests. Other modules have the same directory structure.

Then, let’s look at the “package” file. This is the profile of our package.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Modules",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Api",
targets: ["Api"]),
.library(
name: "CommonUI",
targets: ["CommonUI"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Api",
dependencies: [],
path: "Sources/Api/src"),
.testTarget(
name: "ApiTest",
dependencies: ["Api"],
path: "Sources/Api/test"),
.target(
name: "Network",
dependencies: ["Api"]),
.target(
name: "CommonUI",
dependencies: []),
]
)

If you have never configured the Package file, you can find the details in the official document.

The Package file has three main properties, products, dependencies and targets.

  • We can add third-party dependencies to our package by adding source url and version information in the dependencies property.

  • The most important is the targets, a target can include multiple targets or test targets. One target is a module. Every target can set its source path and isolated dependencies.

  • The products is the output of the targets. A target can generate an executable product or a library. These products can be imported by other packages or targets.

How do we use our customized modules in our project?

Open our project’s General settings, click the add button in the section Frameworks, Libraries, and Embedded Content.

As shown above, our local package Modules produced two libraries, Api and CommonUI. We first add them to our project, and then we can import them when we use them.

There are several concepts we should know. Targets can depend on other targets or products, we can directly import a library into a target without adding the library as the target’s dependencies, because libraries(products) are public.

As mentioned above, we think the Network module is a low-level module, so we don’t expose it as a library, but other modules can add it as their dependencies to utilize its functionality.