Skip to content

Latest commit

 

History

History
933 lines (687 loc) · 40.5 KB

BuildAFramework.md

File metadata and controls

933 lines (687 loc) · 40.5 KB

How to create a framework for iOS

In the last tutorial I wrote, you learnt how to create a reusable knob control, but it wasn't very clear exactly how you should reuse it. The simplest technique is to provide the source code files for developers to drop into their Xcode projects, but this isn't particularly user-friendly. You could compile your code into a static library and pass this to the other developers - this can be dropped straight into a project, however it requires that you distribute a collection of header files as well. A framework offers a solution to this problem, as it packages up a static library with header files, in one easy-to-use package.

In this tutorial you'll start right at the beginning - building a static library project in Xcode. From this you'll learn how to build an dev app which has a dependency on this library project, before discovering how to convert the static library project into a fully-fledged framework.

Getting Started

The main purpose of this tutorial is to explain how to create a framework which can be used in iOS, and therefore there will only be a small amount of objective-C code described in the tutorial. In order to get started you should download the source files for the RWKnobControl available in the zip file here. You'll see how to use them as you go through the process of creating the first project in the section Creating a Static Library Project.

What is a Framework?

A framework collects a static library and the header files associated with it together in a well-defined structure in such a way that Xcode understands how to find the constituent parts. On OSX it is possible to create a dynamically linked framework - i.e. one which is shared between multiple applications and is linked at runtime. On iOS dynamically linked frameworks are not allowed - other than the system ones provided by Apple. There are a few reasons behind this - principally that apps on iOS are not allowed to share code - i.e. exactly the purpose of dynamically linked frameworks. However, this doesn't mean that frameworks are completely off the table - statically linked frameworks can be of great use - collecting the static library with the the header files required.

Since this is actually what a framework is comprised of, you're first going to learn how to create and use a static library, so that when the tutorial moves on to building a framework then it doesn't come across as smoke and mirrors magic.

Creating a Static Library project

Open Xcode and create a new static library project by clicking File > New > Project and selecting iOS > Framework & Library > Cocoa Touch Static Library.

Creating a static library

Name the product RWUIControls and save the project in an empty directory.

Naming the static library

A static library project is made up of header files and .m files, which are compiled to make the library itself.

To make life easier for developers using your library (and later framework) you're going to make it so that the only need to import one header file in order to access all the classes you wish to be available to them. When creating the static library, Xcode created RWUIControls.h and RWUIControls.m - you don't need the implementation file, so right click on RWUIControls.m and select delete. You don't need the file, so move it to the trash when asked. Open up RWUIControls.h and replace the content with the following:

#import <UIKit/UIKit.h>

This represents an import which the entire library needs, and as you create the different component classes you'll add them to this file, ensuring that they become accessible for users.

Because the project you're building today relies on UIKit, and the static library project doesn't link against it by default, add this as a dependency. Select the project in the navigator, and then choose the RWUIControls target in then central pane. Click on Build Phases and then expand the Link Binary with Libraries section. Click the + to add a new framework and navigate to find UIKit before clicking add.

Add UIKit Dependency

A static library is of no use unless it is combined with a selection of header files which can be used by the compiler as a manifest of what classes (and methods on those classes) exist within the binary. Some of the classes you create in your library will be publicly accessible, but some will be for internal use only. Next you need to add an operation in the build which will collect together the public header files and put them somewhere accessible. Later, these will be copied into the framework.

Whilst still looking at the same Build Phases screen in Xcode as before, find Editor > Add Build Phase > Add Copy Headers Build Phase.

Note: If this option is grayed out in the menu, then try clicking in the white area below the existing build phases to get the correct focus.

Add copy headers build phase

You can now add the RWUIControls.h file to the public section of this new build phase by dragging it from the navigator to the public part of the panel. This will ensure that this header file is available for users of your library.

Add header to public

Note: All the header files that are included in any of your public headers must also be made public. Otherwise you'll get compiler errors whilst attempting to use the library.

Creating a UI Control

Now that you've got your project set up, it's time to start adding some functionality to your library - otherwise why would anybody want to use it? Since the point of this tutorial is to describe how to build a framework, not how to build a UI control, we'll borrow the code from the last tutorial. In the zip file you downloaded there is a directory RWKnobControl. Drag it from the finder into the RWUIControls group in Xcode.

Drag to import RWKnobControl

Choose to Copy items into destination group's folder and ensure that the new files are being added to the RWUIControls static library target.

Import settings for RWKnobControl

This will add the .m files to the compilation list and, by default, the .h files to the Project group. This means that they will not be shared - i.e. private.

Default target membership

Note: The 3 target names are somewhat confusing: public is shared as expected, but private is not quite the same as project. Private headers can be shared as private headers - not exactly what you want. Therefore, your headers should either be in the public group (shared) or the project group (not-shared).

You want to share the main control header - RWKnobControl.h, and there are several ways you can do this. The first is to simply drag the file from the project group to the public group in the Copy Headers panel.

Drag header to public

Alternatively, you might find it easier to change the membership in the Target Membership panel when editing the file. This is likely to be more convenient as you continue to add and develop the library.

Selecting target membership

Note: as you continue to add new classes to your library, don't forget to keep the membership up-to-date. Make as few headers public as possible, and ensure that the remainder are in the project group.

The other thing that you need to do with your control's header file is add it to the library's main header file - RWUIControls.h. This means that a developer using your library just needs to include one file, and doesn't have to work out exactly which pieces she needs:

#import <RWUIControls/RWUIControls.h>

Therefore, add the following to RWUIControls.h:

// Knob Control
#import <RWUIControls/RWKnobControl.h>

Configuring Build Settings

You are now very close to being able to build this project and create your first ever static library, however, there are a few settings that you need to configure in order to make the library as user-friendly as possible.

Firstly you need to provide a directory name for the public headers to be copied to. This will ensure that you can locate the relevant headers when you come to use the static library.

Click on the project in the Project Navigator, and then select the RWUIControls static library target. Select the Build Settings tab and then search for "public headers". Double click on the Public Headers Folder Path and enter the following:

include/$(PROJECT_NAME)

Set the public header location

You'll see this directory later on.

The other settings you need to change are related to what gets left in the binary library. The compiler gives you the option of removing code which is never accessed (dead code) and also to remove debug symbols (i.e. function names and other details used in debugging). Since you're creating a framework for others to use you can disable both of these kinds of stripping, and let the user choose when they build their dependent app. To do this, use the same search field again, this time to update the following settings:

  • "Dead Code Stripping". Set this to NO
  • "Strip Debug Symbols During Copy". Set this to NO for all configs
  • "Strip Style". Set this to Non-Global Symbols

It has been a while coming, but you can now build the project. Unfortunately there isn't a lot to see yet, but at least you can confirm it builds.

To build, select the target as iOS Device and press ⌘ + B to perform the build. Once this has completed then the libRWUIControls.a product in the Products group of the Project Navigator will turn from red to black, signaling that it now exists. Right click on libRWUIControls.a and select Show in Finder.

Successful first build

In this directory you can see the static library itself (libRWUIControls.a) and the directory you specified for the public headers - include/RWUIControls. Notice that the headers you made public exist in the folder as you would expect them to.

Creating a dependent development project

Developing a UI Controls library for iOS would be extremely difficult if you couldn't actually see what you were doing - which seems to be the case at the moment. Therefore, in this section you're going to create a new Xcode project, which will have a dependency on the library project, allowing you to develop the framework with a dev app. Crucially, the code for the dev app will be completely separate from the library itself, which makes for a lot cleaner structure.

Begin by closing the static library project by clicking File > Close Project. Then create a new project with File > New > Project. Select iOS > Application > Single View Application, call the new project UIControlDevApp and specify that it should be iPhone only. Save the project in the same directory as you chose for RWUIControls.

To add a dependency on the RWUIControls library, drag RWUIControls.xcodeproj from the Finder into the UIControlDevApp group in Xcode.

Import Library into Dev App

You can now navigate around the library project, inside the dev app project. This is perfect because it means that you can edit code inside the library and run up the dev app to check the changes.

Note: You can't have the same project open in 2 different Xcode windows - therefore, if you're unable to navigate the library project check that you don't have it open elsewhere.

Rather than recreate the dev app from the last tutorial, you can copy the same code in. First of all, select Main.storyboard, RWViewController.h and RWViewController.m and delete them by right clicking and selecting Delete. Then copy the the DevApp folder from the zip you downloaded right into the UIControlDevApp group in Xcode.

Adding files to dev app

To add a build dependency for the dev app on the static library, select the UIControlDevApp project in the Project Navigator, and navigate to the Build Phases tab of the UIControlDevApp target. In the Project Navigator, navigate to the Products group of the RWUIControls project, and then drag libRWUIControls.a from the Project Navigator into the Link Binary With Libraries panel.

Adding library dependency

Now you can finally build and run an app and see it in action. If you followed the previous tutorial on building a knob control, then you'll recognize the simple app that you've built.

First Build and run of dev app

The beauty of using these nested projects like this is that you can continue to work on the library itself, without leaving the dev app project - whilst maintaining the code in different places. Each time you build the project you're also checking that you've got the public/project header membership set correctly, since the dev app will be unable to build if it is missing required headers.

Building a Framework

You would be excused for thinking that when you started this tutorial you were promised a framework, and so far you've done a lot of work and there is no framework in sight. Well, this section will change that. The reason that you've got this far without doing anything towards creating a framework is that a framework is pretty much a static library and a collection of headers - exactly what you've built so far. There are a couple of things which make a framework special:

  1. The directory structure. Frameworks have a special directory structure which is recognized by Xcode. You will create a build task which will create this structure.
  2. The 'slices' in the static library. Currently, when the library is built, it is only built 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 it needs to run on. You will create a new build 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 bear with it - it's not nearly as complicated as it looks.

Framework Structure

As mentioned previously, a framework has a special directory structure which looks like this:

Framework directory structure

To create this, you need to add a script which will create this as part of 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.

Create new run script build phase

This creates a new panel in the build phases, which allows you to run an arbitrary script at some point during the build. You can change when the script runs by dragging the panel around in the list; for the framework project you want the script to be run last, so you can leave it where it is placed by default.

Blank run script phase

Rename the script by double clicking on the panel title ("Run Script") and replacing it with "Build Framework".

Rename script

Paste the following script in the script field:

set -e

export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework"

# Create the path to the real Headers dir
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 3 symlinks required for a framework:

  • Versions/Current => A
  • Headers => Versions/Current/Headers
  • RWUIControls => Versions/Current/RWUIControls

Finally, the public header files are copied into the Versions/A/Headers directory from the public headers path you specified before. The -a argument ensures that the modified times are not changed as part of the copy, thereby preventing unnecessary rebuilds.

Select the RWUIControls static library scheme, and iOS Device build target and build using ⌘ + B.

Build static lib target

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

Static lib: show in finder

Within this build directory you can see the RWUIControls.framework, and you can confirm that the correct directory structure has been created and populated:

Created framework structure

This is step along the path to completing a framework, but you'll notice that there there isn't a static lib in there yet - that's what you're going to sort next.

Multi-architecture Build

iOS apps need to run on a lot of different architectures:

  • arm7 Used in the oldest iOS7-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

Each architecture requires a different binary, and when you build an app Xcode will build the correct architecture for whatever you're currently doing - i.e. if you've asked 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) in preparation for upload to the app store, then Xcode will build for all 3 ARM architectures, allowing the app to be run on all possible devices.

When you build your framework, you want developers using it to be able to use it for all the possible architectures, and therefore you need to make Xcode build for all 5 architectures. This process creates a so-called 'fat' binary, which contains a slice for each of the architectures.

Note: This actually highlights another reason for using a dev app which has a dependency on the static library: the library will only be built for the architecture currently required for the dev app, and will actually only be rebuilt if something has changed. This 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 at the bottom of the existing targets.

Add Target Button

Navigate to iOS > Other > Aggregate, click next and name the target Framework.

Aggregate target

To ensure that when this new framework target is built, the static library is built as well, then 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.

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.

Add build script to framework

Change the name of the script as before by double clicking on Run Script - call it MultiPlatform Build.

Paste the following 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 makes sure that you don't end up with a partially-built framework.
  • Next, the RW_MULTIPLATFORM_BUILD_IN_PROGRESS variable is used to determine whether the script has been 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 that will be used is libRWUIControls.a.

The next part of the script sets up some functions which will be used later on. Paste the following at the 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 (e.g. iphoneos7.0) and will build the static lib. Most of the arguments are passed directly from the current build job, the difference being that ONLY_ACTIVE_ARCH is set to ensure that all architectures are 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.

The next section of the script determines some more variables - in order to use the two methods. You need to know what the other SDK is (e.g. iphoneos7.0 should go to iphonesimulator7.0 and vice versa), and to locate the build directory for that SDK. Paste the following at the end of the script:

# 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

# 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

# Determine the other platform
if [ "$RW_SDK_PLATFORM" == "iphoneos" ]; then
  RW_OTHER_PLATFORM=iphonesimulator
else
  RW_OTHER_PLATFORM=iphoneos
fi

# 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 - using string comparison (and regex) 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 (for example). This regex extracts the non-numeric characters at the beginning of this string, and hence will result in iphoneos or iphonesimulator.
  2. This regex pulls the numeric version number from the end of the SDK_NAME variable - i.e. 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 that the build directory for the other platform can be found. This will be used when joining the two static libraries.

Now you can trigger the build for the other platform, and then join the resultant static libraries. Paste the following on 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 that is currently required (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 are built.
  • 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. Paste the following at the end of the script:

# Ensure that the framework is present in both platorm'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 in both platform build directories is the same, simply to reduce the potential confusion.
  • 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 have the framework placed somewhere easily accessible.

Select the Framework aggregate scheme, and press ⌘ + B to build the framework.

Aggregate framework scheme

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

Built framework on desktop

In order to check that the multi-platform build has 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 then the second line uses the lipo command to get info on the RWUIControls static library. This will list the slices present in the library.

Architectures in fat library

You can see here that there are 5 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 then you would have seen a subset of these slices.

How to use a framework

You've put in a fair amount of work to build this framework, and you are yet to see what the point of it was. Well, that's what you'll discover in this section. One of the primary advantages in using a framework is its simplicity in use. You're going to create a simple iOS app which uses the RWUIControls.framework that you've just built.

Start by creating a new project in Xcode. Click File > New > Project and select iOS > Application > Single View Application. Call your new app ImageViewer, set it for iPhone only and save it in the same directory you've used for the previous two projects. This app will display an image and allow the user to change its rotation using a RWKnobControl.

A sample image has been provided in the ImageViewer directory of the zip file you downloaded. Drag sampleImage.jpg from the finder into the ImageViewer group inside Xcode.

Drag sample image into Xcode

Ensure that the Copy items into destination group's folder check box is checked, and click Finish to complete the import.

Importing a framework follows almost exactly the same process. Drag RWUIControls.framework from the desktop into the Frameworks group in Xcode. Again, ensure that Copy items into destination group's folder is checked.

Import Framework

Open up RWViewController.m and replace the code with the following:

#import "RWViewController.h"
#import <RWUIControls/RWUIControls.h>

@interface RWViewController ()
    @property (nonatomic, strong) UIImageView *imageView;
    @property (nonatomic, strong) RWKnobControl *rotationKnob;
@end

@implementation RWViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Create UIImageView
    CGRect frame = self.view.bounds;
    frame.size.height *= 2/3.0;
    self.imageView = [[UIImageView alloc] initWithFrame:CGRectInset(frame, 0, 20)];
    self.imageView.image = [UIImage imageNamed:@"sampleImage.jpg"];
    self.imageView.contentMode = UIViewContentModeScaleAspectFit;
    [self.view addSubview:self.imageView];
    
    // Create RWKnobControl
    frame.origin.y += frame.size.height;
    frame.size.height /= 2;
    frame.size.width  = frame.size.height;
    self.rotationKnob = [[RWKnobControl alloc] initWithFrame:CGRectInset(frame, 10, 10)];
    CGPoint center = self.rotationKnob.center;
    center.x = CGRectGetMidX(self.view.bounds);
    self.rotationKnob.center = center;
    [self.view addSubview:self.rotationKnob];
    
    // Set up config on RWKnobControl
    self.rotationKnob.minimumValue = -M_PI_4;
    self.rotationKnob.maximumValue = M_PI_4;
    [self.rotationKnob addTarget:self
                          action:@selector(rotationAngleChanged:)
                forControlEvents:UIControlEventValueChanged];
}
    
- (void)rotationAngleChanged:(id)sender
{
    self.imageView.transform = CGAffineTransformMakeRotation(self.rotationKnob.value);
}
    
- (NSUInteger)supportedInterfaceOrientations
{
    return UIInterfaceOrientationMaskPortrait;
}

@end

This is a simple view controller which does the following:

  • Import the framework header with #import <RWUIControls/RWUIControls.h>.
  • Set up a couple of private properties to hold the UIImageView and the RWKnobControl.
  • Create a UIImageView, and use the sampleImage that you added to the project before.
  • Create a RWKnobControl and position it appropriately.
  • Set some properties on the knob control, including setting the change event handler to be the rotationAngleChanged: method.
  • The rotationAngleChanged: method simply updates the transform property of the UIImageView so that the image rotates as the knob control is moved.

For further details on how to use the RWKnobControl check out the previous tutorial, which explains how to create it.

If you build and run this project you'll see a simple app, which as you change the value of the knob control will rotate the image.

Image Viewer Rotating

Using a bundle for resources

You may have noticed that the RWUIControls framework that you've built so far only consists of code and headers - i.e. you haven't used any other assets such as imagery. This is a limitation of a framework, in that it can only contain header files and a static library. In this section you'll learn how to use a bundle to collect assets, which can then be distributed alongside the framework itself.

You're going to create a new UI control to be part of the RWUIControls library - a ribbon control. This will put an image of a ribbon on the top right hand corner of a UIView.

Creating a bundle

The resources will be stored inside a .bundle object, which takes the form of an additional target on the RWUIControls project. Open the UIControlDevApp project, and select the RWUIControls project within there. Click the Add Target button, and then navigate to OS X > Framework & Library > Bundle. Call the bundle RWUIControlsResources and make sure to select Core Foundation from the framework selection box.

Add bundle target

There are a couple of build settings which need configuring since you're building a bundle for use in iOS as opposed to the default OSX. Select the RWUIControlsResources target and then the Build Settings tab. Search for base sdk, select the Base SDK line and press delete. This will switch from OSX to iOS.

Remove the Base SDK override

You also need to change the product name to RWUIControls. Search this time for bundle name and double click to edit. Replace ${TARGET_NAME} with RWUIControls.

Set bundle product name

By default images which have 2 resolutions (i.e. include an @2x version) will be combined into a multi-resolution TIFF, which is not what you want. Search for hidpi and change the COMBINE_HIDPI_IMAGES setting from YES to NO.

Turn off HIDPI images merging

In order that the bundle gets built whenever the framework gets built, then add it as a dependency to the Framework aggregate target. Select the Framework target, and then the Build Phases tab. Expand the Target Dependencies panel, click the +, and then select the RWUIControlsResources target to add it as a dependency.

Add bundle as framework dependency

Open the MultiPlatform Build panel, and add the following to the end of the script:

# Copy the resources bundle to the user's desktop
ditto "${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.bundle" \
      "${HOME}/Desktop/${RW_FRAMEWORK_NAME}.bundle"

This command will copy the built bundle to the user's desktop in the same way that the framework gets copied. If you build the framework scheme now then you'll see that the bundle appears on the desktop.

Bundle on desktop

Importing the bundle into the dependent project

In order to develop against this new bundle, you need to be able to use the bundle in the dev app. This means you must add it as both a dependency, and a object to copy across to the app.

Select the UIControlDevApp project in the Project Navigator, and then click on the UIControlDevApp target. Expand the Products group of the RWUIControls project and drag RWUIControls.bundle to the Copy Bundle Resources panel inside the Build Phases tab. In the Target Dependencies panel, click the + to add a new dependency, and then select RWUIControlsResources.

Add bundle for dev project

Building a ribbon view

That's all the setup required - now you can go ahead and build a new control. Again, the code and imagery has been provided since that isn't the primary purpose of this tutorial. Drag the RWRibbon directory from inside the zip file into the RWUIControls group within the RWUIControls project.

Drag RWRibbon into project

Choose to Copy the items into the destination group's folder, and select that they are part of the RWUIControls static lib target.

RWRibbon membership

The only important part of the source code you've added is how you reference images. If you take a look at the addRibbonView method inside the RWRibbonView.m file, then you'll see the relevant line:

UIImage *image = [UIImage imageNamed:@"RWUIControls.bundle/RWRibbon"];

The important part is that the bundle behaves like a directory - so it's really simple to reference an image inside a bundle.

To add the images to bundle, choose them in turn, and then, in the right hand panel, select that they should belong to the RWUIControlsResources target.

RWRibbon Image membership

The RWRibbon.h header file needs to be exported as public, so select the file, and then choose Public from the drop down menu in the Target Membership panel.

RWRibbon Header membership

Finally, since you've added a new control, add the header to the framework's header file. Open RWUIControls.h and add the following lines:

// RWRibbon
#import <RWUIControls/RWRibbonView.h>

Adding the ribbon to the dev app

Open RWViewController.m in the UIControlDevApp project, and add the following ivar between the curly braces in the @interface section:

RWRibbonView  *_ribbonView;

And to actually create a ribbon view, add the following at the end of viewDidLoad:

// Creates a sample ribbon view
_ribbonView = [[RWRibbonView alloc] initWithFrame:self.ribbonViewContainer.bounds];
[self.ribbonViewContainer addSubview:_ribbonView];
// Need to check that it actually works :)
UIView *sampleView = [[UIView alloc] initWithFrame:_ribbonView.bounds];
sampleView.backgroundColor = [UIColor lightGrayColor];
[_ribbonView addSubview:sampleView];

Build and run the the UIControlDevApp scheme and you'll see the new ribbon control at the bottom of the app:

Dev app with ribbon

Using the bundle in ImageViewer

All that remains in this pretty extensive tutorial is to take a look at how to use this new bundle inside another app - the ImageViewer app you built before.

To start, make sure that your framework and bundle are up to date. Select the Framework scheme and then press ⌘ + B to build it.

Open up the ImageViewer project, find the RWUIControls.framework item inside the Frameworks group and delete it, choosing to move it to the trash. Then, as before, drag the RWUIControls.framework from your desktop to the Frameworks group. This is necessary because the framework has changed since you first imported it.

Delete framework

To import the bundle, simply drag it from the desktop to the ImageViewer group. Choose to Copy items into destination group's folder and ensure that it is added to the ImageViewer target.

Import bundle

You're going to add the ribbon to the image which gets rotated, so there are a few simple changes to make to the code in RWViewController.m.

Change the type of the imageContainer property from UIImageView to RWRibbonView:

@interface RWViewController ()
    @property (nonatomic, strong) RWRibbonView *imageView;
    @property (nonatomic, strong) RWKnobControl *rotationKnob;
@end

Replace the first part of the viewDidLoad method, which before created the UIImageView, to the following:

[super viewDidLoad];
// Create UIImageView
CGRect frame = self.view.bounds;
frame.size.height *= 2/3.0;
self.imageView = [[RWRibbonView alloc] initWithFrame:CGRectInset(frame, 0, 20)];
UIImageView *iv = [[UIImageView alloc] initWithFrame:self.imageView.bounds];
iv.image = [UIImage imageNamed:@"sampleImage.jpg"];
iv.contentMode = UIViewContentModeScaleAspectFit;
[self.imageView addSubview:iv];
[self.view addSubview:self.imageView];

This new code still created a UIImageView, but this now exists as a subview of a RWRibbonView.

Build and run the app, and you'll see that you are now using both the RWKnobControl and the RWRibbonView from the RWUIControls framework.

ImageViewer with Ribbon

Where To Go From Here?

In this tutorial you've learned everything you need to know about how to build a framework for use in iOS projects, including the best way to develop frameworks and how to use bundles to share assets. If there is some functionality that you find yourself using in lots of different apps then maybe you should think about creating a library make code reuse really easy? A framework offers an excellent way to collect a library of code together, and will allow you to drop your library in to any projects you wish.