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.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 5 of this article. Click here to view the first page.

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:

  1. 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.
  2. 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:

ios_framework_directory_structure

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.

ios_framework_framework_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.

ios_framework_new_run_script_build_phase

Rename the script by double clicking on the panel title Run Script and replace it with Build Framework.

ios_framework_rename_script

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.

ios_framework_build_target_static_lib

Right click on the libRWUIControls.a static library in the Products group of the RWUIControls project, and once again select Show In Finder.

ios_framework_static_lib_view_in_finder

Within this build directory you can access the RWUIControls.framework, and confirm the correct directory structure is present and populated:

ios_framework_created_framework_directory_structure

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.

ios_framework_add_target_button

Navigate to iOS/Other/Aggregate, click Next and name the target Framework.

ios_framework_select_aggregate_target

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.

ios_framework_add_dependency_to_framework_target

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.

ios_framework_framework_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 that ONLY_ACTIVE_ARCH is set to ensure all architectures build for the current SDK.
  • make_fat_library uses lipo 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:

  1. SDK_NAME will be of the form iphoneos7.0 or iphonesimulator6.1. This regex extracts the non-numeric characters at the beginning of this string. Hence, it results in iphoneos or iphonesimulator.
  2. This regex pulls the numeric version number from the end of the SDK_NAME variable, 7.0 or 6.1 etc.
  3. Here a simple string comparison switches iphonesimulator for iphoneos and vice versa.
  4. 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 the iphonesimulator 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.

ios_framework_select_framework_aggregate_scheme

This will build and place a RWUIControls.framework on your desktop.

ios_framework_built_framework_on_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.

ios_framework_architectures_in_fat_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.

Contributors

Over 300 content creators. Join our team.