Swift Package Manager with a Mixed Swift and Objective-C Project (part 2/2)

You’ll notice from the title that is is part 2/2. If you not read part 1 then I advise going back. It is not essential but it does provide some tips that will help you work with SPM in a more general way.

BACKGROUND

Distributing a library that has a mixture of ObjC and Swift sources provides some interesting challenges. Depending on your audience you may not have the option of distributing a single library. Whether the concern is size, performance, or cost of updating, there are some developers who are not yet ready to adopt Swift into their projects.

This article will assume that you are trying to use SPM to distribute two modules. One module that is exclusively ObjC and one module that is a mixture of ObjC and Swift.

While there are some challenges to implement a solution for SPM, the problem is not unique to SPM.

Imagine you are trying to ship ModuleX. Ideally a consuming project can import ModuleX without worrying if it is importing the Swift or ObjC version of the module. If you’ve distributed a library using CocoaPods you may have already run into this issue and solved it by using sub-specs: CocoaLumberjack. Or maybe you have two separate pod specs where one depends on the the other. Example: Google’s FirebaseFirestoreSwift.podspec depending on FirebaseFirestore.podspec.

The second example mirrors the approach we will need to take to support mixed source files in SPM. ie. providing a Swift module that depends on an ObjC module.

Obviously one article cannot encompass everything about SPM. My goal is to guide you through four major pain points.

  • How to structure your package manifest (Package.swift) to provide a Swift target that depends on an ObjC target

PACKAGE MANIFEST

To start, you’ll want to create a Package.swift file at the root of your project.

Note: To avoid confusion, from here on out I’ll assume that you have your Swift files in a single directory named SwiftSources located at the same level as your ObjC source files.

.target(
name: "ModuleX-ObjC", // 1
dependencies: [], // 2
path: "ModuleX/", // 3
exclude: ["SwiftSources"], // 4
cSettings: [
.headerSearchPath("Internal"), // 5
]
),
.target(
name: "ModuleX", // 6
dependencies: ["ModuleX-ObjC"], // 7
path: "SwiftSources" // 8
),
  1. First you need to create a target and name it in a way that distinguishes it as your ObjC dependency.

PUBLIC INTERFACES

Exposing public interfaces requires a bit of work. SPM will automatically expose headers that are located in a directory named “include”. There are a couple of ways to address this.

Move all of your header files into the include directory.

Umm. no. This is a huge pain and breaks the convention of colocating header and implementation files.

edit: After poo-pooing this idea I wish I had given it more consideration. It would have been worth the up-front cost to have this structured ‘correctly’. The only downside to having the ‘.h’ files separated from the ‘.m’ files is that Xcode has trouble find them for some keyboard shortcuts. A small price to pay to avoid having to mess with symlinks which cause their own issues in Xcode. Whether or not you can do this will depend on the size and complexity of your codebase.

Have a separate header file that you only use for your Swift package.

Again, no. This is difficult to maintain and you would need to provide relative paths to all of your other headers. It would wind up looking something like this:

// ModuleX.h#import "../MyFeature/SomeClass.h"

Use Symlinks

This is the least disruptive pattern I’ve found. It requires that we provide symlinks to headers we want to make public.

To do this, create and cd into a directory named “include” and run ln -s {projectRoot}**/*.h or some-such equivalent. The result should be a directory of symlinks to your public headers. You probably want to manually check that you aren’t exposing internal headers.

edit: A few months in I’ve found that this technique works but it required writing a script to recursively check that public header files were exposed via symlink correctly. I have not had issues with this approach since that was added to the CI pipeline.

EXPOSING YOUR OBJC TO SWIFT USERS

Bad news. You will need to do something hacky to expose your ObjC interface to Swift users. In your Swift directory, add a new Swift file. I chose to use the name ‘Exports’. The actual file name is unimportant. Add the following code to your new file:

@_exported import ModuleX-Objc

This will allow users to import your library and have access to its dependencies. For more info see: https://forums.swift.org/t/16648/2

HEADER TRAPS

Some build systems require you to have fully qualified import paths. For example, you might have import statements in the form:

#import <MyModule/SomeHeader.h>

When you go to build your Swift package you’ll get the error:
'MyModule/SomeHeader.h' file not found

If this is an internal header or an implementation file then you can use the Xcode-provided SWIFT_PACKAGE macro.

ex:

#if SWIFT_PACKAGE
#import "SomeHeader.h"
#else
#import <MyModule/SomeHeader.h>
#endif

IMPORTANT
This will only work with non-public files since the macro definition is not exported to consuming projects.

More likely you will get around this by gating on some other macro that you have available based on your build system.

ex:

#if SOME_BUILD_SYSTEM
#import <MyModule/SomeHeader.h>
#else
#import "SomeHeader.h"
#endif

CONCLUSION

Hopefully this helps get you a little closer to distributing your legacy project with SPM. Thanks for reading!