Distributing a Swift Macro using CocoaPods

Go from confusion to confidence with a step-by-step Swift Concurrency course, helping you smoothly migrate to Swift 6 and fully leverage its features.
In the last article, I showed you how you can compile a Swift macro into a binary and import it into your Xcode project without using Swift Package Manager.
In this article, I want to take it a step further and show you how you can use CocoaPods to distribute a macro binary with no extra setup required by the client.
Creating a Pod library
Before you can distribute your macro using CocoaPods, you need to create a Pod library that encapsulates the macro binary and exposes the macro API to its clients in a similar way to how Swift Package Manager does it.
This is a relatively simple process, but there are a few things that you need to be aware of.
Installing CocoaPods
The first thing that you need to do is to install CocoaPods if you haven’t already. I would recommend you do this using Bundler to avoid polluting your system Ruby installation and to ensure that everyone on your team is using the same version of CocoaPods.
You can do this by creating a Gemfile in an empty directory:
mkdir StringifyMacro
bundle init
Next, open the generated Gemfile and paste the following:
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "cocoapods"
To install the contents of the Gemfile, run the following command in the terminal:
bundle install --path vendor/bundle
This command will install cocoapods and all of its dependencies in the repository’s directory without needing to install them globally on your system. Once this step is completed, you’ll be able to run pod commands by calling bundle exec pod.
The Pod library
Now that you have CocoaPods installed, you can create a Pod library by running:
bundle exec pod lib create StringifyMacro
This command will download a template and guide you through several steps to configure the library as you wish. For this article, I’ll be making a simple iOS library with no demo application.
Writing the code
The pod library will be light and will contain a single source file called StringifyMacro.swift. This will be the file that contains the public declaration for the macro that clients will use:
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "StringifyMacros", type: "StringifyMacro")
To adhere to the CocoaPods guidelines, you should create this file in the Classes directory for the library. This is similar to SPM’s Sources directory for a given target.
Finally, clients need to access the macro implementation, which you will have in the form of a binary. Create a macros/ directory next to the Classes one and copy the macro binary into it.
If you are not sure how to create a binary for your macro, I would recommend you check out the How to import Swift macros without using Swift Package Manager article on my blog.
Writing the Podspec file
By default, the pod lib create command will create a StringifyMacro.podspec file for you with some example content. This file contains all the information that CocoaPods needs to know about your library to be able to distribute it.
You will need to modify this file to tell CocoaPods where to find the macro binary and what to do with it:
Pod::Spec.new do |s|
s.name = 'StringifyMacro'
s.version = '0.1.7'
s.summary = 'A proof of concept macro to show they can work with cocoapods.'
s.description = <<-DESC
A proof of concept macro to show they can work with cocoapods.
DESC
s.homepage = '<homepage>'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '<your_name>' => '<your_email>' }
s.source = { :git => '<repository_where_the_spec_lives>', :tag => s.version.to_s }
s.ios.deployment_target = '16.0'
# 1
s.source_files = ['StringifyMacro/Classes/**/*']
s.swift_version = "5.9"
# 2
s.preserve_paths = ["StringifyMacro/macros/StringifyMacros"]
# 3
s.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '-load-plugin-executable ${PODS_ROOT}/StringifyMacro/StringifyMacro/macros/StringifyMacros#StringifyMacros'
}
# 4
s.user_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '-load-plugin-executable ${PODS_ROOT}/StringifyMacro/StringifyMacro/macros/StringifyMacros#StringifyMacros'
}
end
Let’s break down the important parts of the Podspec file step by step:
CocoaPodswill look for the macro definition file in theClassesdirectory of the library.- The macro binary is not a source file, so you need to tell CocoaPods to preserve it when it copies the source files to the client project so it can be linked against.
- Populate the
OTHER_SWIFT_FLAGSbuild setting for the pod target with the-load-plugin-executableflag and the path to the macro binary. - Step number 3 is not enough to make the macro available to the client project. While the pod target itself will compile, you will get an error when you try and use the macro in the client project saying that the macro implementation could not be found. To fix this, you need to add the same build setting to the client project using the
user_target_xcconfigproperty.
Importing and using the macro
Once you publish the CocoaPod, you can start using it in a project by simply adding the dependency to a specific target:
target 'StringifyMacroExample' do
use_frameworks!
pod 'StringifyMacro'
end
After running pod install, you should be able to use the macro in your code like so:
import SwiftUI
import StringifyMacro
struct ContentView: View {
var body: some View {
VStack {
Text("Hello, CocoaPods macros!")
}
.onAppear {
print(#stringify(3 + 5))
}
.padding()
}
}