Xcode build rules for different variants of the same .swift file

Bogdan Cosescu
Mac O’Clock
Published in
3 min readApr 11, 2020
Photo by Pritesh Sudra on Unsplash

While working on a Swift SDK framework, I was faced with the following problem: some clients need access to a large set of functionality from the SDK, while others only to a subset. That meant that the same protocol/extension/class/enum be marked public for those advanced clients and internal to others.

The first approach was to use Swift’s #if/#else/#endif preprocessor macros but their functionality is limited. Take for example the following code:

So, it seems obvious that this approach will duplicate code(the body of the protocol SDKData will be duplicated). This can become a nightmare to maintain as the project will grow.

Instead, I wanted something like macro replacement, reminiscent from C languages but not supported in Swift:

Of course, @Visibility would be a macro, replaced with either public or private depending on the desired target.

Xcode Build Rules to the rescue

The solution was to add a custom build rule in all of our project to do this primitive string replacement. New code that was supposed to be either public or internal will be saved in .swiftGen file. This file would then be picked by the build rule associated to the *.swiftGen files which will produce the same file but now with .swift extension and with @Visibility macro replaced with the desired value(public or internal). The generated swift file will then be automatically compiled by Swift Compiler rule.

The generated swift file is saved in ${Project}/.gen directory. Of course, .gen directory would be added in .gitignore as the .swiftGen file would act as a template for the swift file from now on.

${VISIBILITY} can be maintained in two separate .xcconfig files which can then be inherited by the targets. One target will produce the SDK for the advanced users and will look like this:

while the other one will look like this:

Adding the generated .swift file in your project

As a final step, I wanted to have the output files generated by the build rule applied to *.swiftGen file added to my project. This would help during debugging. To modify the Xcode project I used xcodeproj. I added a “Run script” phase in each target affected by the build rule:

Here is the output for AddGeneratedFilesToProject.rb:

Summary

Limited macro support in Swift is understandable as it has caused a lot of issues with developers familiar with C languages. In this case though, it was necessary not to duplicate the same protocol/class/enum body for each desired accessibility level. Is there another way I could have solved this? Maybe… I would love to hear your experience with this type of problem.

--

--