如何为iOS项目开发Framework

原文链接:http://ios.jobbole.com/81583/

上一篇教程里,你学到了如何创建一个可复用的把手控件。然而,还不是很容易让其他开发者方便的复用这个控件。

一种共享它的方式就是直接提供源码文件。然而,这不是特别优雅。有可能你不想共享代码的实现细节。此外,开发者可能不想看见所有的东西,他们只是想继承一部分代码到自己的代码库里。

另一种方式是把你的代码编译成静态库来让开发者添加到他们的项目中去。然而这要求你来提供公共头文件,这样显得非常的笨拙。

你需要有一种简洁的方式来编译你的代码,并且它还要方便的共享和在多个项目间复用。你需要的是只是将静态库和它的头文件打包在一个文件里,然后只需要把这个文件添加到工程里就能立即开始使用。

好消息是这篇教程就是围绕这展开的。通过制作 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 并且通过点击 FileNewProject 和 iOSFramework and LibraryCocoa Touch Static Library 来创建一个新的静态库工程。


把工程命名为 RWUIControls 并且保存工程到一个空目录。
一个静态库工程由头文件和实现文件组成,它们由工程自己创建编译。

为了让开发者更方便的使用你的库和框架,你需要导入一个头文件来访问所有的你希望公开的类,好让他们只需访问这个头文件就行。

当创建静态库工程的时候,Xcode 添加了 RWUIControls.h 和 RWUIControls.m。你不需要实现文件,因此右键 RWUIControls.m 选择删除,按提示把它移到垃圾箱中。

打开 RWUIControls.h 并且用下面的代码替换文件内容:

#import <UIKit/UIKit.h>

这句代码导入了 UIKit 的伞型头文件,它包含有自身所需要的库。当你创建不同的组件类时,你要把它们添加到这个文件里,这样能够确保它们让这个库的使用者能访问。

你构建这个工程时会依赖 UIKit,但 Xcode 静态库工程没有默认的链接到 UIkit。为了修正这个问题,要添加 UIKit 作为一个依赖。选择工程的导航器,并且在主面板选择 RWUIControls 目标。

单击 Build Phases 然后展开 Link Binary With Libraries 部分。单击 + 来添加一个新的框架,查找 UIKit.framework,单击 add 添加。



如果不绑定到头文件的话,静态包是没有用的。这些编译好的类和方法是包含在二进制文件中。你创建的类,有一些你可以在外部使用,另一个则只能在包内使用。

接下来,你需要在构件时添加引用,把公开的头文件放到编译者能使用的地方。最后,你要复制这些东西到框架里。

当你在Xcode里看到Build Phases 时,选择 EditorAdd Build PhaseAdd Copy Headers Build Phase.

注意:如果你发现选项变灰了,试试点击下方的空白区域看看,然后再尝试一遍。


把 RWUIControls.h 从导航器拖到面板的 Public 部分。这确保这个头文件对任何使用你库的用户都可用。


注意:这可能有点多此一举,但把包含有你工程所有公开类头文件的头文件放到公有部分非常重要。否则,开发者在企图使用这个库的时候会发生编译错误。这对任何人都不是开玩笑的,当 Xcode 读取公有头的时又不能读取你忘记添加的公有文件。

创建一个 UI 控件

现在你已经设置好了你的工程,是时候给库添加些功能了。既然这个教程的目的是讲诉如何构建一个 framework,而不是如何构建一个 UI 控件,那你会借用些上篇教程的一些代码。在你之前下载的 zip 文件你会找到 RWKnobControl 目录。把它拖到 Xcode 的 RWUIControls 组别。

选择 Copy items into destination group’s folder 并确保要拷贝的新文件勾选了响应的单选框。


这会同时把实现文件添加到编译列表,默认的头文件在 Project group。这意味着它们都是私有的。


注意:这三个部分的命名如果不拆分开理解会有点令人误解。Public 如你所预料的。Private 头仍然会暴露你的头文件,这有点让人困惑。Project 头是你工程用到的特定私有文件,这有点讽刺。因此,你会慢慢发现要么头文件是放到 Public 要么放在 Project 部分。

现在你需要分享主控件头RWKnobControl.h ,有如下几步要做。首先从Project 组中拖拽Copy Headers 到Public 组。


另一种方式,当你编辑文件的时候会发现更改 Target Membership 面板中的值会更方便。当你开发库继续添加文件的时候这会非常方便。

注意:在你往库中添加新的类时,记得保持成员是最新的。尽可能减少公有的头文件,并确保其余的在 Project 组。

用控件的头文件做的另一件事就是把 RWUIControls.h 它添加到库的主头文件中。这样开发者使用你的库时只需要像下面这样包含这一个文件就行,而不是一堆。

#import <RWUIControls/RWUIControls.h>

因此,把下面的代码添加到 RWUIControls.h

// Knob Control    

#import  <RWUIControls/RWKnobControl.h>

配置 Build 设置

现在你非常接近这个工程的编译部分了。然而,有几个确保库尽可能对用户友好的设置需要配置。

首先,你需要提供一个目录名给你公有头文件将要拷贝到那里去。这确保当你使用静态库的时候能定位到相关的头文件。

单击工程导航栏的工程,然后选择 RWUIControls 静态库目标。选择 Build Setting 标签,然后搜索 public header。双击 Public Header Folder Path 设置并输入下面的路径:

之后你会看到这个目录。

现在你需要改变一些其他的设置,尤其是那些保留在二进制库中的。编译器给了你移除无用代码的选项,指那些从不会访问到的代码。并且你还能移除 debug 符号,例如函数名和其他 debug 时相关的细节。

既然你创建 framework 给其他人使用,那最好把它们都禁用了然后让用户自行选择最适合他们工程的配置。要做这些的话,跟之前一样使用搜索就行,更新下面的设置:

Dead Code Stripping – 设为 NO

Strip Debug Symbols During Copy – 设为 NO for all configurations

Strip Style – 设为 Non-Global Symbols

构建运行。你仍然什么东西看没看到,但这仍然是件好事,这足以说明工程成功的构建的并且没有警告和错误。

要构建的话,选择构建目标为 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 的原因是因为它就是一个静态库和头文件的集合 – 正是你之前所做的。

制作一个 framework 会有几点特别的地方:

目录结构。Frameworks 有着 Xcode 认可的特殊目录结构。你会创建一个构建任务,这将为你创建这种结构。

当你构建库的时候,它只会生成当前必须的架构,例如 i386,arm7,等等。为了让一个框架有效,在构建的时候它需要包含所有需要运行的架构。你将会创建一个新的产品,它将构建必须的架构并把它们放到框架中。

在这个部分会有大量的神奇脚本,但我会讲慢点,它们不会很复杂。

框架结构

正如之前提到的,一个框架有着特殊的目录结构,看起来像是这样


现在在静态库编译过程中要给它添加一个脚本。选择 RWUIControls 工程,并选择 RWUIControls静态库目标。选择 Build Phases 标签并通过选择 Editor/Add Build Phase/Add Run Script Build Phase 来添加一个新的脚本。


在 Build Phases 部分创建了一个新的面板,这能让你在编译阶段的某个时刻运行一个任意的 Bash 脚本。如果你想在编译过程中改变脚本的运行时刻就在列表中拖动面板。对于框架工程来说,在最后运行脚本就行,因此你可以默认放置即可。


双击重命名面板标题为 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 命令,你会看到这几个部分的子集。

如何使用框架

好,现在你已经有了一个框架和一些静态库,它们可以优雅的解决你可能还没遇到的问题。但是这样有什么意义呢?

使用框架的一个主要优点就是使用起来很简单。现在,你使用RWUIControls.framework创建一个简单的IOS app。

用Xcode创建一个新项目,选择 File/New/Project在选择iOS/Application/Single View Application。调出你的新app的ImageViewer;让它只能适配iPhone 并且存放在与上两个项目相同的文件夹下。这个app将给你展示如何使用RWKnobControl手动的旋转一张图片。

查找ImageViewer 目录下之前下好的zip文件,里面有样图。把sampleImage.jpg 从finder里拖到Xcode的ImageViewer 组里。


检查Copy items into destination group’s folder ,然后点击Finish 按钮完成导入。

用同样的步骤导入框架,把RWUIControls.framework 从桌面拖到Xcode的Frameworks 组里。同样的,在Copy items into destination group’s folder 之前,确保你已经检查过


打开RWViewController.m 并替换如下代码:

#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

这个简单的视图控制器还需要做如下操作:

这个简单的视图控制器还需要做如下操作:

导入#import  <RWUIControls/RWUIControls.h> 这个头文件

设置UIImageView 和RWKnobControl 的私有属性成hold

创建UIImageView并设置成你之前添加到项目里的示例图片

创建RWKnobControl ,适当的调整它的位置。

设置下按钮控件的属性,包括rotationAngleChanged:方法的事件监听处理

rotationAngleChanged:方法仅仅监听UIImageView 的transform 属性,因此图片可以随着按钮的移动而旋转。

更多的使用RWKnobControl 的细节请看上一章,里面解释了怎么创建它。

Build并且运行。你就会看到一个简单的app,随着按钮的改变而造成图片的旋转。


使用资源包

你注意到RWUIControls 仅由代码和头文件构成了么?你没有使用其他的资源(图片之类)。在IOS上framework只能柏涵一个头文件和一个静态库。

现在系好安全带,我们马上要起飞了。在这一节中,你会学习到如何使用框架本身的资源包突破这个限制。

为RWUIControls 包创建一个新的UIView ,把它放在右上角,设置图片为一条缎带。

创建bundle

资源加到bundle里之后,RWUIControls 项目里会形成额外的target

打开UIControlDevApp 项目。选择RWUIControls 子项目。点击 Add Target按钮,然后依次点击OS X/Framework and Library/Bundle。输入RWUIControlsResources。 然后从框架选项卡中选择Core Foundation


bundle建立之后,需要做一些设置。选择RWUIControlsResources target然后点击 Build Settings 选项卡。找到base sdk,选择Base SDK这一行,然后按下delete。这样就能从OSX系统变为IOS系统。


你还需要把product name改为RWUIControls. 找到product name双击修改。把${TARGET_NAME} 替换成RWUIControls


图片默认情况下会有两个分辨率,这可能造成一些有趣的结果;比如当你包含一个retina @2x的版本时,它们会结合成一个多分辨率的TIFF格式,那并不是一件好事。找到hidpi 然后把COMBINE_HIDPI_IMAGES 设置为NO


当你创建frameword时,bundle也会建立框架并把它添加成target的附属。选择Framework target,点击Build Phases选项卡。展开Target Dependencies 面板,点击 +, 然后选择RWUIControlsResources 添加为附属。

现在,在Framework target的Build Phases里,打开MultiPlatform Build 面板,添加如下代码在末尾:

# Copy the resources bundle to the user's desktop

ditto "${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.bundle" \

      "${HOME}/Desktop/${RW_FRAMEWORK_NAME}.bundle"

这个命令会复制built bundle带用户桌面。现在,建立框架,你就能看到bundle 出现在桌面上了。


导入Bundle

为了再开发新的bundle,也要能在示例APP里用它。就需要你把它添加为附属,然后根据app添加到相应的对象里。

在导航栏项目中,选择UIControlDevApp 项目,然后点击UIControlDevApp target。扩展RWUIControls 组,把RWUIControls.bundle拖到 Copy Bundle Resources 面板内。

在Target Dependencies 面板点击加号,添加成新的附属,然后选择RWUIControlsResources

建立一个Ribbon 视图

从RWUIControls 项目里的RWUIControls 组里拖拽你之前下载好的zip文件到RWRibbon 目录。


勾选 Copy the items into the destination group’s folder,确保他们能复制到RWUIControls 静态包中。


源码中一个很重要的地方是如何引用图片。如果你看一眼RWRibbonView.m 文件里的addRibbonView函数,你就能看到下面这行:

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

bundle就像一个目录,因此引用bundle里的图片真的很简单。

想要从bundle添加图片,可以在右边的面板中选择它们,它们应该属于RWUIControlsResources target。


我们来讨论一下框架的权限范围应不应该是公开的?好,现在你需要导出RWRibbon.h 头文件,选择这个文件,然后从Target Membership 面板的下拉菜单里选择Public 。


最后,你需要添加这个头到框架的头文件。打开RWUIControls.h 然后添加下面代码:

// RWRibbon

#import <RWUIControls/RWRibbonView.h>

添加Ribbon到示例APP

打开UIControlDevApp 项目里的RWViewController.m 。然后在@interface 区间里添加下面的变量:

RWRibbonView  *_ribbonView;

创建ribbon视图,添加下面代码到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];

构建,运行UIControlDevApp ,在下面,你就能看到一个新的ribbon控制器。


在ImageViewer里使用Bundle

最后想要和你分享的是怎么样使用另一个app里的bundle,我们用你之前创建的ImageViewer app来示范

首先确认下你的框架和bundle是不是最新的,之后选择Framework 然后按下cmd+B构建它

打开ImageViewer 项目,找到Frameworks组里的RWUIControls.framework 选项,使用 Move to Trash 删除它。然后从你的桌面拖拽RWUIControls.framework 到你的Frameworks 组里。这是一定要做的,因为新导入的框架和你之前的有许多的不同。


注意:如果Xcode拒绝添加框架,你没有正确的删除之前的。发送了这种情况,你可以从Finder打开ImageViewer 目录,找到这个框架,然后删除它。

从桌面拖拽bundle导入到ImageViewer 组。选择Copy items into destination group’s folder 并且确保勾选了ImageViewer 选框。


你可以添加ribbon到图片里,要像选择它,你可以在RWViewController.m 代码里做几个简单的改变。

打开UIImageView 和RWRibbonView ,修改imageView 的类型:

@property (nonatomic, strong) RWRibbonView *imageView;

为了创建和配置UIImageView,用下面的代码替换viewDidLoad 方法:

[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];


构建运行app,现在你就能看到使用了theRWUIControls 框架中的RWKnobControl 和RWRibbonView 的效果了。



最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,724评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,104评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,142评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,086评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,076评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,914评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,220评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,871评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,318评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,834评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,951评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,574评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,162评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,162评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,383评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,349评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,652评论 2 343

推荐阅读更多精彩内容