How to expose Obj-C library to SPM

Swift Package Manager gains finally it’s matureness (sort of) and it’s time to think seriously if we can use it instead of CocoaPods.
Our app is based upon several different modules (internal frameworks) with their own external dependencies. Most of these dependencies are well known, well supported libraries so they started supporting SPM since its first introduction.

Everything seems pretty straightforward but… one of them it’s an Obj-C framework from a third party partner used to provide some campaigns; looking at its repository is pretty clear to me it’s not actively maintained.
However it’s Objective-C so, trust me, it still works with no special issues (we can’t say the same for old Swift code yet).
Now my attempt is to provide a Package.swift manifest so I can integrate it with no special treatment.

Make it SPM complaint

Distribuiting an SPM package which is Obj-C based sources provides some interesting challenges.
This library was made for CocoaPods; it contains an events-sdk folder where the entire library code is contained: a set of folders with headers (.h) and implementation (.m) files.
You can browse the structure directly from GitHub.

The first thing we need to do is to make it complaint with the SPM specs; so we created a Sources/Criteo folder where we moved the entire structure as initially designed.

Headers visibility

Package.swift contents is initially simple, we just declare the target as any other Swift library. No more than a standard swift package init does.
However opening it and trying to compile results in several errors.
Library’s code is organized in different folders; each folder contains headers and implementation of a class and #import other headers contained in the same library but inside other folders.

I got a lot of file not found error from compiler at each #import inside both headers and implementation files..
SPM seems not love this hierarchical structure; initially I tried to change all the imports by adding it’s own relative location (ie. #import "CRTOEventQueue.h to #import "../event/CRTOEventQueue.h").
It works fine and the entire library seems compiled successfully.
So I tried to create an example project and import the library.
While I can correctly import Criteo I cannot see any class header.

This is the first gotcha.
Exposing public interfaces in SPM requires a bit of work: SPM will automatically expose headers that are located in a directory named include inside the Sources folder.
We have different ways to solve it:

  • Move all header files into the include directory
    It works and it’s pretty simple and straightforwards. However it breaks the convention typically used to group header/implementations and the entire logical organization is fucked up too.
  • Use a separate header file that you only use for your Swift package
    This is pretty difficult to maintain and you need to change all the imports to include the relative paths as I said above.
  • Use symbolic links to headers inside the include directory
    Reasonable; you can use terminal’s shortcuts to create a symlink file for each .h inside the structure ln -s .../Sources/Criteo*/**/*.h .../Sources/Criteo/include.

In my case I preferred using the last option.
At this point we have exposed all the headers of our project.
Now we should tell the compiler where to search for headers in order to resolve the various imports inside the framework itself.
In order to do it I’ve used headerSearchPath options inside the cSettings node you can specify inside the Package.swift.

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Criteo",
    products: [
        .library(
            name: "Criteo",
            type: .dynamic,
            targets: ["Criteo"]),
    ],
    dependencies: [
    ],
    targets: [
        .target(
            name: "Criteo",
            dependencies: [],
            path: "Sources/Criteo",
            cSettings: [
                .headerSearchPath("../event"),
                .headerSearchPath("../event/serializer"),
                .headerSearchPath("../network"),
                .headerSearchPath("../product"),
                .headerSearchPath("../service"),
                .headerSearchPath("../util"),
            ])
    ]
)

Our Swift Package Manager lib is now ready. We need to fix the .podspec file in order to read our changes.
Obviously we need to change the path of the source; this is pretty easy: s.source_files = 'Sources/*/.{h,m}'.
Moreover we need to ignore our include folder to CocoaPods in order to avoid lots of warnings related to duplicate symbols. To accomplish it just add: s.exclude_files = 'Sources/Criteo/include/*/.h'.

Et voilà, our Obj-C library is ready to be used from SPM.

Leave a Comment