7 Circles of SPM or how to make a modular application using Swift Package Manager

Andrey Gluhih
e-Legion
Published in
6 min readMar 30, 2021
Thanks to Jackie Zhao @jiaweizhao for making this photo available freely on Unsplash 🎁

I believe many developers have faced the task of breaking a project into modules. This article does not contain any information on how to solve cyclic dependencies or allocate functionality to layers. It is expected that this has already been implemented. The aim of the article is to describe the problems you may face when breaking a project into submodules using the SPM tool. And also to help you to understand whether it is worth upgrading to SPM or not.

Many people may have a question: Why should I split the project with SPM if I can simply create subprojects. Yes, you can, yet there are numerous advantages to use SPM for breaking up:

— With SPM we get rid of .xcodeproj files (and forget about the related conflicts);
— We gain a native ability to distribute our modules in the future. Let’s suppose you will need to use the existing authorization for your new application, then it costs you nearly nothing to distribute this functional module;
— No alternatives for projects under different operating systems of Linux, Windows;
— Packages do not require Xcode for development.

How does the process look like?

We start with creating a Package.swift file which will contain all the necessary dependencies and the information describing what this particular module is all about.

Here is an example of a file with the parameters you are likely to encounter when working:

After creating a module, we connect it to the project (or to another module, if the module you made is only an auxiliary for others). For convenience, you can add the package to the workspace, just drag and drop the root folder with the package to the Project navigator.

Here we stop the introduction and move on to the key points of the article where we will look at the problems you might face. For this article swift-tools-version:5.3, Xcode Version 12.2 was used.

Circle 1. A Small Community.

It is often difficult to find any information about problems you can encounter when working with SPM. Therefore, the time spent solving the problem can increase drastically. This is exactly why this article was written: I want to share solutions that will save your time.

Circle 2. The SPM Package Scripts Phase is Missing.

A built-in solution will be available soon, there is already a proposed solution to the problem. As for now, if you want to add linting or something similar, there are several options: adding them to your project’s main target, and running them using the git hooks (e.g. checkout) or simply from the terminal.

Circle 3. R.swift and SPM.

We will have to create the .xcodeproject file since we don’t have one yet and we can’t call the R.swift script to generate it.

You can use XcodeGen and its analogs to create it. Alternatively, a swift package generate-xcodeproj, the standard SPM script for generating .xcodeproj files.

I suggest using the standard tool not to add any new dependencies. Though unfortunately, generating this way is not really suitable for R.swift. If we try to call the script on the generated file we get:

error: [R.swift] Project file at ‘file:///Users/…/Resources.xcodeproj/’ could not be parsed, is this a valid Xcode project file ending in *.xcodeproj?

We will need XcodeEdit to localize the problem and find out the reason why the project file could not be unparsed. XcodeEdit is the framework R.swift uses for parsing. So, we build it and get an exec file to which we pass the .xcodeproj. Call it:

pathtoexec/XcodeEdit-Example Resources.xcodeproj

and what we see is:

Fatal error: ‘try!’ expression unexpectedly raised an error: XcodeEdit_Example.AllObjectsError.fieldMissing(key: “buildRules”): file XcodeEdit_Example/main.swift, line 21

Here’s how you can solve it:

sed -i ‘’ -e ‘s/isa = “PBXNativeTarget”;/isa = “PBXNativeTarget”;buildRules = ();/’ Resources.xcodeproj/project.pbxproj

Unfortunately, our problems do not end here. generate-xcodeproj does not add resource files to the project meaning that R.swift will parse .xcodeproj/.pbproject when the required resource files are not there.

We can solve this by connecting xcodeproj ruby gem, which is used to add the required files. But still, this approach is quite burdensome, so better go back to the option of .xcodeproj tools like XcodeGen.

An example of a minimal config file for XcodeGen:

The command creating the .xcodeproj file:

xcodegen generate — spec Resources.yml

Now the .xcodeproj problem is solved, here is an example of a complete script for R.swift generation:

Circle 4. Cocoapods Dependency in the SPM Package.

The most difficult thing is that there is no native way of Pod package connection, same as there are no fat frameworks, while some developers are not in a rush about transferring their libraries to SPM. This is where a proxy wrapper package with XCFramework comes in to help us. If the Pod you want to link to SPM is an open-source one, you can download the library source code and build the XCFramework using this guide.

If the Pod is distributed as a pre-built binary via .framework, you will have to act in a slightly different way. Similar to the previous case, you need to build the XCFramework. Firstly, we need to build 2 frameworks from the fat framework supplied by the Pod — one for the simulator and one for the device. After that, we have to combine them into a universal framework. Here is a script that makes an XCFramework from a framework with architectures arm64 and x86_64. You can find out the architectures that the framework supports by using the lipo -info pathtoframework command.

And after the XCFramework has been made, you need to wrap it into a package using binaryTarget. You can additionally wrap it with the necessary dependencies by plugging it into another target:

Circle 5. Crash When Trying to Get the SPM Package Bundle (Bundle.module).

Unfortunately, I could not understand why this happens, but there are cases when the resource bundle currently named yourpackagename_yourpackagename does not get into the directory, where the package’s auto-generated code is waiting for it. As an option, you can write your own code creating the auto generator, and add another possible path there.

Circle 6. Blocking Resolve Swift Packages Stage.

The problem occurs when all of your SPM frameworks are uploaded by the project, and you open your project after closing it. If there are numerous SPM dependencies, Xcode may resolve them for over a minute. During this time Xcode may lag and cease to respond to any interactions.

Circle 7: Other Linker Flags and SPM.

You can encounter a situation where a package must be linked with special flags. A common example is the ObjC flag. These flags for the linker can be specified with

likerSettings: [.unsafeFlags([“-ObjC”])]

in your package’s target. Unfortunately, if your package uses unsafeFlags, it cannot be connected directly to the project, but only through a proxy package and when it is specified as a local or branch dependency.

Conclusion

The Swift Package Manager is a convenient and intuitive native dependencies manager. However, if your plan is to use it to break your application into subproject-packages, you need to get ready for some temporary risks and problems. Fortunately, almost any SPM-related problem has a workaround, but are you really willing to look for it and take these risks?

I hope the solutions I have suggested will help you!

Feedback and suggestions are always welcomed.

--

--