在上一篇教程里,你学到了如何创建一个可复用的按钮控件。然而,对于开发者如何简单方便地来复用的话效果还不是非常明显。
一种共享它的方式就是直接提供源码文件。然而,这不是特别优雅。有可能你不想共享代码的实现细节。此外,开发者可能不想看见所有的东西,因为它们只是想继承一部分代码到自己的代码库里。
另一种方式是把你的代码编译成静态库来让开发者添加到他们的项目中去。然而这要求你来提供公共头文件,这样显得非常的笨拙。
你需要有一种简洁的方式来编译你的代码,并且它还要方便的共享和在多个项目间复用。你需要用什么方式来打包静态库并且它的头文件在一个文件里,然后只需要把这个文件添加到工程里就能立即开始使用。
好消息是这篇教程就是围绕这展开的。通过制作 framework,你会学到它能够帮你解决这些迫在眉睫的问题。OS X 对制作 framework 有着最好的支持,因为 Xcode 提供了一个工程模板,它包含有一个默认的构建目标还可以容纳资源文件,例如图片,声音和字体。你能够为 iOS 创建一个 framework,但这有点棘手,如果你跟着我一步一步来,你将会学到如何解决这些阻碍。
通过本教程的任务,你将:
- 用 Xcode 构建一个基本静态库工程
- 构建一个依赖与这个静态库工程的 app
- 探索如何把静态库转换成一个完整的 framework
- 最后,你将会看到如何把一个图片文件打包到 framework 中的资源包中
这篇教程的主要目的是解释如何在 iOS 项目中创建一个可复用 framework,因此不会像本站中其他教程一样,而只会有少量的 Objective-C 代码,而且这些代码只是用来证明讲到的概念。
在这里可以先下载好 RWKnobControl
资源文件。当你在 创建静态库项目
这个部分里创建第一个工程的过程中,你会看到如何使用它们。
你要创建的所有的这些代码和工程文件在 Github 上都能访问到,并且还有每个构建部分的独立提交。
什么是 Framework?
Framework 是一种资源集合,它把一个静态库和它的头文件汇集成一个单一的结构,这样 Xcode 能够很容易的合并到你的工程中去。
在 OS X 中,能够创建一个动态链接库。通过动态链接,framework 能够显性的实时更新而不需要应用程序重新链接它们。在运行时状态,一个单一的二进制库代码的副本在所有处理过程之间共享利用,因此这能够减少内存用量并提升系统的性能。正如你所看到的,这真是个强大的东西。
在 iOS 中,你不能用这种方式给系统添加自定义的 framework,因此只有 Apple 提供的动态链接库才行。
然而,这不意味着 framework 跟 iOS 就毫无相关了。对于在不同 app 中进行复用,静态链接库仍然是一个便捷的打包代码库的方式。
既然 framework 本质上是对静态库的一站式购物,那这篇教程中首要事情你了解如何创建和使用静态库。当这篇教程进展到构建 framework 时,你会知道接下来会发生什么。
打开 Xcode 并且通过点击 File\New\Project
和 iOS\Framework and Library\Cocoa Touch Static Library
来创建一个新的静态库工程。
把工程命名为 RWUIControls
并且保存工程到一个空目录。
一个静态库工程由头文件和实现文件组成,它们由工程自己创建编译。
为了让开发者更方便的使用你的库和框架,你需要导入一个头文件来访问所有的你希望公开的类,好让他们只需访问这个头文件就行。
当创建静态库工程的时候,Xcode 添加了 RWUIControls.h
和 RWUIControls.m
。你不需要实现文件,因此右键 RWUIControls.m
选择删除,按提示把它移到垃圾箱中。
打开 RWUIControls.h
并且用下面的代码替换文件内容:
#import <UIKit/UIKit.h>
这句代码导入了 UIKit
的伞型头文件,它包含有自身所需要的库。当你创建不同的组件类时,你要把它们添加到这个文件里,这样能够确保它们让这个库的使用者能访问。
你构建这个工程时会依赖 UIKit
,但 Xcode 静态库工程没有默认的链接到 UIkit
。为了修正这个问题,要添加 UIKit
作为一个依赖。选择工程的导航器,并且在主面板选择 RWUIControls
目标。
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.framework, before clicking add.
单击 Build Phases
然后展开 Link Binary With Libraries
部分。单击 +
来添加一个新的框架,查找 UIKit.framework
,单击 add
添加。
把 RWUIControls.h
从导航器拖到面板的 Public
部分。这确保这个头文件对任何使用你库的用户都可用。
注意:这可能有点多此一举,但把包含有你工程所有公开类头文件的头文件放到公有部分非常重要。否则,开发者在企图使用这个库的时候会发生编译错误。这对任何人都不是开玩笑的,当 Xcode 读取公有头的时又不能读取你忘记添加的公有文件。
现在你已经设置好了你的工程,是时候给库添加些功能了。既然这个教程的目的是讲诉如何构建一个 framework,而不是如何构建一个 UI 控件,那你会借用些上篇教程的一些代码。在你之前下载的 zip 文件你会找到 RWKnobControl 目录。把它拖到 Xcode 的 RWUIControls
组别。
选择 Copy items into destination group’s folder
并确保要拷贝的新文件勾选了响应的单选框。
这会同时把实现文件添加到编译列表,默认的头文件在 Project group
。这意味着它们都是私有的。
注意:这三个部分的命名如果不拆分开理解会有点令人误解。Public
如你所预料的。Private
头仍然会暴露你的头文件,这有点让人困惑。Project
头是你工程用到的特定私有文件,这有点讽刺。因此,你会慢慢发现要么头文件是放到 Public
要么放在 Project
部分。
另一种方式,当你编辑文件的时候会发现更改 Target Membership
面板中的值会更方便。当你开发库继续添加文件的时候这会非常方便。
注意:在你往库中添加新的类时,记得保持成员是最新的。尽可能减少公有的头文件,并确保其余的在 Project
组。
用控件的头文件做的另一件事就是把 RWUIControls.h
它添加到库的主头文件中。这样开发者使用你的库时只需要像下面这样包含这一个文件就行,而不是一堆。
#import <RWUIControls/RWUIControls.h>
因此,把下面的代码添加到 RWUIControls.h
// Knob Control
#import <RWUIControls/RWKnobControl.h>
现在你非常接近这个工程的编译部分了。然而,有几个确保库尽可能对用户友好的设置需要配置。
首先,你需要提供一个目录名给你公有头文件将要拷贝到那里去。这确保当你使用静态库的时候能定位到相关的头文件。
单击工程导航栏的工程,然后选择 RWUIControls
静态库目标。选择 Build Setting
标签,然后搜索 public header。双击 Public Header Folder Path
设置并输入下面的路径:
include/$(PROJECT_NAME)
之后你会看到这个目录。
现在你需要改变一些其他的设置,尤其是那些保留在二进制库中的。编译器给了你移除无用代码的选项,指那些从不会访问到的代码。并且你还能移除 debug 符号,例如函数名和其他 debug 时相关的细节。
既然你创建 framework 给其他人使用,那最好把它们都禁用了然后让用户自行选择最适合他们工程的配置。要做这些的话,跟之前一样使用搜索就行,更新下面的设置:
Dead Code Stripping
– 设为 NOStrip Debug Symbols During Copy
– 设为 NO for all configurationsStrip Style – 设为 Non-Global Symbols
构建运行。你仍然什么东西看没看到,但这仍然是件好事,这足以说明工程成功的构建的并且没有警告和错误。
To build, select the target as iOS Device and press cmd+B to perform the build. Once completed, 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.
要构建的话,选择构建目标为 iOS Device
并按下 cmd+B
来执行构建。一旦完成,项目导航器的 Products 组别里的 libRWUIControls.a
会从红色变为黑色,这表示文件已生成。右键 libRWUIControls.a
并且选择 Show in Finder
。
在这个目录中你能看到生成的静态库,libRWUIControls.a
,并且公有头文件单独放在 include/RWUIControls
。
当你不能亲眼看到你在做什么的时候,为 iOS 开发一个 UI 控件库极其的困难,现在似乎就是这样。
没人要你盲目的工作,因此在这个部分你将会创建一个新的 Xcode 工程,它会用到你刚创建的库。这能让你通过一个示例 app 来开发 framework。自然地,这个 app 的代码会完全的与库本身的代码分离开来,这样一来会让结构更清晰。
关闭静态库工程。然后创建一个新的工程。选择 iOS/Application/Single View Application
,并取名为 UIControlDevApp
。设置类前缀为 RW
并指定仅 iPhone 可用。最后保存到 RWUIControls
相同的目录。
把 RWUIControls.xcodeproj
拖到 UIControlDevApp
组别来把 RWUIControls
作为一个依赖项。
注意:你不能在两个不同的窗口中打开同一个工程。如果你发现你不能切换到库工程,请检查你没有在另一个 Xcode 窗口中打开它。
你可以简单的拷贝代码而不是重新创建上一篇教程的 app。首先选择 Main.storyboard
,RWViewController.h
和 RWViewController.m
然后删除它们。接着拷贝 DevApp
文件夹到 UIControlDevApp
组别。
现在添加静态库作为示例 app 的依赖构建:
- 在工程中选择
UIControlDevApp
工程。 - 导航至
UIControlDevApp
目标的Build Phases
标签。 - 打开
Target Dependencies
面板并单击 + 来显示选择器。 - 找到
RWUIControls
静态库,单击Add
来添加。这个动作表示当构建示例 app 的时候,Xcode 会检查是否静态库需要重新构建。
为了链接静态库,展开 Link Binary With Libraries
面板并再次点击 +。选择 libRWUIControls.a
单击添加。
这个行为会让 Xcode 把示例 app 与静态库链接起来,就像链接系统 framework 一样比如 UIKit
。
构建运行。你会看到跟上一篇教程中熟悉的画面。
嵌套工程的好处就是你能够在不离开示例 app 工程的情况下继续开发静态库,正如你在不同的部位维护代码一样。你每次构建项目的时候,你也要同时检查 public/project 头成员是否正确设置。如果丢失了任何必须的头文件那么示例 app 将不会成功构建。
现在, 你可能会不耐烦地敲打你的脚趾并且想要知道 framework 到底什么时候才会开始。这可以理解,因为到目前为止你做了一大堆东西但还没有看到 framework。
好的,某些东西要开始变化了,马上就来了。到现在你还没有创建一个 framework 的原因是因为它就是一个静态库和头文件的集合 - 正是你之前所做的。
制作一个 framework 会有几点特别的地方:
-
目录结构。Frameworks 有着 Xcode 认可的特殊目录结构。你会创建一个构建任务,这将为你创建这种结构。
-
当你构建库的时候,它只会生成当前必须的架构,例如 i386,arm7,等等。为了让一个框架有作用,在构建的时候它需要包含所有需要运行的架构。你将会创建一个新的产品,它将构建必须的架构并把它们放到框架中。
在这个部分会有大量的神奇脚本,但我会讲慢点,它们不会很复杂。
正如之前提到的,一个框架有着特殊的目录结构,看起来像是这样:
现在在静态库编译过程中要给它添加一个脚本。选择 RWUIControls
工程,并选择 RWUIControls
静态库目标。选择 Build Phases
标签并通过选择 Editor/Add Build Phase/Add Run Script Build Phase
来添加一个新的脚本。
在 Build Phases 部分创建了一个新的面板,这能让你在编译阶段的某个时刻运行一个任意的 Bash 脚本。如果你想在编译过程中改变脚本的运行时刻就在列表中拖动面板。对于框架工程来说,在最后运行脚本就行,因此你可以默认放置即可。
Rename the script by double clicking on the panel title Run Script and replace it with Build Framework.
双击重命名面板标题为 Build Framework
。
把下面的 Bash 脚本粘贴到脚本框中。
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"
这段脚本首先创建了 RWUIControls.framework/Versions/A/Headers
目录,然后创建了一个框架所必须的三个语法链接
-
Versions/Current => A
-
Headers => Versions/Current/Headers
-
RWUIControls => Versions/Current/RWUIControls
最后,公有头文件从你之前指定的公有头文件路径拷贝到 Versions/A/Headers
目录。-a
参数确保了在拷贝的时候编辑时间不会改变,从而防止不必要的重新构建。
现在,选择 RWUIControls
静态库方案和 iOS Device
构建目标,然后通过 cmd+B
构建。
右键 libRWUIControls.a
并在 Finder
中显示。
在构建目录中你可以访问到 RWUIControls.framework
,并确认目录的结构显示的是正确的:
在完成你框架的道路上这真是一个质的飞跃,但你会发现仍然没有一个静态库。这就是接下来要做的。
iOS app 需要在不同的架构上运行:
- arm7: 用于 iOS 7 所支持的最老的设备
- arm7s: 用于 iPhone 5 和 5C
- arm64: 用于 iPhone 5S 和 iPhone 6 等 64-bit ARM 处理器
- i386: 用于 32-bit 模拟器
- x86_64: 用于 64-bit 模拟器
每种架构都需要不同的二进制库,并且当你构建一个 app 的时候,无论你当前是何种设备 Xcode 都会正确的构建相应的架构。
这意味着构建会很快。当你归档 app 或构建 release 模式的 app 时,Xcode 会构建所有的三种 ARM 架构,从而让 app 运行到大部分设备上。那其他的版本呢?
自然地,当你构建框架时,你想要开发者能够尽可能使用所有的架构,对吗?如果是这样那表示你会得到同行的尊敬与敬佩。
因此你需要让 Xcode 构建所有的五种架构。这个过程会创建一个所谓的臃肿的库,它包含有每个架构部分。啊哈!
注意:其实这里强调的另一个原因是要创建一个依赖静态库的示例 app:这个库只为示例 app 需要的架构构建,并只会在某些东西改变的时候才重新编译。为什么这会令你异常兴奋?因为这会让开发周期尽可能的缩短。
单击 RWUIControls 工程,创建一个新的目标(target)。
选择 iOS/Other/Aggregate
, 单击 Next
并命名目标为 Framework
。
注意:为什么要使用 Aggregate
目标来构建一个 Framework?为什么不直接新建?因为 Frameworks 对 OS X 的支持更好,这个事实体现在 Xcode 为 OS X 应用提供了一个非常方便直接的 Cocoa Framework 构建目标。为了解决这个问题,你要使用 Aggregate
构建目标(target)来做为编译框架目录结构的 bash 脚本的钩子(hook)。你开始明白这里面疯狂的地方了吗?
无论何时创建一个新的 framework 目标(target)都必须确保添加了静态库依赖。选择 Framework 目标(target)和 Build Phases
标签。展开 Target Dependencies
面板并添加静态库依赖。
这个目标的主要构建部分是多平台编译,你将会用到脚本来执行。正如你之前所做的,在 Build Phases
中创建一个 Run Script
。
双击,把名字命名为 MultiPlatform Build
。
粘贴下面的脚本到脚本框中:
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
确保如果脚本的某部分失败了那就让整个脚本都失败。这能帮你避免生成不完全的 framework。- 接下来,
RW_MULTIPLATFORM_BUILD_IN_PROGRESS
变量决定是否脚本有被递归的调用。如果有,那就退出执行。 - 然后就是设置一些变量。框架的名字将会跟工程名字一样,例如
RWUIControls
,还有静态库是libRWUIControls.a
。
接下来的脚本会设置些工程随后会用到的函数。把下面的代码添加到脚本框的底部:
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
需要SDK
作为参数,例如iphoneos7.0
,然后会构建相应的静态库。大部分参数都是直接从当前的构建任务中传进来,但不同的地方在于ONLY_ACTIVE_ARCH
是用来确保为当前的 SDK 构建所有的架构。make_fat_library
使用lipo
把两个静态库变成一个。它的参数是两个输入库后面紧跟着输出位置。点击来了解更多关于 lilp 的信息。
下个部分的脚本确定了更多变量,为了你能使用上面两个方法。你需要知道其他的 SDK 是什么,例如 iphoneos7.0
应该跳转到 iphonesimulator7.0
反之亦然,还要定位 SDK 的构建目录。
# 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
这四个语句看起来都非常相似,它们使用字符串比较和正则表达式来确定 RW_OTHER_PLATFORM
和 RW_OTHER_BUILT_PRODUCTS_DIR
的值。
这四个 if
语句的详细解释:
SDK_NAME
将会是iphoneos7.0
或iphonesimulator6.1
。这个正则表达式从字符串的开头处开始提取非数字字符。因此,它的结果是iphoneos
或者iphonesimulator
。- 这个正则表达式从
SDK_NAME
变量取得数字版本号,例如 7.0 或 6.1 等等。 - 这是简单的
iphonesimulator
和iphoneos
之间的字符串比较,反之亦然。 - 从产品构建目录路径的末尾处得到平台名称并用其他平台替换。这个确保其他平台的构建目录能被找到。当加入两个静态库的时候这至关重要。
现在你可以为其他平台编译了,随后会加入产生的静态库。
把下面的脚本添加到末尾处:
# 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}"
- 首先通过之前定义好的函数来编译其他平台。
- 如果你当前要为模拟器编译,那默认的 Xcode 只会为那个系统编译,例如 i386 或者 x86_64。为了编译所有的架构,第二部分调用
build_static_library
用iphonesimulator SDK
重新编译,来确保编译了所有架构。 - 最后调用
make_fat_library
函数把当前构建目录的静态库和其他构建目录加到一起来制作完整的多架构静态库。这个会放到 framework 里面。
最后是个简单的拷贝命令的脚本。在末尾添加下面的脚本:
# 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"
- 第一个命令保证 framework 出现在多平台的构建目录里。
- 第二个部分拷贝完成的 framework 到用户的桌面。这是可选步骤,但我发现把 framework 放到某个容易访问的地方会非常友好。
选择 Framework
集合(aggregate) 方案,并按下 cmd+B 来编译框架。
编译完成后桌面会出现 RWUIControls.framework
。
为了检查多平台时候正确编译,启动终端并执行以下操作:
$ cd ~/Desktop/RWUIControls.framework
$ RWUIControls.framework xcrun lipo -info RWUIControls
第一行是切换到框架目录,第二行使用了 lipo 命令来得到关于 RWUIControls
库的相关信息。这会列出这个库里出现的所有部分。
你能看到这儿有五个部分:i386, x86_64, arm7, arm7s and arm64,正好是你在编译的时候设置的。你之前运行过 lipo -info
命令,你会看到这几个部分的子集。
Okay, you have a framework, you have libraries and they’re elegant solutions for problems you’ve not yet encountered. But what’s the point of all this?
One of the primary advantages in using a framework is its simplicity in use. Now you’re going to create a simple iOS app that uses the RWUIControls.framework
that you’ve just built.
Start by creating a new project in Xcode. Choose 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
.
Look in the ImageViewer
directory of the zip file you downloaded earlier for a sample image. Drag sampleImage.jpg
from the finder into the ImageViewer
group in Xcode.
Check the Copy items into destination group’s folder
box, and click Finish
to complete the import.
Importing a framework follows a nearly identical process. Drag RWUIControls.framework
from the desktop into the Frameworks
group in Xcode. Again, ensure that you’ve checked the box before Copy items into destination group’s folder
.
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 that does the following:
- Import the framework’s header with
#import <RWUIControls/RWUIControls.h>.
- Set up a couple of private properties to hold the
UIImageView
and theRWKnobControl
. - Create a UIImageView, and use the sample image that you added to the project a few steps back.
- 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 thetransform
property of theUIImageView
so the image rotates as the knob control is moves.
For further details on how to use the RWKnobControl
check out the previous tutorial, which explains how to create it.
Build and run. You’ll see a simple app, which as you change the value of the knob control the image rotates.
Did you notice that the RWUIControls
framework only consists of code and headers? For example, you haven’t used any other assets, such as images. This is a basic limitation on iOS, where a framework can only contain header files and a static library.
Now buckle up, this tutorial is about to take off. In this section you’ll learn how to work around this limitation by using 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 place an image of a ribbon on the top right hand corner of a UIView.
The resources will be added to a bundle, which takes the form of an additional target on the RWUIControls
project.
Open the UIControlDevApp project, and select the RWUIControls
sub-project. Click the Add Target
button, then navigate to OS X/Framework
and Library/Bundle
. Call the bundle RWUIControlsResources
and select Core Foundation
from the framework selection box.
There are a couple of build settings to configure since you’re building a bundle for use in iOS as opposed to the default of 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.
You also need to change the product name to RWUIControls
. Search for product name and double-click to edit. Replace ${TARGET_NAME}
with RWUIControls
.
By default, images which have two resolutions can produce some interesting results; for instance when you include a retina @2x version. They’ll combine into a multi-resolution TIFF, and that’s not a good thing. Search for hidpi and change the COMBINE_HIDPI_IMAGES
setting to NO.
Now you’ll make sure that when you build the framework, the bundle will also build and add the framework as a dependency to the 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.
Now, within the Framework
target’s Build Phases
, 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. Build the framework scheme now so you can see the bundle appear on the desktop.
In order to develop against this new bundle, you’ll need to be able to use it in the example app. This means you must add it as both a dependency, and an object to copy across to the app.
In the Project Navigator, select the UIControlDevApp
project, 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
.
That’s all the setup required. Drag the RWRibbon
directory from inside the zip file you downloaded earlier into the RWUIControls
group within the RWUIControls
project.
Choose Copy the items into the destination group’s folder
, making sure they are part of the RWUIControls
static lib target by ticking the appropriate box.
An important part of the source code is how you reference images. If you take a look at addRibbonView
inside the RWRibbonView.m
file you’ll see the relevant line:
UIImage *image = [UIImage imageNamed:@"RWUIControls.bundle/RWRibbon"];
The bundle behaves just 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 theRWUIControlsResources
target.
Remember the discussion about making sure the framework is accessible to the public? Well, now you need to export the RWRibbon.h
header file, select the file, then choose Public
from the drop down menu in the Target Membership
panel.
Finally, you need to add the header to the framework’s header file. Open RWUIControls.h
and add the following lines:
// RWRibbon
#import <RWUIControls/RWRibbonView.h>
Open RWViewController.m
in the UIControlDevApp
project, and add the following instance variable between the curly braces in the @interface section:
RWRibbonView *_ribbonView;
To 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 UIControlDevApp
scheme and you’ll see the new ribbon control at the bottom of the app:
The last thing to share with you is how to use this new bundle inside another app, the ImageViewer
app you created earlier.
To start, make sure your framework and bundle are up to date. Select the Framework
scheme and then press cmd+B
to build it.
Open up the ImageViewer
project, find the RWUIControls.framework
item inside the Frameworks
group and delete it, choosing Move to Trash if you’re prompted. Then drag the RWUIControls.framework
from your desktop to the Frameworks
group. This is necessary because the framework is much different than it was when you first imported it.
Note: If Xcode refuses to let you add the framework, then it might not have properly moved it to the trash. If this is the case then delete the framework from the ImageViewer
directory in Finder and retry.
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’s added to the ImageViewer
target by ticking the necessary box.
You’re going to add the ribbon to the image, which rotates, so there are a few simple changes to make to the code inRWViewController.m
.
Open it up and change the type of the imageView
property from UIImageView
to RWRibbonView
:
@property (nonatomic, strong) RWRibbonView *imageView;
Replace the first part of the viewDidLoad
method which was responsible for creating and configuring the UIImageView
, with 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];
Build and run the app. You’ll see you’re now using both the RWKnobControl
and the RWRibbonView
from theRWUIControls
framework.