So you have a solution to a problem that many other iOS developers have and you want to help them, but you also want to protect your code and build a business? Binary framework. That’s what you want!
You might have noticed that you can’t see the source code of the UIKit frameworks you use to build your iOS apps on top of every day. That’s because Apple ships the UIKit as a binary framework in its iOS SDK.
If you’re distributing a framework that contains intellectual property, this is the way to do it.
Binary Frameworks
A binary framework is already compiled source code with resources with a defined interface that you can use in your apps. It comes in two flavors: a static library and a dynamic framework. In this article, we’ll focus on dynamic frameworks.
I will walk you through creating an iOS binary framework and (spoiler alert: here comes the real tricky part…) distributing one.
We have quite the experience with this as we have been distributing the iOS Instabug SDK as a binary framework for many years now, through many changes (static to dynamic) and tools (migrating to CocoaPods), and from recently shipping one monolithic framework to two frameworks.
So let’s get to it.
Contents
Setup
Creating a Demo App
Creating a Demo App
Creating a Binary Framework
Explaining What Just Happened
Adding a Very Basic Function
Using Your Binary Framework in Your Demo App
Recapping What You Just Did
Explaining What Just Happened
Adding a Very Basic Function
Using Your Binary Framework in Your Demo App
Recapping What You Just Did
Distributing a Binary Framework
Exporting Framework
Creating a Second Test App
Integrating Your Binary Framework in Your Test App
Publishing Your Test App
Recapping What You Just Did
Exporting Framework
Creating a Second Test App
Integrating Your Binary Framework in Your Test App
Publishing Your Test App
Recapping What You Just Did
One More Thing
Setup
A good practice is to always have a demo app for your binary framework. This will make testing your changes easy and quick in an environment you already familiar with. Let’s start by creating a simple iOS app.
Creating a Demo App
Open Xcode.
From the top menu select New.
From the submenu, select Project.
Choose Single View App and press Next.
Set Product Name to “HelloDemoApp” and press Next.
Choose your preferred destination for the new project.
Congratulations, you just created your demo app!
Creating a Binary Framework
Now that you have your demo app in place, let’s add your new binary framework.
Go to HelloDemoApp.Xcodeproj — the first file in the project navigator panel on the far left.
If the left-side menu isn’t expanded, tap on this button at the top left:
You will find TARGETS and listed are your apps. That’s how Xcode organizes its files: Workspace > Project > Targets.
Targets have compiled sources (source files) and source files can be shared with other targets in the same project.
Each target produces a Product. In the case of an app, the product is your app.
At the bottom left you will find these icons: Tap on the
icon to add a new target.
Scroll to the Framework & Library section.
Choose Cocoa Touch Framework then press Next.
Set the Product Name to “HelloLoggingFramework” and make sure the language is set to Objective-C. (Don’t be scared — this article is about the setup, not the code. If you’re curious about why we use Objective-C, check out Why My Team Doesn’t Use Swift And Can’t Anytime Soon.)
Press Finish.
Explaining What Just Happened
You should now see two new targets below your app in the left-side menu: HelloLoggingFramework and HelloLoggingFrameworkTests.
You should also see two new directories in the project navigator panel on the far left with the same names. Xcode has generated two new targets for you: the framework target and a unit test target for that framework.
If you expand the HelloLoggingFramework directory, you will find two files: Info.plist and HelloLoggingFramework.h — the latter is the umbrella header file that will be used to communicate between your framework and the host app (written in Swift or Objective-C).
Adding a Very Basic Function
Next, let’s add a class to your new binary framework.
Create a new class
We will create a new class to use in your app. Let’s call it HelloLogger
Right-click on the HelloLoggingFramework directory in the project navigator panel.
Choose New File… from the menu.
Choose Cocoa Touch Class and press Next.
Make sure you set:
- Class: “HelloLogger”
- Subclass of: “NSObject”
- Language: “Objective-C”
Press Next then Create.
Done!
Add a single method to that class
Now we will add the method helloWithText:
that will just append “Hello” to the beginning of the text and print it in the console.
Open the HelloLogger.h file and add this line:
- (void)helloWithText:(NSString *)text;
Then, open HelloLogger.m and add this snippet:
- (void)helloWithText:(NSString *)text { NSLog(@"Hello, %@", text); }
Done with that, too! Good progress.
Using Your Binary Framework in Your Demo App
Next, we will use your new binary framework in your app.
Open ViewController.swift in your HelloDemoApp directory in the project navigator panel.
Add an import
statement for your binary framework at the top of the file:
import HelloLoggingFramework
Now, try to create an instance of your class in viewDidLoad
and add this line:
let helloLogger = HelloLogger()
OOPS!
It says Use of unresolved identifier 'HelloLogger'.
We didn’t have any problems importing your binary framework, so that’s fine, but we can’t use its only class. Why?
That’s because any class you create in a binary framework by default has access scope to Project
, which isn’t accessible outside that target. To fix this:
Make your class public
Select the HelloLoggingFramework target from the left-side menu.
Select Build Phases from the top bar.
Expand the Headers section.
Drag your class HelloLogger.h
from Project to Public.
Now let’s go back to ViewController.swift from the far left project navigator panel and check that error again.
Still there. ?
Remember that HelloLoggingFramework.h? Let’s take a look at it.
It says:
In this header, you should import all the public headers of your framework using statements like #import <HelloLoggingFramework/PublicHeader.h>
HelloLoggingFramework/PublicHeader.h
is what’s called an “umbrella header” of your binary framework. It’s your interface with the outside world and it dictates that to expose any class, you need to import its header there.
So let’s add this:
#import <HelloLoggingFramework/HelloLogger.h>
Now, let’s head back to ViewController.swift. The error is gone. ?
Next, let’s actually use that class.
Add this line:
helloLogger.hello(withText: "World")
Run the demo app by clicking the icon in the top left of Xcode, and check the console at the bottom right.
Good job!
Recapping What You Just Did
- You created a demo app.
- You created a binary framework to use in your app.
- You added a class to this binary framework.
- You exposed that class to use in your app.
- You called that class from your binary framework to your app.
Now you have a perfect setup for developing your binary framework, but how about sharing that functionality with others without sharing the code?
Distributing a Binary Framework
In this section, you will distribute your binary framework to a second app, but first we need to export your binary framework.
Exporting Your Framework
Select your HelloLoggingFramework target from the Schemes menu at the top of Xcode.
From the Product menu at the top bar, choose Clean, then choose Build.
Find your binary framework by expanding the Products directory in the project navigator panel. Right click on HelloLoggingFramework.framework, then press Show in Finder.
Grab your binary framework file HelloLoggingFramework.framework and save it somewhere you can access later.
Creating a Second Test App
You will now create another iOS app.
It’s a simple Single View App, like the one you created before.
Let’s call it “TestApp”. This app will be for simulating your users’ integrations.
Run your test app to make sure everything is working fine.
All good? Great.
Integrating Your Binary Framework in Your Test App
Drag your binary framework file HelloLoggingFramework.framework and drop it into your TestApp directory in the project navigator panel.
You should see this prompt:
Press Finish.
Now, go to ViewController.swift and use your binary framework like before.
Add an import
statement for your binary framework at the top of the file:
import HelloLoggingFramework
Then, add this snippet in viewDidLoad
:
let logger = HelloLogger() logger.hello(withText: "world")
Run your app.
If you see this error below, it’s because your dynamic framework needs to be embedded in your app.
dyld: Library not loaded: @rpath_HelloLoggingFramework.framework_HelloLoggingFramework Referenced from: _Users_yousefhamza_Library_Developer_CoreSimulator_Devices_70678C21-1274-4422-9472-661307C51C24_data_Containers_Bundle_Application_EF6F9477-63E9-4024-AD90-4AEC3EF3A8FA_TestApp.app_TestApp Reason: image not found
You can do this by selecting TestApp.Xcodeproj from your project navigator.
Then, select your TestApp target.
Addyour binary framework in the Embedded Binaries section.
Run your test app.
Everything works and console logs are just as expected. ?
Publishing Your Test App
When you publish your framework out there for other people, you should consider their usage end to end.
In the Schemes menu at the top of Xcode, choose a physical device or Generic iOS Device.
From the top bar, choose Product > Archive.
Error?
ld: warning: ignoring file /Users/yousefhamza/Repos/TestApp/TestApp/HelloLoggingFramework.framework/HelloLoggingFramework, file was built for x86_64 which is not the architecture being linked (arm64): /Users/yousefhamza/Repos/TestApp/TestApp/HelloLoggingFramework.framework/HelloLoggingFramework Undefined symbols for architecture arm64: "_OBJC_CLASS_$_HelloLogger", referenced from: objc-class-ref in ViewController.o ld: symbol(s) not found for architecture arm64 clang: error: linker command failed with exit code 1 (use -v to see invocation)
The decryption of that says that your binary framework was built for architecture x86_64
(the simulator) and we are now building for a device, whose architecture is arm64
, and Xcode can’t find it in your binary framework.
If we just went ahead and built our framework on a device to get the arm64
architecture, we would get that error when trying to build the test app on the simulator.
So what do we do?
Introduce an aggregated target
You have been introduced to the framework target, now we will introduce a new target: the aggregated target.
An aggregated target is an Xcode target that lets you build a group of targets at once. It doesn’t have any Products itself, like an app for an app target or a framework for framework targets. It doesn’t have build rules either, but it can have a Run Script
build phase or a Copy Files
build phase only.
Let’s go back to our original project (demo app + framework).
In the top bar, select File > New > Target.
In the Cross-platform section under Other, you will find Aggregate. Press Next.
Let’s just call the Product Name: “Framework”. Press Finish.
First, we need it to run with the release configuration.
Open the Schemes menus and choose your new Framework scheme.
Then, open it again and choose Edit Scheme….
From Run, set Build Configuration to Release.
Now, in the just created Framework aggregated target, let’s go to the Build Phases tab.
Addyour binary framework to the Target Dependencies section.
Now for a new trick. We will add a build phase from that button above Target Dependencies.
Choose New Run Script Phase.
Let’s write that script!
LIPO
LIPO will be our friend in this section. It’s what will actually fix our previous error by combining the binaries of your framework built for the simulator (arch x86_64
) and for the device (arch arm64
).
Note that in this step, we will archive the frameworks, not only build them. We need to do this to get the “. bcsymbolmap” files. These are needed with the framework and its dYSMs so when Apple rebuilds your framework on iTunes Connect, you can get the final dYSMs of your framework for your favorite crash reporting tool to symbolicate your crash reports.
In the following steps, we will be working with lots of paths, so it’s easier to create and store them in some variables. Add the following snippet to the top of the script:
FRAMEWORK_NAME="HelloLoggingFramework" SIMULATOR_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${FRAMEWORK_NAME}.framework" DEVICE_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FRAMEWORK_NAME}.framework" DEVICE_BCSYMBOLMAP_PATH="${BUILD_DIR}/${CONFIGURATION}-iphoneos" DEVICE_DSYM_PATH="${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FRAMEWORK_NAME}.framework.dSYM" SIMULATOR_DSYM_PATH="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${FRAMEWORK_NAME}.framework.dSYM" UNIVERSAL_LIBRARY_DIR="${BUILD_DIR}/${CONFIGURATION}-iphoneuniversal" FRAMEWORK="${UNIVERSAL_LIBRARY_DIR}/${FRAMEWORK_NAME}.framework" OUTPUT_DIR="./HelloLoggingFramework-Aggregated"
Then add this line to your script:
Xcodebuild -project ${PROJECT_NAME}.Xcodeproj -scheme ${FRAMEWORK_NAME} -sdk iphonesimulator -configuration ${CONFIGURATION} clean install CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphonesimulator
This builds your framework for the target simulator.
Add the following line to your script, too:
Xcodebuild -project ${PROJECT_NAME}.Xcodeproj -scheme ${FRAMEWORK_NAME} -sdk iphoneos -configuration ${CONFIGURATION} clean install CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphoneos
This builds your framework for the device.
After creating the frameworks, let’s continue with your script.
Let’s clean up the final directories:
rm -rf "${UNIVERSAL_LIBRARY_DIR}" mkdir "${UNIVERSAL_LIBRARY_DIR}" mkdir "${FRAMEWORK}" rm -rf "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR"
Now, we take one of the framework files to our universal folder:
cp -r "${DEVICE_LIBRARY_PATH}/." "${FRAMEWORK}"
Note that here, we actually want all the framework files from the device product, but it’s the binary only that we need to merge.
Now for the real magic, lipo
, add this snippet:
lipo "${SIMULATOR_LIBRARY_PATH}/${FRAMEWORK_NAME}" "${DEVICE_LIBRARY_PATH}/${FRAMEWORK_NAME}" -create -output "${FRAMEWORK}/${FRAMEWORK_NAME}" | echo cp -r "${FRAMEWORK}" "$OUTPUT_DIR"
That will merge the binaries of the simulator product framework and the device product simulator and copy the result to our output directory.
What we just created here is called a “fat” binary. That’s why the tool that extracts single architectures out of a fat binary (or adds architectures to a single binary) is called lipo
, as in “liposuction”.
Now we actually need to do the same with dYSMs files and add the result to that framework at the output directory.
cp -r "${DEVICE_DSYM_PATH}" "$OUTPUT_DIR" lipo -create -output "$OUTPUT_DIR/${FRAMEWORK_NAME}.framework.dSYM/Contents/Resources/DWARF/${FRAMEWORK_NAME}" \ "${DEVICE_DSYM_PATH}/Contents/Resources/DWARF/${FRAMEWORK_NAME}" \ "${SIMULATOR_DSYM_PATH}/Contents/Resources/DWARF/${FRAMEWORK_NAME}" || exit 1
And this magic snippet:
UUIDs=$(dwarfdump --uuid "${DEVICE_DSYM_PATH}" | cut -d ' ' -f2) for file in `find "${DEVICE_BCSYMBOLMAP_PATH}" -name "*.bcsymbolmap" -type f`; do fileName=$(basename "$file" ".bcsymbolmap") for UUID in $UUIDs; do if [[ "$UUID" = "$fileName" ]]; then cp -R "$file" "$OUTPUT_DIR" dsymutil --symbol-map "$OUTPUT_DIR"/"$fileName".bcsymbolmap "$OUTPUT_DIR/${FRAMEWORK_NAME}.framework.dSYM" fi done done
What this does is two things:
- Copies the “.bcsymbolmap” file to our output directory.
Fixes an issue where some symbols on your crash reports after symbolication appear
hidden
.
Here’s the full script:
FRAMEWORK_NAME="HelloLoggingFramework" SIMULATOR_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${FRAMEWORK_NAME}.framework" DEVICE_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FRAMEWORK_NAME}.framework" DEVICE_BCSYMBOLMAP_PATH="${BUILD_DIR}/${CONFIGURATION}-iphoneos" DEVICE_DSYM_PATH="${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FRAMEWORK_NAME}.framework.dSYM" SIMULATOR_DSYM_PATH="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${FRAMEWORK_NAME}.framework.dSYM" UNIVERSAL_LIBRARY_DIR="${BUILD_DIR}/${CONFIGURATION}-iphoneuniversal" FRAMEWORK="${UNIVERSAL_LIBRARY_DIR}/${FRAMEWORK_NAME}.framework" OUTPUT_DIR="./HelloLoggingFramework-Aggregated" Xcodebuild -project ${PROJECT_NAME}.Xcodeproj -scheme ${FRAMEWORK_NAME} -sdk iphonesimulator -configuration ${CONFIGURATION} clean install CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphonesimulator Xcodebuild -project ${PROJECT_NAME}.Xcodeproj -scheme ${FRAMEWORK_NAME} -sdk iphoneos -configuration ${CONFIGURATION} clean install CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphoneos rm -rf "${UNIVERSAL_LIBRARY_DIR}" mkdir "${UNIVERSAL_LIBRARY_DIR}" mkdir "${FRAMEWORK}" rm -rf "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR" cp -r "${DEVICE_LIBRARY_PATH}/." "${FRAMEWORK}" lipo "${SIMULATOR_LIBRARY_PATH}/${FRAMEWORK_NAME}" "${DEVICE_LIBRARY_PATH}/${FRAMEWORK_NAME}" -create -output "${FRAMEWORK}/${FRAMEWORK_NAME}" | echo cp -r "${FRAMEWORK}" "$OUTPUT_DIR" cp -r "${DEVICE_DSYM_PATH}" "$OUTPUT_DIR" lipo -create -output "$OUTPUT_DIR/${FRAMEWORK_NAME}.framework.dSYM/Contents/Resources/DWARF/${FRAMEWORK_NAME}" \ "${DEVICE_DSYM_PATH}/Contents/Resources/DWARF/${FRAMEWORK_NAME}" \ "${SIMULATOR_DSYM_PATH}/Contents/Resources/DWARF/${FRAMEWORK_NAME}" || exit 1 UUIDs=$(dwarfdump --uuid "${DEVICE_DSYM_PATH}" | cut -d ' ' -f2) for file in `find "${DEVICE_BCSYMBOLMAP_PATH}" -name "*.bcsymbolmap" -type f`; do fileName=$(basename "$file" ".bcsymbolmap") for UUID in $UUIDs; do if [[ "$UUID" = "$fileName" ]]; then cp -R "$file" "$OUTPUT_DIR" dsymutil --symbol-map "$OUTPUT_DIR"/"$fileName".bcsymbolmap "$OUTPUT_DIR/${FRAMEWORK_NAME}.framework.dSYM" fi done done
Now run your aggregated Framework target and check your project directory.
Let’s go back to our TestApp:
- Remove the old framework.
- Add this new one to the Embedded Binaries section just like we did before.
- Make sure you Clean first from Product, then run.
- Try on both simulator and a device and check.
Simulator works!
iOS Device… well, it depends.
Architectures
Remember when we talked about x86_64
for simulators and arm64
for devices? Well arm64
isn’t the only architecture in iOS devices. This is the architecture of iOS devices that have 64bit chips to support older devices like the iPhone 5. You need to support armv7
architecture, but if your deployment target is below iOS 11, it will not include it in the produced architectures.
So if you need to, do this:
Open your binary framework project HelloDemoApp.Xcodeproj, then navigate to the project menu.
Choose HelloLoggingFramework from Targets.
Open the Build Settings tab and press All.
Under Deployment, change the iOS Deployment Target to a version below iOS 11.
Amazing!?
Now you have a fully production-ready framework. That’s your canvas. Add the functionality that you and (hopefully) others want, and share it with them.
Recapping What You Just Did
- You exported your binary framework from your development project.
- You built a test app.
- You integrated your binary framework in the test app.
- You created an aggregated target.
- You used LIPO to make your binary framework support both simulators and devices.
One More Thing
If you upload your app to the Apple App Store, you will get an e-mail with a warning:
"Too many symbol files" when submitting to App Store.
That’s happening because your framework now has all the architectures (armv7
, arm64
) and your user app could just be supporting arm64
. You will need to strip the extra architectures when uploading to iTunes.
If you published your framework with CocoaPods, CocoaPods will take care of the above for you. If not, you will have to add one extra step: adding a Run Script Phase to Build Phases, like what we did with the aggregated framework before, with the following script:
################################################################################ # # Copyright 2015 Realm Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ################################################################################ # This script strips all non-valid architectures from dynamic libraries in # the application's `Frameworks` directory. # # The following environment variables are required: # # BUILT_PRODUCTS_DIR # FRAMEWORKS_FOLDER_PATH # VALID_ARCHS # EXPANDED_CODE_SIGN_IDENTITY # Signs a framework with the provided identity code_sign() { # Use the current code_sign_identitiy echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements $1" /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements "$1" } echo "Stripping frameworks" cd "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}" for file in $(find . -type f -perm +111); do # Skip non-dynamic libraries if ! [[ "$(file "$file")" == *"dynamically linked shared library"* ]]; then continue fi # Get architectures for current file archs="$(lipo -info "${file}" | rev | cut -d ':' -f1 | rev)" stripped="" for arch in $archs; do if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then # Strip non-valid architectures in-place lipo -remove "$arch" -output "$file" "$file" || exit 1 stripped="$stripped $arch" fi done if [[ "$stripped" != "" ]]; then echo "Stripped $file of architectures:$stripped" if [ "${CODE_SIGNING_REQUIRED}" == "YES" ]; then code_sign "${file}" fi fi done
Summary
I hope you came eager to distribute your awesome idea that contains IP in nice packaging: iOS binary frameworks. Now you have the knowledge to go ahead and expand your development horizons. There’s more than you can create on the iOS platform than just apps! Good luck.
Learn more:
Instabug empowers mobile teams to maintain industry-leading quality apps with comprehensive bug and crash reports and actionable performance monitoring.