Updating Objective-C Frameworks for Swift Package Manager

Intro

You have a dusty old Objective-C framework that you want to publish as a Swift Package Manager Library. Here’s how.

iStumbler Labs publishes a number of Objective-C frameworks and they all need updating to work with Swift Package Manager:

Updating all of these for Swift Package Manager was a bit of a chore but there’s an easy to implement process to make it less painless.

Follow the Project Layout Conventions

Swift Package Manager (SPM) expects a particular source code layout, e.g.

Project/
  Project.xcodeproj  ← Not strictly necessary for a Swift Package, nice to have
  Package.swift      ← Defines the products, dependencies, and targets for Project
  Sources            ← Source code for all your projects
    Project          ← Name of the library target
      includes       ← Header files go here
    Tests            ← Test target
    projectctl       ← Command Line Tool

It’s possible to configure different locations for source files and includes, however it’s easiest to follow the SPM convention, as this simplifies the package description and makes it easier for other developers to understand the project structure.

Example Project

We’ll use IcedHTTP as an example, here is the finder layout:

And the Xcode project layout:

First create the expected structure in the Finder:

Then add Sources to your Xcode project as a Group:

Move all the .m files and the Info.plist to the Project folder (Sources/IcedHTTP in this example), and move all the .h files to Sources/includes in the Xcode Project Navigator.

You will need to adjust the Info.plist File Build Setting for each Target, this is a good time to update your build schemes (in particular, I like to add a Composite Target for building All the targets in the project):

At this point check to make sure all your targets build correctly from the Xcode Project

Now we can define our Swift Package, create Package.swift at the root of the project, next to the Xcode Project

Content of the Package.swift file should look like:

// swift-tools-version:5.10
import PackageDescription

let package = Package(
  name: "IcedHTTP",
  platforms: [.macOS(.v10_14), .iOS(.v14), .tvOS(.v14)],
  products: [
    .library( name: "IcedHTTP", type: .dynamic, targets: ["IcedHTTP"])
  ],
  targets: [
    .target(name: "IcedHTTP")
  ]
)

Once that file is saved we can open the Swift Package in Xcode (you don’t have the close the Xcode Project, they can both be open which is handy for cross-checking the Package definition and Xcode builds. Note that the Package Icon replaces the Xcode Project icon in the upper left corner:

Any issues with the Package definition will show up automatically, error messages are generally helpful.

If you get errors like 'Framework/Framework.h' file not found you will need to wrap imports using the SWIFT_PACKAGE compile time #define:

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

Once your package definition is correct, you can see the built product, depending on which run destination you have selected. Choose Product -> Show Build Folder in Finder

Double check your Xcode Project to make sure you can still build using your configured targets.

Commit your changes to the git repo (you have a git repo, right?) and push them to your favorite git hosting service.

When you want to publish a version of your pacakge, tag your repo with a Sepantic Version String and push to a public repo.