Lukas Z's Blog

How to Include Binary Vendor Frameworks in Xcode With and Without Cocoapods

Sometimes we have a closed source binary library that we want to include in our iOS project. To do that Xcode needs to do the following things:

  • Find the framework (via defaults or the Framework Search Paths setting).
  • Embed the framework in the bundle and sign it.
  • Link against the framework during build.
  • Strip any architectures from the framework binary that are not required (or even forbidden: You cannot for instance ship a binary that contains code for x86_64 through AppStoreConnect).

I want to add another complication, which is of course optional: The framework is only included for some configurations, for example DebugWithFramework. It should not be included in all other configs.

First I will show how to do it manually and then I will show the Cocoapods way, which, in my humble developer opinion, is superior.

Approach 1: The manual approach

So let’s say we want to do the above steps manually.

For the optional “only some configurations” requirement make sure the configurations have been created.

Step 1: Add the Framework

We can just pull the binary framework into our file tree in Xcode and we’re done. However, if we only want to include it in some configurations we should uncheck the target membership of the framework and add it manually.

Step 2: Framework Search Paths & Linker Setting

We need to tell our build system about our Framework. I believe this is already done for us if we add the Framework to our target and/or if we drag the Framework into the “Embedded Frameworks” box in settings.

But this part of the post is about the manual approach, with inclusion for only certain configurations, so we need to do two things: Set the Framework Search Path and tell the Linker.

But this is easy:

In Build Settings look for Other Linker Flags and add two lines for the configurations that matter (you may need to click the drop down arrow first): “-framework” and “CoolFramework” if “CoolFramework” is the name of your framework, otherwise please substitute the correct value.

Finally in Build Settings look for Framework Search Paths. Again select the configurations that you care about and add a new line below $(inherited) that looks something like this:


$(PROJECT_DIR)/Frameworks/MyCoolFramework/debug

And you should be good to go.

At this point your app probably compiles but at runtime it crashes complaining that it can’t find the Framework.

That’s because it’s not in the Bundle yet. For that you need the next step:

Step 3: Embed & Sign

If it’s supposed to be included for all configs we can just add it by dragging it into the Embedded Frameworks box in our project settings (assuming it’s not already there). Then make sure the option is set to “Embed & Sign” and you’re done.

It gets slightly more complicated if we include it manually. For that we can add a custom script build phase in our Build Phases tab that looks somewhat like this:


if [[ -z ${SCRIPT_INPUT_FILE_0} || -z ${SCRIPT_OUTPUT_FILE_0} ]]; then
    echo "This Xcode Run Script build phase must be configured with Input & Output Files"
    exit 1
fi

echo "Embed ${SCRIPT_INPUT_FILE_0}"
if [[ $CONFIGURATION == 'DebugWithFramework' || $CONFIGURATION == 'ReleaseWithFramework' ]]; then
      FRAMEWORK_SOURCE=${SCRIPT_INPUT_FILE_0}
        FRAMEWORK_DESTINATION=${SCRIPT_OUTPUT_FILE_0}
        DESTINATION_FOLDER=`dirname ${FRAMEWORK_DESTINATION}`
        
        mkdir -p ${DESTINATION_FOLDER}
        cp -Rv ${FRAMEWORK_SOURCE} ${FRAMEWORK_DESTINATION}

        CODE_SIGN_IDENTITY_FOR_ITEMS="${EXPANDED_CODE_SIGN_IDENTITY_NAME}"
        if [ "${CODE_SIGN_IDENTITY_FOR_ITEMS}" = "" ] ; then
            CODE_SIGN_IDENTITY_FOR_ITEMS="${CODE_SIGN_IDENTITY}"
        fi

        BINARY_NAME=`basename ${FRAMEWORK_DESTINATION} .framework`
        codesign --force --verbose --sign "${CODE_SIGN_IDENTITY_FOR_ITEMS}" ${FRAMEWORK_DESTINATION}/${BINARY_NAME}
        echo " ✅ Embedded successfully"
    else
        echo " ❌ Non MyCoolFramework build detected - do not embed"
    fi

Script phases have input and output files (extra input text fields), and in our case we can set it to the following values:


$(SOURCE_ROOT)/Frameworks/MyCoolFramework/debug/MyCoolFramework.framework

for the Input and


${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MyCoolFramework.framework

for the output.

So the output specifies where the binary goes in the compiled bundle.

Disclaimer: This code is a slightly modified snippet from Github. If I find it, I’ll link to it later.

Step 4: Strip Unnecessary Architectures

The app may run fine now in your Simulator or even on an actual iPhone or iPad, but if we want to submit our app to the AppStore we need to do one more thing: Remove architectures that are not used by the target devices.

Quick sidenote: What are architectures?

Well if you write source code it eventually gets compiled into machine code. Zeroes and Ones that are actually instructions (opcodes + arguments) for the CPU. But CPUs are different, they have different architectures and each architecture has different opcodes. (Simply speaking.) Until Apple switches to Apple Silicon, your mac (and your iOS Simulator) most likely has a x86_64 Intel CPU. And your iPhone most likely has a armv7 CPU.

If you have a binary framework that you did not compile yourself it must have the binary code for correct architectures already included. And the best case is that it has the code for _all_required architectures.

So what do we do before uploading to the AppStore?

We get rid off all binary code that is not for iPhone and iPad.

And we do it using a command line tool called lipo.

Now remember, Xcode does all this for you automatically, usually, but we are handling a custom case here where our framwork is binary.

Here’s the build script phase for that:


if [[ $CONFIGURATION == 'DebugWithFramework' || $CONFIGURATION == 'ReleaseWithFramework' ]]; then

  FRAMEWORK="MyCoolFramework"
  FRAMEWORK_EXECUTABLE_PATH="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/$FRAMEWORK.framework/$FRAMEWORK"
  EXTRACTED_ARCHS=()

  for ARCH in $ARCHS
  do
    lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH"
    EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH")
  done

  lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}"
  
  rm "${EXTRACTED_ARCHS[@]}"
  rm "$FRAMEWORK_EXECUTABLE_PATH"
  mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"
fi

This can be changed a bit to work for all frameworks I guess, but here it’s the special case in which we have one framework, MyCoolFramework, and two configurations DebugWithFramework and ReleaseWithFramework.

Step 5: Using the Framework in our code

Just briefly I want to mention how you can prevent compilation errors by using an #if clause in your code. Consider this Swift code:


#if canImport(MyCoolFramework)
import MyCoolFramework
#endif

You can use these if-blocks liberally everywhere in your Swift code.

Conclusion & Open Questions

I think we’re done. (If not please write a comment so I can fix it. I don’t have a lot of time to double check everything I’ve written here. Thanks.)

As you can see the manual approach is tiresome but at least we learn somehting about how Xcode builds apps for us!

I want to mention one more complication that can and probably will occur: What if our Framework has dependencies, for example Cocapods that must be present? They can be added in the Podfile but if we need to restrict the inclusion to certain configurations then we must take extra steps.

That’s why we should use Cocoapods all the way in the first place! ;)

Ok let’s look at how that’s done in the second part.

Approach 2: Using Cocapods

Now we will make our own Cocoapod that simply includes our vendor framework. Now why do we want to do that? Because Cocoapods takes care of all the steps above for us!

That’s right, the batteries are included. Let’s start.

Btw. I assume you are already using Cocoapods and your project already has a Podfile. (If not, please add Cocoapods now and run pod init in your project folder.)

Step 1: Writing a gemspec file

A pod is defined by a gemspec file that instructs the pod binary what to do when installing. I’ll keep it brief and just paste you own for our case. But there are more options which may be required. Especially if you are also having swift and objective c files in there somewhere that must be compiled. Our case is actually simpler because the framework is binary.

We’re writing a local pod by the way. You could have it remotely in some git repo and you could even publish it in the cococapods repository but our case here it’s local only and it’s in the same git repository as our app. It’s basically just in a subfolder.

Here we go:


Pod::Spec.new do |spec|
  spec.name         = "MyCoolFramework"
  spec.version      = "0.0.1"
  spec.summary      = "Podspec wrapper for the MyCoolFramework framework."
  spec.authors      = { "Lukas Zielinski" => "http://www.lukaszielinski.de" }
  spec.homepage     = "http://www.lukaszielinski.de"
  spec.platform     = :ios, "12.0"
  spec.source       = { :http => 'file://' + __dir__ + '/MyCoolFramework.framework.zip' }
  spec.public_header_files = "MyCoolFramework.framework/Headers/*.h"
  spec.ios.vendored_frameworks = "MyCoolFramework.framework"

  spec.dependency 'RxSwift', '5.1.0'
  spec.dependency 'RxCocoa', '5.1.0'
  spec.dependency 'RxDataSources', '4.0.1'
end

The gemspec should be in the same folder as they framework and the framework folder should be zipped. Cocoapods will unzip the archive during installation.

There’s also a speciality here: Our framework has dependencies, it requires the RxSwift pod to run. I’ve added this to show you to demonstrate that it’s easy to add dependencies now and also to show a peculiarity about Cocoapods below.

Step 2: Adding our brand new pod

Let’s see how our Podfile looks now:


target 'App' do
  pod "MyCoolFramework", :podspec => "Frameworks/MyCoolFramework/debug/", :configurations => ["DebugWithFramework, "ReleaseWithFramework"]

  pod "RxCocoa", "5.1.0", :configurations => ["DebugWithFramework, "ReleaseWithFramework"]
  pod "RxDataSources", "4.0.1", :configurations => ["DebugWithFramework, "ReleaseWithFramework"]
  pod "RxSwift", "5.1.0", :configurations => ["DebugWithFramework, "ReleaseWithFramework"]
  # explicit dependency for RxCocoa:
  pod 'RxRelay', '~> 5.0', :configurations => ["DebugWithFramework, "ReleaseWithFramework"]
end

And if we don’t care about configurations it’s just this:


target 'App' do
  pod "MyCoolFramework", :podspec => "Frameworks/MyCoolFramework/debug/"
end

Now we can run pod install and we’re good to go! We can run the app and even submit it to the AppStore, because Cocoapods takes care of all the steps from the manual approach for us!

There’s one thing that you should look out for regarding the configurations. As you see in the Podfile examples, it gets much more verbose when we want to restrict the pod to be included in certain configurations only. That is because Cocoapods requires us to also specify all the dependencies and the dependencies’ dependencies (transitive dependencies) explicitely IF we want to restrict them to some configurations.

Consider this:


target 'App' do
  pod "MyCoolFramework", :podspec => "Frameworks/MyCoolFramework/debug/", :configurations => ["DebugWithFramework, "ReleaseWithFramework"]
end

Here our framework is not included in the “Debug” stage, however it’s dependencies are. This is something to look out for.

Protip: Build and afterwards look inside your compiled app’s package. There’s a frameworks folder there. You should only see pods that are dependencies of our framework in apps build with those configurations.

Final words

That’s it, I hope this helps someone. Please send me any corrections or comment below. Thank you!

P.S.: You can follow me on Twitter.

Comments

Webmentions