How to Create a Framework for iOS
Learn how to create a framework for iOS, allowing you to elegantly and efficiently package up and redistribute your code across different apps and teams By Sam Davies.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Create a Framework for iOS
45 mins
- Getting Started
- What is a Framework?
- Creating a Static Library Project
- Creating a UI Control
- Configuring Build Settings
- Creating a Dependent Development Project
- Building a Framework
- Framework Structure
- Multi-Architecture Build
- How to Use a Framework
- Using a Bundle for Resources
- Creating a bundle
- Importing the Bundle
- Building a Ribbon View
- Add the Ribbon to the Example App
- Using the Bundle in ImageViewer
- Where To Go From Here?
Building a Framework
By now, you’re probably impatiently tapping your toes and wondering where the framework comes into play. Understandable, because so far you’ve done a lot of work and yet there is no framework in sight.
Well, things are about to change, and quickly too. The reason that you’ve not created a framework is because it’s pretty much a static library and a collection of headers – exactly what you’ve built so far.
There are a couple of things that make a framework distinct:
- The directory structure. Frameworks have a special directory structure that is recognized by Xcode. You’ll create a build task, which will create this structure for you.
- The Slices. Currently, when you build the library, it’s only for the currently required architecture, i.e. i386, arm7, etc. In order for a framework to be useful, it needs to include builds for all the architectures on which it needs to run. You’ll create a new product which will build the required architectures and place them in the framework.
There is a quite a lot of scripting magic in this section, but we’ll work through it slowly; it’s not nearly as complicated as it appears.
Framework Structure
As mentioned previously, a framework has a special directory structure which looks like this:
Now you’ll add a script to create this during the static library build process. Select the RWUIControls project in the Project Navigator, and select the RWUIControls static library target. Choose the Build Phases tab and add a new script by selecting Editor/Add Build Phase/Add Run Script Build Phase.
This creates a new panel in the build phases section, which allows you to run an arbitrary Bash script at some point during the build. Drag the panel around in the list if you want to change the point at which the script runs in the build process. For the framework project, run the script last, so you can leave it where it’s placed by default.
Rename the script by double clicking on the panel title Run Script and replace it with Build Framework.
Paste the following Bash script into the script field:
set -e
export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework"
# Create the path to the real Headers die
mkdir -p "${FRAMEWORK_LOCN}/Versions/A/Headers"
# Create the required symlinks
/bin/ln -sfh A "${FRAMEWORK_LOCN}/Versions/Current"
/bin/ln -sfh Versions/Current/Headers "${FRAMEWORK_LOCN}/Headers"
/bin/ln -sfh "Versions/Current/${PRODUCT_NAME}" \
"${FRAMEWORK_LOCN}/${PRODUCT_NAME}"
# Copy the public headers into the framework
/bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \
"${FRAMEWORK_LOCN}/Versions/A/Headers"
This script first creates the RWUIControls.framework/Versions/A/Headers directory before then creating the three symbolic links required for a framework:
- Versions/Current => A
- Headers => Versions/Current/Headers
- RWUIControls => Versions/Current/RWUIControls
Finally, the public header files copy into the Versions/A/Headers directory from the public headers path you specified before. The -a argument ensures the modified times don’t change as part of the copy, thereby preventing unnecessary rebuilds.
Now, select the RWUIControls static library scheme and the iOS Device build target, then build using cmd+B.
Right click on the libRWUIControls.a static library in the Products group of the RWUIControls project, and once again select Show In Finder.
Within this build directory you can access the RWUIControls.framework, and confirm the correct directory structure is present and populated:
This is a leap forward on the path of completing your framework, but you’ll notice that there isn’t a static lib in there yet. That’s what you’re going to sort out next.
Multi-Architecture Build
iOS apps need to run on many different architectures:
- arm7: Used in the oldest iOS 7-supporting devices
- arm7s: As used in iPhone 5 and 5C
- arm64: For the 64-bit ARM processor in iPhone 5S
- i386: For the 32-bit simulator
- x86_64: Used in 64-bit simulator
Every architecture requires a different binary, and when you build an app Xcode will build the correct architecture for whatever you’re currently working with. For instance, if you’ve asked it to run in the simulator, then it’ll only build the i386 version (or x86_64 for 64-bit).
This means that builds are as fast as they can be. When you archive an app or build in release mode, then Xcode will build for all three ARM architectures, thus allowing the app to run on most devices. What about the other builds though?
Naturally, when you build your framework, you’ll want developers to be able to use it for all possible architectures, right? Of course you do since that’ll mean you can earn the respect and admiration of your peers.
Therefore you need to make Xcode build for all five architectures. This process creates a so-called fat binary, which contains a slice for each of the architectures. Ah-ha!
Note: This actually highlights another reason to create an example app which has a dependency on the static library: the library only builds for the architecture required by the example app, and will only rebuild if something changes. Why should this excite you? It means the development cycle is as quick as possible.
Note: This actually highlights another reason to create an example app which has a dependency on the static library: the library only builds for the architecture required by the example app, and will only rebuild if something changes. Why should this excite you? It means the development cycle is as quick as possible.
The framework will be created using a new target in the RWUIControls project. To create it, select the RWUIControls project in the Project Navigator and then click the Add Target button shown below the existing targets.
Navigate to iOS/Other/Aggregate, click Next and name the target Framework.
Note: Why use an Aggregate target to build a Framework? Why so indirect? Because Frameworks enjoy better support on OS X, as reflected by the fact that Xcode offers a straightforward Cocoa Framework build target for OS X apps. To work around this, you’ll use the aggregate build target as a hook for bash scripts that build the magic framework directory structure. Are you starting to see the method to the madness here?
Note: Why use an Aggregate target to build a Framework? Why so indirect? Because Frameworks enjoy better support on OS X, as reflected by the fact that Xcode offers a straightforward Cocoa Framework build target for OS X apps. To work around this, you’ll use the aggregate build target as a hook for bash scripts that build the magic framework directory structure. Are you starting to see the method to the madness here?
To ensure the static library builds whenever this new framework target is created, you need to add a dependency on the static library target. Select the Framework target in the library project and add a dependency in the Build Phases tab. Expand the Target Dependencies panel, click the + and choose the RWUIControls static library.
The main build part of this target is the multi-platform building, which you’ll perform using a script. As you did before, create a new Run Script build phase by selecting the Build Phases tab of the Framework target, and clicking Editor/Add Build Phase/Add Run Script Build Phase.
Change the name of the script by double clicking on Run Script. This time name it MultiPlatform Build.
Paste the following Bash script into the script text box:
set -e
# If we're already inside this script then die
if [ -n "$RW_MULTIPLATFORM_BUILD_IN_PROGRESS" ]; then
exit 0
fi
export RW_MULTIPLATFORM_BUILD_IN_PROGRESS=1
RW_FRAMEWORK_NAME=${PROJECT_NAME}
RW_INPUT_STATIC_LIB="lib${PROJECT_NAME}.a"
RW_FRAMEWORK_LOCATION="${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework"
-
set -e
ensures that if any part of the script should fail then the entire script will fail. This helps you avoid a partially-built framework. - Next, the
RW_MULTIPLATFORM_BUILD_IN_PROGRESS
variable determines whether the script was called recursively. If it has, then quit. - Then set up some variables. The framework name will be the same as the project i.e. RWUIControls, and the static lib is libRWUIControls.a.
The next part of the script sets up some functions that the project will use later on. Add the following to the very bottom of the script:
function build_static_library {
# Will rebuild the static library as specified
# build_static_library sdk
xcrun xcodebuild -project "${PROJECT_FILE_PATH}" \
-target "${TARGET_NAME}" \
-configuration "${CONFIGURATION}" \
-sdk "${1}" \
ONLY_ACTIVE_ARCH=NO \
BUILD_DIR="${BUILD_DIR}" \
OBJROOT="${OBJROOT}" \
BUILD_ROOT="${BUILD_ROOT}" \
SYMROOT="${SYMROOT}" $ACTION
}
function make_fat_library {
# Will smash 2 static libs together
# make_fat_library in1 in2 out
xcrun lipo -create "${1}" "${2}" -output "${3}"
}
-
build_static_library
takes an SDK as an argument, for example iphoneos7.0, and will build the static lib. Most of the arguments pass directly from the current build job, the difference being thatONLY_ACTIVE_ARCH
is set to ensure all architectures build for the current SDK. -
make_fat_library
useslipo
to join two static libraries into one. Its arguments are two input libraries followed by the desired output location. Read more about lipo here
The next section of the script determines some more variables which you’ll need in order to use the two methods. You need to know what the other SDK is, for example iphoneos7.0 should go to iphonesimulator7.0 and vice versa, and to locate the build directory for that SDK.
Add the following to the very end of the script:
# 1 - Extract the platform (iphoneos/iphonesimulator) from the SDK name
if [[ "$SDK_NAME" =~ ([A-Za-z]+) ]]; then
RW_SDK_PLATFORM=${BASH_REMATCH[1]}
else
echo "Could not find platform name from SDK_NAME: $SDK_NAME"
exit 1
fi
# 2 - Extract the version from the SDK
if [[ "$SDK_NAME" =~ ([0-9]+.*$) ]]; then
RW_SDK_VERSION=${BASH_REMATCH[1]}
else
echo "Could not find sdk version from SDK_NAME: $SDK_NAME"
exit 1
fi
# 3 - Determine the other platform
if [ "$RW_SDK_PLATFORM" == "iphoneos" ]; then
RW_OTHER_PLATFORM=iphonesimulator
else
RW_OTHER_PLATFORM=iphoneos
fi
# 4 - Find the build directory
if [[ "$BUILT_PRODUCTS_DIR" =~ (.*)$RW_SDK_PLATFORM$ ]]; then
RW_OTHER_BUILT_PRODUCTS_DIR="${BASH_REMATCH[1]}${RW_OTHER_PLATFORM}"
else
echo "Could not find other platform build directory."
exit 1
fi
All four of these statements are very similar, they use string comparison and regular expressions to determine RW_OTHER_PLATFORM
and RW_OTHER_BUILT_PRODUCTS_DIR
.
The four if
statements in more detail:
-
SDK_NAME
will be of the formiphoneos7.0
oriphonesimulator6.1
. This regex extracts the non-numeric characters at the beginning of this string. Hence, it results iniphoneos
oriphonesimulator
. - This regex pulls the numeric version number from the end of the
SDK_NAME
variable,7.0
or6.1
etc. - Here a simple string comparison switches
iphonesimulator
foriphoneos
and vice versa. - Take the platform name from the end of the build products directory path and replace it with the other platform. This ensures the build directory for the other platform can be found. This will be critical when joining the two static libraries.
Now you can trigger the build for the other platform, and then join the resulting static libraries.
Add the following to the end of the script:
# Build the other platform.
build_static_library "${RW_OTHER_PLATFORM}${RW_SDK_VERSION}"
# If we're currently building for iphonesimulator, then need to rebuild
# to ensure that we get both i386 and x86_64
if [ "$RW_SDK_PLATFORM" == "iphonesimulator" ]; then
build_static_library "${SDK_NAME}"
fi
# Join the 2 static libs into 1 and push into the .framework
make_fat_library "${BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \
"${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \
"${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}"
- First there’s a call to build the other platform using the function you defined beforehand
- If you’re currently building for the simulator, then by default Xcode will only build the architecture for that system, e.g. i386 or x86_64. In order to build both architectures, this second call to
build_static_library
rebuilds with theiphonesimulator
SDK, and ensures that both architectures build. - Finally a call to
make_fat_library
joins the static lib in the current build directory with that in the other build directory to make the multi-architecture fat static library. This is placed inside the framework.
The final commands of the script are simple copy commands. Add the following to the end of the script:
# Ensure that the framework is present in both platform's build directories
cp -a "${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}" \
"${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework/Versions/A/${RW_FRAMEWORK_NAME}"
# Copy the framework to the user's desktop
ditto "${RW_FRAMEWORK_LOCATION}" "${HOME}/Desktop/${RW_FRAMEWORK_NAME}.framework"
- The first command ensures that the framework is present in both platform’s build directories.
- The second copies the completed framework to the user’s desktop. This is an optional step, but I find that it’s a lot easier to place the framework somewhere that is easily accessible.
Select the Framework aggregate scheme, and press cmd+B to build the framework.
This will build and place a RWUIControls.framework on your desktop.
In order to check that the multi-platform build worked, fire up a terminal and navigate to the framework on the desktop, as follows:
$ cd ~/Desktop/RWUIControls.framework
$ RWUIControls.framework xcrun lipo -info RWUIControls
The first command navigates into the framework itself, and the second line uses the lipo
command to get the required information on theRWUIControls static library. This will list the slices that are present in the library.
You can see here that there are five slices: i386, x86_64, arm7, arm7s and arm64, which is exactly what you set out to build. Had you run the lipo -info
command beforehand, you would have seen a subset of these slices.