While SPM offers numerous benefits, it also poses challenges, particularly for larger projects rather than smaller ones. Frustration arises when the Package.resolved file frequently changes, particularly with numerous in-house packages, especially if they are pinned to a Git branch instead of a specific version or commit. If you get yourself in such situation, changing git branches and opening Xcode often causes SPM to resolve packages again and again. Further more, this all introduces annoyance that expresses itself in form of not being able to open Xcode project while offline.

How SPM uses Package.resolved file?

During the initial fetching of dependencies, SPM utilizes either the Package.swift manifest or the .pbxproj file to establish their version constraints. SPM then resolves these dependencies, taking into account the version constraints specified in the manifest. After resolving the dependencies, SPM creates or updates Package.resolved file. This file contains a record of the exact versions of each dependency that SPM selected during the resolution process. The Package.resolved file essentially locks down the versions of dependencies to ensure a consistent build environment. It prevents unintended updates to dependencies that could introduce breaking changes.

Well, that last sentence is not always true.

What’s the problem then?

As I said earlier, on the huge projects you often have a lot of internal dependencies that are distributed via central git repository. To ease the development and to not always have to worry about correct version, you may want to set your version strategy such that it always points at the develop branch (what ever branch suits your needs). Yes, this definitely makes development easier but it also introduces all the problems outlined above.

A case for exact version constraints

Having explicit version constraints solves all of these problems. Instead of declaring dependency version range, you always opt into setting exact version. That way there is no need for Package.resolved file (.lock file in other technologies). If you pin your local dependencies to a branch that undergoes frequent changes, you can either ensure to update packages manually when needed or pin them to a specific commit.

But what about the release?

If you have chosen to go with git branch route as your versioning strategy, it could allow for untested code to slip in to the release. To remedy that sort of issue, pinnig dependencies to specific version (git tag in this case) or commit is the way to go. You can always write a script which sets up your dependency constraints differently for the release phase such that you have easy time in development phase with your dependencies while production code is still locked to specific revision.

Ignoring Package.resolved file completely

After pinning all dependencies to the exact version, you can safely delete the Package.resolved file and add it to the list of ignored files in your .gitignore. Now Xcode will not resolve package graph whenever you change your branch or open Xcode which should save your sanity.

Final thoughts

While this approach has worked out for me so far, it is important to note that it might not be for everyone. For starters, transitive dependencies are still not in your control, pinning package against branches instead of specific commits can be dangerous if not attended, Package.resolved file exists for a reason and who knows what else I might have forgotten. As always, never do something just because random engineer wrote on the internet. Carefuly consider multiple approaches and use the requirements of your project to make informed decision that will best serve your team and project.