Friday 8 January 2016

How to Create a Framework for iOS 8 on Xcode 6

How to Create a Framework for iOS 8 on Xcode 6

9
The internet is full with documentation about how to build an iOS framework. However, when we began our internal development here at Insert, we still had to overcome some non-trivial challenges before we were able to get our SDK working the way we wanted.
Additionally, in xcode 6, Apple dramatically changed the way developers create and build frameworks, so a lot of the documentation you’ll find on the Internet is not up-to-date.
In this post we’ll show you, step-by-step, how to create and build a framework for iOS 8.
We will also address important challenges such as:
  • How to combine Swift and Objective-C code within the same SDK?
  • How to build the framework for the all the relevant architectures (armv7, armv7s, arm64, i386), etc. If you just need the solution for this one you’ll need to add a new Build Phase to your project and use the “run script” at the bottom of this post.
In our example we will use a Manager to enable\disable the Framework and a CustomView Class which will contain (surprise) a custom UIView. In this example we want to show you how how resources such as xib and png files can be integrated within the framework.
Let’s begin with Step #1
1) Creating a project from scratch
Since xcode 6 there is a built-in option to create a dynamic framework project. This is the option you should choose if you need to create a framework project from scratch:

 What’s the difference between a “Static Library” and a “Framework”?
A “Static Library” is mainly compiled code which takes the form of a (dot).a file, for example InsertLib.a. It’s possible to export static libraries by sharing the a file together with some public header files that contain the public Classes and Methods which the clients of the static lib can use.
“Cocoa Touch Framework” is essentially a bundle which contains a “dynamic library”, H files and resources. The idea of “dynamic library” – or in different words “dynamic linking” – is to have one copy of the shared code in a single device (think of CoreLocation.Framework for example) that is shared by all the apps that link to it. This dynamic concept promises the device will improve system performance by minimizing the framework’s memory usage.
2) Add the Manager Class files with the following code:
InsertManager.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface InsertManager : NSObject

+(instancetype) sharedManager;

-(void) startManager;
-(void) stopManager;

-(void) showMessageInViewController:(UIViewController *)viewController;

-(BOOL) isManagerRunning;

@end
InsertManager.m
#import "InsertManager.h"
#import "CustomView.h"

@interface InsertManager()

@property (nonatomic) BOOL isEnabled;

@end

@implementation InsertManager

+ (instancetype) sharedManager {
   static InsertManager *sharedManager = nil;
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
      sharedManager = [[[self class] alloc] init];
   });
   return sharedManager;
}

- (void) startManager {
   NSLog(@"Manager is running");
   _isEnabled = YES;
}

- (void) stopManager {
   NSLog(@"Manager stopped..");
   _isEnabled = NO;
}

-(BOOL) isManagerRunning {
   return _isEnabled;
}

-(void) showMessageInViewController:(UIViewController *)viewController {
   if (_isEnabled) {
      NSBundle* frameworkBundle = [NSBundle bundleForClass:[self class]];
      CustomView *csView = [[frameworkBundle loadNibNamed:@"CustomView" owner:self options:nil] firstObject];
      csView.frame = CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height);
      [viewController.view addSubview:csView];
   }
}

@end
3) Add the CustomView code:
CustomView.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface CustomView : UIView

@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) IBOutlet UIButton *closeButton;

@end
CustomView.m
#import "CustomView.h"

@implementation CustomView

- (IBAction)closeButtonClicked:(id)sender {
    [self removeFromSuperview];
}

@end
CustomView.xib – Download from Github to see how it is configured.
Newsroom.png – we use this file as a background image to demonstrate how resources such as PNG files can be used in a framework and exported to an app.
4) When you create a new “Cocoa Touch Framework” project on xcode, a default H file will be generated automatically with name “<Framework-name>”.h – Make sure you add to this file all the public H files, i.e. H files that contains public methods to be used by the framework client. In our example add the following:
#import <UIKit/UIKit.h>

//! Project version number for InsertSampleFramework.

FOUNDATION_EXPORT double InsertSampleFrameworkVersionNumber;

//! Project version string for InsertSampleFramework.

FOUNDATION_EXPORT const unsigned char InsertSampleFrameworkVersionString[];

// In this header, you should import all the public headers of your framework using statements like

#import <InsertSampleFramework/InsertManager.h>

5) In xcode click on the Target and go to the “Build Phase” section, add the public H files to “public” in the “Headers” build phase:
6)  Now just build the Framework and you’ll have the product ready. However we’ll be able to use the framework only in the context of an app. We will use in this guide Apple Sample Project called “Tabster”. The complete code of this project can be downloaded from iOS Developer Library  – go to this page and search for “Tabster” and click on it. In Tabster page, look for the button “Download Sample Code”, download the code and open the project in xcode.
Let’s see how we can take our framework to work with an external app…
Integrate the framework in external app for Development
1) Open the Tabster project and run the app “as is”, see that it’s working as expected.
2) Copy the root folder “InsertSampleFramework” into the Root folder of Tabster
3) Now let’s drag our Framework project inside as a dependency (note that you’ll have the close the xcode window of the Framework project first since this xcodeproj can be opened only in one xcode window).


4) Add the Framework as a dependency in the Build Phases

5) Add the framework to “Link Binary with Libraries”. If the framework has Swift code you’ll also need to add the framework in the “General” tab under “Embedded Binaries”


6) Let’s run and see how it works just as a sanity check (notice that we haven’t integrated the framework in code yet).
7) Now let’s use our amazing framework in the Tabster app. It’s a fairly simple app: open the Storyboard and add a label (Insert Framework Enable\Disable), UISegmentControl and a UIButton to ThreeViewController
8) In Tabster go to ThreeViewController.m and add:
#import <InsertSampleFramework/InsertManager.h>
9) Now let’s add the following IBActions to ThreeViewController.m:
#pragma mark - IBAction

- (IBAction)segmentValueChanged:(id)sender {
    UISegmentedControl *sc = (UISegmentedControl *)sender;
    NSInteger selectedSegment = sc.selectedSegmentIndex;
    if (selectedSegment == 1) {
        [[InsertManager sharedManager] startManager];
    }
    else if (selectedSegment == 0) {
        [[InsertManager sharedManager] stopManager];
    }
}


- (IBAction)showCustomView:(id)sender {
    [[InsertManager sharedManager] showMessageInViewController:self];
}
10) Run the app, go to tab “Three” and play with the on\off segment control. Verify that the button “Show Custom View” will work only when the manager is running.

Integrate our framework in external app for Distribution
Most companies and individual developers who develop a Framework for iOS, eventually want to their framework to be used in Distribution mode. The most important step you’ll have to do at this stage is to build the framework for all possible architectures (armv7, armv7s, arm64, x86, etc). We’ll achieve this by adding a “Build Phase” to run a script that will build the framework 3 times – one for each family of architectures (iOS Simulator, older devices (armv7, armv7s), new devices (arm64).
Click on the Framework target and add a “New Run Script Phase”:



Here is the line you should copy & paste to the build phase:


Some developers prefer to write the script directly in this box, I prefer to have the script in a separate .sh file so I can track it on Git and track changes when they are needed in the future.
The actual script is in ios-build-framework-script.sh :
set -e
set +u
# Avoid recursively calling this script.
if [[ $SF_MASTER_SCRIPT_RUNNING ]]
then
exit 0
fi
set -u
export SF_MASTER_SCRIPT_RUNNING=1


# Constants
SF_TARGET_NAME=${PROJECT_NAME}
UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal

# Take build target
if [[ "$SDK_NAME" =~ ([A-Za-z]+) ]]
then
SF_SDK_PLATFORM=${BASH_REMATCH[1]}
else
echo "Could not find platform name from SDK_NAME: $SDK_NAME"
exit 1
fi

if [[ "$SF_SDK_PLATFORM" = "iphoneos" ]]
then
echo "Please choose iPhone simulator as the build target."
exit 1
fi

IPHONE_DEVICE_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphoneos

# Build the other (non-simulator) platform
xcodebuild -project "${PROJECT_FILE_PATH}" -target "${TARGET_NAME}" -configuration "${CONFIGURATION}" -sdk iphoneos BUILD_DIR="${BUILD_DIR}" OBJROOT="${OBJROOT}" BUILD_ROOT="${BUILD_ROOT}" CONFIGURATION_BUILD_DIR="${IPHONE_DEVICE_BUILD_DIR}/arm64" SYMROOT="${SYMROOT}" ARCHS='arm64' VALID_ARCHS='arm64' $ACTION

xcodebuild -project "${PROJECT_FILE_PATH}" -target "${TARGET_NAME}" -configuration "${CONFIGURATION}" -sdk iphoneos BUILD_DIR="${BUILD_DIR}" OBJROOT="${OBJROOT}" BUILD_ROOT="${BUILD_ROOT}"  CONFIGURATION_BUILD_DIR="${IPHONE_DEVICE_BUILD_DIR}/armv7" SYMROOT="${SYMROOT}" ARCHS='armv7 armv7s' VALID_ARCHS='armv7 armv7s' $ACTION

# Copy the framework structure to the universal folder (clean it first)
rm -rf "${UNIVERSAL_OUTPUTFOLDER}"
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework" "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework"

# Smash them together to combine all architectures
lipo -create  "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/arm64/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/armv7/${PROJECT_NAME}.framework/${PROJECT_NAME}" -output "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/${PROJECT_NAME}"
1) Make sure you build for iOS Simulator when you want to build for Distribution – the build script detects that and automatically build for the other platforms.
2) After running “Build” you need to choose the Framework under the Distribution-universal directory.
3) Integrate the Framework into the Xcode project that use it and you are set.

 For more information, please visit : www.programmingyan.com

No comments:

Post a Comment