Should I divide my code's components into targets or packages?

In this thread, Jeremy David Giesbrecht (@SDGGiesbrecht) did a great breakdown of the difference between SPM's targets and modules:

Having understood that, I'm still not sure how to apply those definitions towards structuring my code. My codebase has been growing, and I'm looking to modularize it and get more serious about enforcing architectural boundaries. I have several different components I've identified:

  1. UI helpers (UI code that could be reused in other apps)
  2. UI (UI code that's fairly specific to my one app, often a concrete use of the helpers in #1)
  3. System utilities, extensions/wrappers to platform features, etc.
  4. Several networking "gateways" that wrap third party web APIs
  5. A datastore service that isolates
  6. Generic data structures/algorithms

And so on, you get the point.

Ultimately, these will all be used by my one Mac App, but I would still like to enforce clean interfaces around them, so I can reduce some of the interdependence (a fancy word for spaghetti code :spaghetti:). In a sense, I'm trying to get back some of the access control that Java packages got me used to.

Should "components" like these be targets within one package, or independent packages (each with one target)?

1 Like

Here is an attempt to summarize various answers I have given across the forums in the past few months. (Search for any of the terms along with my handle and you will get plenty of hits with more details.)

  • What group of functionality do I want versioned together? → That’s a good candidate for a package, which is fundamentally about versioning and API stability.
  • What group of functionality do I want to be able to individually resolve and fetch? Do I need a unit I can vend outside SwiftPM (dynamic library, executable)? → That’s a good candidate for a product, which is fundamentally about resolution and skipping dead code.
  • What group of functionality do I want to be able to import as a single statement? Is backdoor internal interaction between its components necessary? → That’s a good candidate for a target, which is fundamentally about importing and access control.
6 Likes

Hi Jeremy, fancy seeing you here! Thanks for taking the time to help me out.

From everything I've read thus far, my overall inclination is towards having one package, with multiple products, each with one main target (and a test target, optionally a UI test target, etc.). I'll describe my rationale below. Could you let me know if it sounds reasonable?


I guess in some sense, there's no real versioning beyond the git commit sha. E.g. I don't intended to bump the semantic versioning numbers with every feature or code change I make, since the sole intended consumer of these components is the main app that glues them together (which resides in the same repo). If I'm not mistaken, that points me away from the need to have multiple packages.

Ah products. I forgot to mention those in my post. From what I gather, dependency resolution and fetching is done on a package as a whole. I.e. you can't cherry pick a single products' source files out of a package. For that, you would promote the product into its own new swift package. Is that correct?

I'm not sure, that sounds like a style/best-practice thing, and I don't know what the current convention is. Do I want:

import MyAppAllTheThings

or...

import MyAppNetworking
import MyAppCollections
import MyAppUtils
// and so on...

:thinking:

There's some "components" (my generic term for my yet-undecided unit of modularity) that seem reasonably reusable (utils, collections, algorithms, Swifty wrappers around old APIs, etc.), and some that are intentionally quite specific to the particular app (UIs, data models, etc.)

If I go with "1 package, multiple products" today, and I decide in the future that I want to reuse some library (e.g. MyAppUtils) in other projects, I can just promote that product into new a package, and all my existing sources wont' need to be changed, since they already had import MyAppUtils.

That seems undesirable to me. Using internal APIs of another component is basically subverting the explicit dependency boundaries I'm trying to establish. Right?

Yes, in your case, I don’t see any good reason for having multiple packages. (In fact it would likely be a real pain; update something at the bottom, and everything above it needs to separately update the hash it is pointing to.)

SwiftPM checks out the entire repository, but then it tries hard to skip subdependencies the product doesn’t need. (Its actual ability to identify skippables still isn’t perfect, but has been improving over Swift releases.)

In my experience, there is a tug‐of‐war between various clients. Some are more annoyed by building and importing way more than they need, others are more annoyed by having to write many import statements. Finding the right balance requires spending time writing client code to experience the tension for yourself.

Correct. And this is my recommendation from what you have said.

What I meant by that was simply that where such a boundary would be inappropriate to impose, then don’t impose it, and keep both “sides” in the same target. The presence of internal (as opposed to private or public) is your primary cue that a boundary might be inappropriate. When you upgrade internal to public just to stretch something across a such boundary, it also grants access to clients, spilling aspects of your implementation into the API. There is no access control level that grants access to other modules in the same package while withholding access from clients of the package.

1 Like

Very clear response. No further questions.

Thanks so much!

Thought it might be useful to future readers to post an update on what I ended up doing

Here's my structure (in terms of packages/products/targets, not the file hierarchy):

Foo Xcode Workspace
├── 🛠 Foo app Xcode project
├── 📦 FooShell Swift Package
│   └── 🏛 FooShell library product
│       └── 🎯 FooShell target
└── 📦 FooComponents Swift Package
    ├── 🏛 FooCore library product
    │   └── 🎯 FooCore target
    ├── 🏛 FooUtils library product
    │   └── 🎯 FooUtils target
    └── ... and so on

I wanted to make FooShell (which contains all my storyboards, view controllers, etc.) into just another library product under the FooComponents package. But, it appears that you can't specify platform requirements on a per-product or per-target basis, so I ended up needing to split it off into its own Swift package.

In FooShell, I imported FooComponents as a local dependency:

//...
dependencies: [
	.package(path: "../GizmoComponents"),
],
targets: [
	 .target(
		name: "GizmoShell",
		dependencies: [
			.product(name: "GizmoCore", package: "GizmoComponents"),
   	 ]
	),
   //...
],
//...
1 Like

I'm also in the progress of modularizing my app's code. I don't have much experience with SwiftPM, but I find this approach works for me: creating a stand-alone swift package for each component outside the app's project folder, then importing those components by using Swift local package support. That avoids the hassle Jeremy mentioned.

If you do that, nothing is pinned, so it is very easy to get pieces out of sync with one another and end up with stuff broken downstream (unless you make sure a single top‐level package depends on absolutely everything—even tests—and you only ever make changes with that package open).

1 Like

Understood. I agree it doesn't scale. It's not a good approach for team based distributed development. But in my case they are all components of a set of apps (a GUI app and perhaps a command line app in future). So it's relatively easy to do the verification. When the app has multiple different versions, I may need a better way for version control.