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

Update: Dec 18, 2021

The original version of this article describes the steps I took to get a project with mixed ObjC and Swift source files building using Swift Package Manager. In retrospect this was a bad idea. Presumably you are already maintaining an Xcode project file (likely you have a couple other ways of building as well).

  1. Separating targets for Swift and ObjC source code. ie. How to structure the package manifest (Package.swift) to provide a Swift target that depends on an ObjC target.
  2. Resolving public interfaces so that APIs are available from ObjC projects.
  3. Passing through an ObjC interface so that it’s available to Swift users
  4. Issues with diverging module import syntax. ex: # import <foo/bar.h> vs #import “bar.h"
  1. Fewer sources of truth to maintain. This is huge. Every way of building your library is a vector for issues. Fewer vector, fewer issues.
  2. No need to have separate targets for Swift and ObjC source code. Resolving the underlying modules is handled automatically by Xcode and is based on the project file you already maintain.
  3. No need to restructure your project or use hacky workarounds like symlinks in order to expose your ObjC interfaces to your Swift code.
  4. No need to re-export nested ObjC modules to end users.
  5. No need to modify existing imports statements to support diverting syntax.
  6. No need to have additional CI jobs related to building from a Package file.
  7. No need to duplicate QA efforts aside from making sure that the XCFramework is available. If the framework can be obtained from the Package, you can safely assume that it will behave the same as if it were vended from other sources.
  • Generate project files on-demand using Xcodegen or a similar tool.
  • Use the project to create XCFrameworks.
  • Distribute those frameworks via a repository that contains ONLY a Package.swift file.

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.

  • How to structure your package manifest (Package.swift) to provide a Swift target that depends on an ObjC target
  • How to resolve your public interface so that your APIs are available from ObjC projects
  • How to pass through your ObjC interface so that it’s available to Swift users
  • How to avoid the trap of fully qualified module import syntax. ie. issues surrounding# import <foo/bar.h> vs #import “bar.h"

PACKAGE MANIFEST

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

.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.
  2. If you had any dependencies for your ObjC library you would put them here
  3. This assumes that all of your ObjC files are in a directory named ‘ModuleX/’. If all of your source files are in a directory named ‘src’ or ‘sources’ then this argument can be omitted as SPM will automatically find them.
  4. This relies on the important assumption that all of your Swift source files are colocated in their own directory(s). In this example we assume that they are in a directory named “SwiftSources”. Currently there is not a way to exclude files based on extension. 😢
  5. Header search paths are for files within ModuleX to resolve themselves so that we don’t get cannot find <module-name/module-name.h> type errors. This is not the same as exposing them as a publicly available interface (we will cover how to do that shortly). This example assumes you have your internal headers in a directory named “Internal”.
  6. This is the name that will be used to import your Swift module into projects.
  7. This is the dependency on your ObjC module.
  8. Again, this assumes that your Swift source files are colocated in a directory named “SwiftSources”.

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.

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.

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

HEADER TRAPS

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

#if SWIFT_PACKAGE
#import "SomeHeader.h"
#else
#import <MyModule/SomeHeader.h>
#endif
#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!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store