Thursday, October 13, 2011

Using Kiwi with UI Components on iOS

Trying to get unit testing with Kiwi while being able to use UIKit components was beginning to get frustrating.  The issue is that the tests would crash whenever trying to initialize any UIView components such as UILabel, UITextField, etc.  Well, to some the answer may be obvious, but to relative newbies to iOS like myself it was not.  In order to create instances of UI components you need an actual application running(duh?).  When setting up a unit test bundle in Xcode you can either set it up with or without an app that hosts the tests.  Logic tests are done in test bundles that don't have a host app and application tests are done in test bundles that have a host app.  So for those struggling to create tests that rely on UI components, here's how you do it.

Thanks to Allen Ding the creator of Kiwi for his help and pointing me in the right direction.  Happy testing!

Tuesday, October 11, 2011

Creating a Universal Framework from a 3rd Party Framework Project

One of the things I find irritating about development for iOS is when projects are included directly inside of another project or workspace.  Or even the case where they only provide a static library that you must then separately copy the headers into one of your project folders.  Framework packages seem much cleaner.  So I decided to modify the UISpec base project and create targets to output a universal framework that can be included in any other project.  One drawback of this approach though is that whenever the UISpec projects are updated we either have to figure out which files changed and update our own custom UISpec project or re-create the targets to create the universal framework.  In any case, here is how you create a universal framework from the UISpec project in Xcode 4.2 built for iOS 5.

Edit: The content below was modified to be more generic and calls out UISpec specific steps where necessary.

Cleanup Source Control Files (Optional)
- (optional) Remove all svn or git directories by running 'find . -name ".svn" -exec rm -rf {} \;' or 'find . -name ".git*" -exec rm -rf {} \;' from the base project directory.

Create New Bundle Target for Framework
- Open the framework project.
- (UISpec specific) Modify specs/main.m.  Remove "@implementation main" and "@end" from the file.
- Rename the static library target to "<Framework Name> Static Library".  ex. rename "UISpec" target to "UISpec Static Library".  This is to allow creation of a bundle target with the name of the framework so that the framework is built as <Framework Name>.framework.
- (UISpec specific) Go to the project level "Build Settings" and change the compiler to "LLVM GCC 4.2".
- Add a new target of type "Bundle" named <Framework Name>.  ex. "UISpec" This will be the framework target.
- Go to the "Build Settings" for the newly created target.
- Change the Base SDK to "iOS 5.0" or whichever version is appropriate for your framework.
- Change "Build Active Architecture Only" to "No".
- Change "Architectures" to "$(ARCHS_STANDARD_32_BIT)".
- Change "Valid Architectures" to "$(ARCHS_STANDARD_32_BIT)".
- (UISpec specific) Change the "Compiler for C/C++/Objective-C" to "LLVM GCC 4.2"
- Change "Dead Code Stripping" to "No".
- Change "Link With Standard Libraries" to "No".
- Change "Mach-O Type" to "Relocatable Object File".
- Change "Wrapper Extension" to "framework".
- Change "Generate Debugger Symbols" to "No".  (project may not always contain this option)
- Go to the "Build Phases" tab.
- Click "Add Phase" and then select "Add Copy Headers".
- In the "Copy Headers" section add the headers from the project as public/private/project headers as appropriate.  Click the "+" button and search for "*.h".  Select all(or appropriate subset because there may be example or other unnecessary files that don't need to be added) then click "Add".  Move the headers up to the appropriate section.
- In the "Compile Sources" section add the source files.  Click the "+" button and search for "*.m".  Select all(or appropriate subset because there may be some example files in the framework project that are not needed) then click "Add".
- Under the "Link Binary With Libraries" section and "Copy Bundle Resources" section, remove all entries.
- Add any necessary imports required by the framework classes to the <Framework Name>-Prefix.pch file.
- (UISpec specific)Under UISpec/SupportingFiles/UISpec-Prefix.pch remove the Cocoa.h entry and add the following entries:
    #import <Foundation/Foundation.h>
    #import <UIKit/UIKit.h>

Create Target for Universal Framework
- Create an "Aggregate" target named "<Framework Name> Universal Framework".
- Update the "Build Settings" by changing "SDKROOT" to "iphoneos".
- Click "Add Build Phase" and then "Add Run Script".  Copy the script below into the script area in "Run Script".  Be sure to update the "FMK_NAME" property to the name of your framework.

# Sets the target folders and the final framework product.

# Install dir will be the final output to the framework.
# The following line create it in the root folder of the current project.

# Working dir will be deleted after the framework creation.

# Building both architectures.
xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphoneos
xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphonesimulator

# Cleaning the oldest.
if [ -d "${INSTALL_DIR}" ]
rm -rf "${INSTALL_DIR}"

# Creates and renews the final product folder.
mkdir -p "${INSTALL_DIR}"
mkdir -p "${INSTALL_DIR}/Versions"
mkdir -p "${INSTALL_DIR}/Versions/${FMK_VERSION}"
mkdir -p "${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources"
mkdir -p "${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers"

# Creates the internal links.
# It MUST uses relative path, otherwise will not work when the folder is copied/moved.
ln -s "${FMK_VERSION}" "${INSTALL_DIR}/Versions/Current"
ln -s "Versions/Current/Headers" "${INSTALL_DIR}/Headers"
ln -s "Versions/Current/Resources" "${INSTALL_DIR}/Resources"
ln -s "Versions/Current/${FMK_NAME}" "${INSTALL_DIR}/${FMK_NAME}"

# Copies the headers and resources files to the final product folder.
cp -R "${DEVICE_DIR}/Headers/" "${INSTALL_DIR}/Versions/${FMK_VERSION}/Headers/"
cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/"

# Removes the binary and header from the resources folder.
rm -r "${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/Headers" "${INSTALL_DIR}/Versions/${FMK_VERSION}/Resources/${FMK_NAME}"

# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal final product.
lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/Versions/${FMK_VERSION}/${FMK_NAME}"

rm -r "${WRK_DIR}"

Xcode Scheme cleanup
Since you had renamed the <Framework Name> target and created a new one, you'll probably see <Framework Name> and <Framework Name> 2 as schemes in your project.  Manage your schemes and delete those two schemes.  Then click the "Autocreate" button.  The schemes should now have the updated names.

That's it!  You should be able to build the framework using the new "<Framework Name> Universal Framework" target.  The framework output should be in the base project folder under a directory named "Products".