Need advice for designing modules or packages having dependencies


I come to get some advice about package dependency designs. Please forgive the verboseness. I failed to express what exactly I want to ask you for briefly and elegantly because I don't know the terms related to what I ask for. So I wrote the following long, which might be boring, story.

I have a project to develop an app having some features that cooperate. As it gets larger in the codebase size, I decided to separate the features into private Swift packages, with a vague expectation that they might be re-used another day. All seemed reasonable at the moment. Each package has a repository (remote and local). After making some changes, I commit and push them. The changes are automatically applied to my project by clicking "Update to Latest Package Versions" in Xcode. I have tried using local packages once but moved to remote ones since I usually work in many places.

Simply say, there are packages A, B, and C. Package A is a collection of small utility functions. B and C have a dependency on A. All of the packages are imported into the project. I wanted to make some changes to A for experimental purposes, so I created a branch and made the changes. The changes don't affect any features that package B and C depend on. I wanted to replace package A with a version of the test branch. I didn't want to merge the changes into the main branch until I was sure it still worked perfectly after the changes. I changed the version rules in Package Dependencies. Complaints came out from packages B and C since they expected the package to be the main branch version, as written in their package.swift file.

Now I have to go to packages B and C and modify the package.swift file to make them expect the test branch of A and commit & push the changes. This is not good. I didn't expect this at the moment I designed them.

I tried to redesign packages B and C, so they don't require package A explicitly and reveal their features only if package A is available, using #canImport. It didn't help at all. I learned it doesn't work as expected in the Swift package due to how the compiler works.

The story tells the problem simpler than it is. The actual situation is I ended up with an ugly dependency tree, and if I touch a node, it forces me to touch all the nodes that depend on the node. What a demanding naughty tree!

Now I feel something was deeply wrong from the moment I designed them. Should package A be just some files to be included wherever they are needed, not as a package? Or is there a way to work around this situation?

I have thought about making another package containing only the parts that depend on packages A and B. However, it will be worse because multiple dependencies generate packages corresponding to the intersections from the possible combinations.

First, open the main application project in Xcode. Then in Finder, find (or first make) a local clone of A. Drag the root directory of A into the Xcode project’s file navigator. You will have made a local reference to A’s current checkout, whose files you can edit directly (or you can check out a branch you already made on another device). That local reference will override anything in the package graph. When you are done editing it, then from the repository of A, check it in, push it, merge it or whatever. You can then delete it from Xcode’s file navigator (with or without deleting it from the local file system) and it will all go back to the standard version resolution.

You can look up the swift package edit command for how to do something similar with command line SwiftPM if you are ever not using Xcode. (The command is deprecated, but will not be removed until there is a replacement that works more like Xcode does.)

As much as possible, it is wise to write comprehensive tests inside A, so that you can tell whether or not A works without plugging it into any clients. The better job you do of that, the less you will need to reach for the strategies described above.

Thanks a lot!

Wow, how did I never think it could go that way?

That sounds like it is similar to developing with local packages instead of remote ones and push-pull steps. Still, I can push the changes from the local ones to the remote ones, right? It seems a lot handy that the local copies can override the original packages.

I will figure out when I do this myself, though, but do I still have to modify the package.swift files in the overriding packages B and C to make them see the branch I am working on in package A?

Yes. Basically what you are doing is temporarily switching a fraction of the dependency graph back to local development while you are experimenting and until your work is ready for the push‐pull steps that persist the polished result remotely.

No. The local copy will be used in place of any reference with the same identity, no matter how many places it appears in the dependency graph. You are creating a bypass from the top of your work area straight down to the particular package you are tinkering with. Nothing in between needs to change or even know about it.