Using System Headers in Swift

CocoaPods to the Rescue


Note: I’m not entirely happy with the formatting of the code blocks below. You can read the original post on Ind.ie if you like. You cannot leave comments there though.

One of the nice things about Swift is that is was designed to interact not only with Objective-C, but also directly with C. In order for this to work, the C-based APIs need to be exposed as Swift modules. Out of the box the iOS development environment already contains a number of modules for quite a few standard system APIs. For my NetUtils framework, I needed one for ifaddrs.h though, which is not available. Since I couldn’t find the information I needed to do this in one place, I am putting it all down in this article.

Step 1: Set up the framework

After I had setup my framework, I wanted to start using getifaddrs from ifaddrs.h. Normally, in C you would just type #include <ifaddrs.h>. In Swift, you could do the same in your bridging header when your target is an application, but not when it’s a framework. So when I tried to add this include to my umbrella header, I got the following error message:

Terminal output:
/<path_to_project>/NetUtils/NetUtils/NetUtils.h:13:10: error: include of non-modular header inside framework module 'NetUtils' #include <ifaddrs.h>
^

Setting “Allow Non-modular Includes In Framework Modules” to YES didn’t help, I still got the same error message.

After doing some research, I came to the conclusion that I needed to define a module for ifaddrs.h before I could use it. When you search for this on the Internet, you will probably find this Stack Overflow post that describes a way of doing this (in a few variations). Also, I found this gist that is quite nice because when you use that, you don’t need to change your project’s build settings anymore. It does require a custom shell script build phase though.

Step 2: Using the framework as a CocoaPod

Both solutions have one problem, though: when you want to use the framework as a CocoaPod, they don’t work. The first doesn’t work because the required build settings are not used by CocoaPods, and the second because the required shell script build phase is not used by CocoaPods. The reason for this is that CocoaPods creates its own project and target for the pod, and it doesn’t know that it needs to apply those custom build settings.

After a few hours of tinkering, trial and error, thinking, and more trial and error, I came up with a solution that I think is quite acceptable. It has two parts: (a) adding the module.modulemap files to the project, and (b) telling CocoaPods that it needs to add the right build settings so that the module map files are used.

Adding the Module Map Files

I want my framework to be usable on iOS, iPhone Simulator, and OS X. Since they all have a different path to the ifaddrs.h file, I created three module maps. This is the one for the iPhone Simulator, the other two are similar:

Module map:
module ifaddrs [system] [extern_c] {
header "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/include/ifaddrs.h"
export *
}

I stored this file in my project at the path $(SRCROOT)/ifaddrs/iphonesimulator/module.modulemap. The other two have their own folders that also contain only this module.modulemap file. In order to be able to build the framework itself, I of course had to tell Xcode to use these module maps by setting the “Import Paths” build setting in the “Swift Compiler — Search Paths” section. You should use the path $(SRCROOT)/ifaddrs/iphonesimulator/module.modulemap; the screen shot below shows the expanded version of that variable.

So now your framework should compile and you should be able to use the ifaddrs module by simply importing it in the header of your Swift files. However, when you try to use it as a CocoaPod, you will get compiler errors saying that the ifaddrs module is unknown.

Configuring the CocoaPod

CocoaPods to the rescue! As I wrote before, CocoaPods doesn’t know that these special build settings have to be used. Fortunately, there is a way to tell it that it has to, by adding the xcconfig key to the pod spec. In my case, in the (JSON) pod spec, I added these lines:

Podspec fragment:
"xcconfig": {
"SWIFT_INCLUDE_PATHS[sdk=iphoneos*]": "$(SRCROOT)/NetUtils/ifaddrs/iphoneos",
"SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]": "$(SRCROOT)/NetUtils/ifaddrs/iphonesimulator",
"SWIFT_INCLUDE_PATHS[sdk=macosx*]": "$(SRCROOT)/NetUtils/ifaddrs/macosx"
},
"preserve_paths": [ "ifaddrs/*" ],

The preserve_paths directive is needed because otherwise CocoaPods will discard the module map files.

Please note that the path now also contains NetUtils, which it didn’t before. The reason is that CocoaPods uses a folder structure where the Pods.xcodeproj project contains all the pod folders, of which NetUtils (my project’s name) is one.

Not Every Computer Is The Same

There is one more thing left to do. You might have noticed that the module.modulemap files contain the path of Xcode itself (/Applications/Xcode.app). For my computer that is (currently) fine, because that’s what I’m using. Someone else may use different Xcode versions side by side or use a completely different location for Xcode. In order to allow for that, I added a small shell script that replaces the default Xcode path with the configured path on your computer. This is done by calling xcode-select -p, so if you have changed it using xcode-select or even if you have changed it only by setting the DEVELOPER_DIR environment variable, it will use the correct path.

I called this shell script injectXcodePath.sh and it has the following contents. It includes a few utility functions that I regularly use besides the main function.

Bash:
#!/bin/sh defaultXcodePath="/Applications/Xcode.app/Contents/Developer"
realXcodePath="`xcode-select -p`"
fatal() {
echo "[fatal] $1" 1>&2
exit 1
}
absPath() {
case "$1" in
/*)
printf "%s\n" "$1"
;;
*)
printf "%s\n" "$PWD/$1"
;;
esac;
}
scriptDir="`dirname $0`"
scriptName="`basename $0`"
absScriptDir="`cd $scriptDir; pwd`"
main() {
for f in `find ${absScriptDir} -name module.modulemap`; do
cat ${f} | sed "s,${defaultXcodePath},${realXcodePath},g" > ${f}.new || fatal "Failed to update modulemap ${f}"
mv ${f}.new ${f} || fatal "Failed to replace modulemap ${f}"
done
}
main $*

Now I have to tell CocoaPods to run this script. You can do that using the prepare_command key in the pod spec:

Podspec fragment:
"prepare_command": "ifaddrs/injectXcodePath.sh"

Complete Pod Spec

That’s it, everything should work now. For your convenience, here is the complete pod spec of version 0.4 of NetUtils:

CocoaPods podspec:
{
"name": "NetUtils",
"version": "0.4",
"summary": "Swift library that simplifies getting information about your network interfaces and their properties, both for iOS and OS X.",
"homepage": "https://github.com/svdo/swift-netutils",
"license": {
"type": "The Unlicense <http://unlicense.org>"
},
"source": {
"git": "https://github.com/svdo/swift-netutils.git",
"tag": "0.4"
},
"authors": "Stefan van den Oord",
"platforms": {
"ios": "8.0",
"osx": "10.9"
},
"source_files": "NetUtils/**/*.swift",
"requires_arc": true,
"xcconfig": {
"SWIFT_INCLUDE_PATHS[sdk=iphoneos*]": "$(SRCROOT)/NetUtils/ifaddrs/iphoneos",
"SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*]": "$(SRCROOT)/NetUtils/ifaddrs/iphonesimulator",
"SWIFT_INCLUDE_PATHS[sdk=macosx*]": "$(SRCROOT)/NetUtils/ifaddrs/macosx"
},
"preserve_paths": [ "ifaddrs/*" ],
"prepare_command": "ifaddrs/injectXcodePath.sh"
}

Feedback

As always, feedback is much appreciated. Thanks!


Originally published at ind.ie.

Show your support

Clapping shows how much you appreciated Stefan van den Oord’s story.