How to add local Swift Package as dependency?

While it's fine to use local packages as dependencies for the sake of developing a Swift package for distribution, I would recommend against using Swift packages as a replacement for Xcode Projects for normal development purposes, except for simple Swift-only modules that will only ever be used by Swift code.

At my organization, we were really tired of Xcode Project files due to the constant merge conflicts their randomized GUIDs cause. So, based on some advice given on this forum and in prior WWDC talks, we decided to start using Swift Packages for all our new modules.

However, with the benefit of hindsight, I would not make that decision again. We have been continually plagued by flaws and limitations with SPM and how it's integrated with Xcode, which have collectively been a bear to deal with. And we've been forced to keep using Xcode projects in several cases because Obj. C headers in an Xcode-project-defined module cannot take advantage of @objc Swift code that's defined a Swift module.

So my advice is, if your app has any Obj. C in it, don't use local Swift Packages. If your app needs to directly reference asset catalogs, don't put them in a Swift Package. If your code needs to link to a pre-built third-party binary framework, don't put it in a Swift Package. That's pretty limiting.

Any @objc Swift code in your packages won't be visible from Obj. C headers in your Xcode projects. There's no workaround or solution. Quite simply, Xcode doesn't embed a Headers folder in the .framework that it builds from a Swift Package, and it doesn't add the headers themselves either. To me this means there is a huge lack of test coverage around SPM's Xcode integration, or a very poor understanding of the use-cases for Xcode project files that people might try to use Swift Packages as replacements for.

Did I mention, the Asset Catalog support is woeful? Consuming modules won't be able to access your package's resource bundle directly, meaning you'll have to write a whole new API to allow consumers to access those resources. If you write that API in Swift, it won't work with Obj. C code in Xcode projects that consume the package, forcing you to make a whole separate package written in Obj. C with its own copy of the resources. Not to be harsh, but it's really ridiculous that it was released in this form.

Further, the ability to link to external binaries is useless, because .binaryTarget only works for binaries that are inside the same directory structure as the package itself. Why? Because it was only designed as a way to let people distribute a prebuilt framework in a Swift package wrapper—it wasn't designed with the use-case in mind of someone who wants their Swift package to simply link to an existing framework in their build folder or Carthage folder.

After many wasted hours, I did finally discover an extremely ugly hack to link a Swift package to a framework outside that package's directory structure. However, since:

  • linking a Swift package to an XCFramework via .linkedFramework is completely unsupported,
  • Swift packages' build conditions provide no way to specify different build settings between simulator and device builds; and
  • Xcode projects can't link to Swift packages that have unsafe flags (a bug);
  • and unsafe flags are the only way to set custom linker flags for a Swift package...

... therefore the only way to link to an external framework requires the kludgiest hack I've ever had to do in my entire career.

And the above is just the tip of the iceberg of problems and limitations with Swift packages.

Despite their flaws, Xcode projects remain by far the most flexible, powerful, bug-free, and useful way to define a local module. If you're not going to distribute code, and you're working on a Mac with Xcode, save yourself tons of headache, trials, and tribulations, and just use an Xcode Project file. The occasional merge conflicts are a small price to pay.

Note: I sincerely hope that the SPM/Xcode integration improves in the future, but given that Xcode 13 did not so far address any of the major issues that I'm aware of about Swift Packages then it doesn't feel like they are really prioritizing making SPM as a viable replacement for Xcode project files. That's why I proposed that we add something like "Swift Projects" so that the purpose of it is clearly stated as for it to be able to support everything Xcode projects currently support.

SPM was designed as a way to distribute non-mixed-source modules and manage such dependencies, and it's very serviceable in this regard for a small set of small dependencies. But even there, its propensity for extremely slow deep clones makes it not really a viable alternative to Carthage or Cocoapods yet since it takes 15-20 minutes after pasting the URL to GitHub - firebase/firebase-ios-sdk: Firebase iOS SDK into Xcode before you even get the menu of options of which libraries to add as dependencies, and then another 15-20 minutes after that to actually add them (plus the same delay anytime someone clones the project and opens it before they can start building, plus it adds every one of Firebase's many dependencies into your main workspace).

Sounds like a lot of complaining but I was one of the most gung-ho early adopters of Swift Packages, and I tried as hard as I could to make it work and communicate issues to Apple. But at the end of the day, Apple is in control of Xcode and SPM already meets their criteria for integration with the IDE. They would probably say, "If it doesn't meet your use case, then don't use it," and I agree. But also they are the ones who started recommending people to start using it instead of Xcode projects—without telling people about all the problems and limitations, and then only making a half-hearted attempt to actually fix those issues.

So I'm just saying, beware. It'll be your own valuable time that you'll be potentially wasting.

Like for example, now we can't run our tests on a device anymore because someone added a CombineTestExtensions package that depends on XCTest, and is a dependency of a bunch of our Swift package's test targets. Well, XCTest wasn't built with bitcode enabled, yet there's no way to disable Bitcode when building a Swift Package that's not a test target. So we literally can't compile and run our full test suite on a physical device anymore, because Xcode requires that if your module has Bitcode enabled, all its dependencies must also have Bitcode enabled.

Apple wants us to send bug reports about this stuff, but it's getting to the point where honestly, Apple shouldn't need me to tell them that every build setting needs to be configurable. Yet they've done the SPM/Xcode integration in such a way that many settings are automatically assumed, and cannot be changed or overridden. Or if you do change/override a setting, you have to use an "unsafeFlag" that makes your Swift Package unable to be linked to from an Xcode project or app. And you can't even inherit settings from an .xcconfig file or define a custom configuration name—there's just "Debug" and "Release" and "woe be unto you if your project uses a configuration name other than Debug or Release because now it's just going to automagically guess whether it should be Debug or Release and if it's wrong then it's up to you to hack the system to fix it or live with the consequences."

Quite a departure from Xcode projects.

4 Likes