No Manifest Destiny
The consumer of a package does not need to have a Package.swift
file, unless they will be establishing themselves as a package to be consumed elsewhere, or will be building from the command line. The Swift build system takes care of establishing the linkage to included packages.
In this entry, I will only cover the GUI methods, expressed by Xcode, as that is my experience with the SPM, and, I suspect, it will be the manner that most developers will interact with the Swift Package Manager.
Adding Packages
The Main Package List
Packages are collected and displayed in a list that is visible if we select the main project in the Project Navigator, the Main Project in the Target List, and the “Swift Packages” Tab:
Simple Drag-And-Drop
The way that this works, is we just drag the entire package directory (the directory containing the Package.swift
file) into the Xcode project of the consumer.
This results in the package being embedded into the project. It hasn’t (yet) been linked to any targets, but it is now a project resource.
Additionally, we can see that the ENTIRE directory was added; not just the files referenced in the Package.swift
file.
One feature of direct embedding is that we can actually edit package files, in place. We cannot do this for files that are fetched from remote repos.
Note also, that the package is embedded as a simple project asset (no special section).
Drag-and-drop only affects the one package. It does not affect any packages that Package_A
depends upon. Resolution of those dependencies is the responsibility of Package_A
.
In fact, it is treated exactly like an embedded Xcode project. It is meant to support debugging and development. It isn’t really meant to be used for ongoing release and maintenance.
Embedding The Package Into the Target
Now that the package is a project asset, its product can be embedded into the project. This is done in the usual way:
If we hit the “+” button, the following menu appears:
We select the package product, and it ends up as an embedded asset:
It should be noted that drag-and-drop packages don’t appear in the project’s Package List:
That’s because drag-and-drop packages are treated like embedded Xcode projects; even if they use the package icon.
NOTE ABOUT EMBEDDING:
The default library type for packages is static. That means that the library simply needs to be linked in and not embedded. If the library is dynamic, however, it needs to be linked, signed and embedded. The name of the library is also not allowed to have underscores (If this were a dylib, the package would be named “
Package-A
“).
Adding From the File Menu
Another way to add a package, is via the File
-> Swift Packages
-> Add Package Dependency...
menu item:
Adding A Git URI
In this case, we should supply a Git URI. If we have associated a Git account in our Xcode preferences, repositories visible to that account are shown. It can take a few seconds for all of them to appear, and we can simply select one of those:
But it is more likely that we will enter an explicit Git URI:
Once we hit “NEXT,” the following screen will appear:
If we entered an explicit URI, then it is likely that a version will already be entered. Otherwise, we can enter a version, manually.
The Git repository must have tags set, using simple semantic versioning, for this to work.
We can also set a branch (the HEAD revision will be used):
Branches are not meant to be used for release builds, and cannot be mixed with version-based ranges.
We can also enter commit hashes, for a specific commit.
Once we hit “NEXT,” we will see a screen, similar to this (NOTE: If this is being accessed from a target, then it will look like Fig. 17 -No “Add To Target” Column):
This allows us to immediately attach it to one of our Xcode targets. We will be required to embed it.
Once this is done, the package will be placed in a separate location in the Project Navigator:
The entire repo is also added, like with the drag-and-drop addition, but, in this case, we will not be able to edit any of the package assets:
The package will now be visible in the Main Package List:
If we want to add the package to other targets, we follow the same procedure as above (Embedding), as the package is now a project component.
Adding From The General and Build Phases Tabs
If we look at a specific target in the Target Navigator, and select the “General” tab, we can add the package, using the “Frameworks and Libraries” section:
If we hit the “+” button, we will get this screen:
At the bottom, is a popup menu. If we choose that, we will get a selection called “Add Package Dependency…”
If we select that, we will be taken to the same workflow as above (Adding A Git URI).
We can also do the same thing from the “Build Phases” Tab:
In both of the above cases, Xcode will add an embed step, if necessary (if the library is dynamic).
Using The Package
Assuming the package was a library (either static or dynamic), we use it like we would any library, by importing it into Swift files that access the library contents:
import Package_A
Note that we don’t have special builds for each operating system (like “Package_A_iOS
“, “Package_A_MacOS
“, etc.). We simply use the name defined in the main Package.name
property (Note that a dylib that uses dashes for its bundle ID will still use the underscore for the import).
Also, the package is resolved, loaded and built upon opening the project, so it may be a few seconds before the packages are ready for use.
Cache Issues
SPM is heavily cached, and ensuring that we have the latest version of a package can sometimes be a challenge, while developing (I have found it doesn’t really pose a problem, once the project has stabilized).
Xcode has a File
-> Swift Packages
submenu that allows us to flush the cache, and rebuild the packages. I will often use the Product
-> Clean Build Folder
menu, and also clear the “DerivedData” directory.
Version Conflicts
SPM will prevent depending on incorrect versions of nested dependencies (“Dependency Hell”).
Trying To Include A Dependency That Has An Incorrect Version Sub-Dependency
Look at Fig. 25:
What happened here, was that I had already included a dependency on Package_A
, for Version 2.0.0, then tried to add Package_B
, which had a dependency on Package_A
, but at Version 1.0.0.
SPM realized that these dependencies conflicted, and blocked me from including Package_B
.
A Nasty “Gotcha”
Even if we are allowed to include the offending dependency (in the following example, I went and included Version 2.0.0 of Package_C, which depended upon Version 2.0.0 of Package_A
), we may still have a dependency collision.
Look at Fig. 26:
The library won’t build, however, in Xcode (It will build in swift build
, though. Go figure. I assume that swift build
is smart enough to avoid loading the library twice).
That’s because we now have TWO copies of the Package_A
static library in our app. One is loaded with the Package_C
dylib, and the other is directly embedded as a static lib.
We will see the following error: "Swift package product 'Package-A' is linked as a static library by 'Package_D' and 'Package-C'. This will result in duplication of library code."
in the Project Navigator Error Pane.
The solution to this, is to remove our dependency to Package_A
, trusting Package_C
to provide it:
In Fig. 28, we can see that we have both libraries in the Project Navigator, even though we only have one listed in the Swift Packages tab.
It should be noted that, even though Package_C is providing access to Package_A, we don’t get Package_A
“for free,” when we import Package_C
. We still need to explicitly import Package_A
:
import Foundation import Package_A import Package_C public struct Package_D: PackageProtocol { public let indent: Int public let text: String public init(indent inIndent: Int = 0) { indent = inIndent let prefix = String(repeating: "\t", count: inIndent) text = "\(prefix)Package_D, Version: 1.0.0\n" + Package_C(indent: inIndent + 1).text + "\n" + Package_A(indent: inIndent + 1).text } }