iPhone 应用开发指南中文版

lxhgdut

贡献于2012-05-16

字数:0 关键词: iOS 开发 移动开发

来自 http://www.apple.com.cn/developer/iphone/library/documentation/ iPhone/Conceptual/iPhoneOSProgrammingGuide/Introduction/ Introduction.html 介绍 请注意: 本文档之前命名为 iPhone OS编程指南。 iPhone SDK为创建 iPhone的本地应用程序提供必需的工具和资源。在用户的 Home屏幕上, iPhone的本地应用程序表示为图标。它们和运行在 Safari内部 的 web应用程序不同,在基于 iPhone OS的设备上,它们作为独立的执行程序 来运行。本地应用程序可以访问 iPhone和 iPod Touch的所有特性,比如加速 计、位置服务、和多点触摸接口,正是这些特性使设备变得更加有趣。本地应 用程序还可以将数据保存在本地的文件系统中,甚至可以通过定制的 URL类型 来和安装在设备上的其它程序进行通讯。 为 iPhone OS开发本地应用程序需要使用 UIKit框架 。利用该框架提供的基础设 施和缺省行为,您可以在几分钟内创建一个具有一定功能的应用程序。 UIKit框 架(和系统中的其它框架)不但提供大量的缺省行为,而且提供了一些挂钩, 开发者可以通过这些挂钩来定制和扩展它的行为。 谁应该阅读本文? 本文的目标读者是希望创建 iPhone本地应用程序的新老 iPhone OS开发者,目 的是向您介绍 iPhone应用程序的架构,展示 UIKit和其它重要系统框架中的一 些关键的定制点。在介绍这些内容的同时,本文还将提供一些有助于正确设计 的指导意见。文中还指出一些为特定主题提供建议和进行进一步讨论的其它文 档。 虽然本文描述的很多框架也存在于 Mac OS X系统中,但阅读本文并不需要熟 悉 Mac OS X及其技术。 先决条件 在开始阅读本文之前,您必须至少对下面这些 Cocoa概念有基本的理解: ■ 有关 Xcode和 Interface Builder的基本信息及其在应用程序开发中的作 用。 ■ 如何定义新的 Objective-C类。 ■ 如何 管理内存 包括如何 创建 和释放 Objective-C对象。 ■ 委托 对象在管理应用程序行为中的作用。 ■ 目标 -动作范式在用户界面管理中的作用。 不熟悉 Cocoa和 Objective-C的开发者可以在 Cocoa基本原理指南 中得到相应 的信息。 iPhone应用程序的开发需要在运行 Mac OS X v10.5或更高版本系统以及基于 Intel的 Macintosh电脑上进行,还必须下载和安装 iPhone SDK。有关如何得 到 iPhone SDK的信息,请访问 http://www.apple.com.cn/developer/ iphone/网站。 本文的组织 本文有如下章节: ■ “核心应用程序 ” 描述 iPhone应用程序的基本结构,介绍一些所有应用程 序都需要做好处理准备的关键任务。 ■ “窗口和视图 ” 描述 iPhone的窗口管理模型,展示如何通过视图来组织用 户界面。 ■ “事件处理 ” 描述 iPhone事件处理模型,展示如何处理多点触摸和运动事 件,以及如何在应用程序中使用拷贝和粘贴操作。 ■ “图形和描画 ” 描述 iPhone OS的图形架构,展示如何描画各种形状和图 像,以及如何在使用动画。 ■ “文本和 Web” 描述 iPhone OS的文本支持,介绍一些管理系统键盘的实 例。 ■ “文件和网络 ” 为如何操作文件和网络连接提供一些指导原则。 ■ “多媒体支持 ” 展示如何使用 iPhone OS中的音频和视频技术。 ■ “设备支持 ” 展示如何使用外接配件接口、位置服务、加速计、和内置的 照相机接口。 ■ “应用程序的偏好设置 ” 展示如何配置应用程序的偏好设置及如何将这些 设置显示在 Settings应用程序中。 提供反馈 如果您对本文有什么反馈,可以通过每个页面下方的内置反馈表进行反映。 如果您发现苹果软件或文档存在问题,我们鼓励您报告给苹果公司。如果您希 望某个产品或文档在将来有所改变,则可以提交功能增强报告,具体做法是访 问 ADC网站上的缺陷报告( Bug Reporting)页面并提交报告,其 URL如下: http://developer.apple.com/bugreporter/ 您必须有正当的 ADC登录名和密码才能提交报告。按照缺陷报告页面上的指令 进行操作就可以免费得到一个登录名。 相关信息 下面的文档中包含一些重要的信息,所有的开发者在开发 iPhone OS的应用程 序之前都应该加以阅读: ■ iPhone开发指南 从工具的角度描述 iPhone开发过程中的一些重要信息, 介绍如何配置设备及如何使用 Xcode(和其它工具)连编、运行、和测 试您的软件。 ■ Cocoa基本原理指南 介绍 iPhone应用程序开发中使用的设计模式以及其 它与实践相关的信息。 ■ iPhone人机界面指南 就如何设计 iPhone应用程序的用户界面提供指导和 重要信息。 下面的框架参考和概念性文档提供一些与 iPhone关键主题相关的信息: ■ UIKit框架参考 和 Foundation框架参考 为本文讨论的类提供参考信息。 ■ iPhone OS的视图控制其编程指南 讨论如何在 iPhone应用程序界面中使 用视图控制器。 ■ iPhone OS的表视图编程指南 介绍如何使用表视图,表视图在 iPhone应 用程序中经常被使用。 ■ Objective-C 2.0编程语言 介绍 Objective-C和 Objective-C运行系统, 它们是 iPhone OS的很多动态行为及扩展性的基础。 核心应用程序 所有的 iPhone应用程序都是基于 UIKit框架构建而成的,因此,它们在本质上 具有相同的核心架构。 UIKit负责提供运行应用程序和协调用户输入及屏幕显示 所需要的关键对象。应用程序之间不同的地方在于如何配置缺省对象,以及如 何通过定制对象来添加用户界面和行为。 虽然应用程序的界面和基本行为的定制发生在定制代码的内部,但是,还有很 多定制需要在应用程序的最高级别上进行。这些高级的定制会影响应用程序和 系统、以及和设备上的其它程序之间的交互方式,因此,理解何时需要定制、 何时缺省行为就已经足够是很重要的。本章将概要介绍核心应用程序架构和高 级别的定制点,帮助您确定什么时候应该定制,什么时候应该使用缺省的行 为。 核心应用程序架构 从应用程序启动到退出的过程中, UIKit框架负责管理大部分关键的基础设施。 iPhone应用程序不断地从系统接收事件,而且必须响应那些事件。接收事件是 UIApplication对象的工作,但是,响应事件则需要您的定制代码来处理。 为了理解事件响应需要在哪里进行,我们有必要对 iPhone应用程序的整个生命 周期和事件周期有一些理解。本文的下面部分将描述这些周期,同时还对 iPhone应用程序开发过程中使用的一些关键设计模式进行总结。 应用程序的生命周期 应用程序的生命周期是由发生在程序启动到终止期间的一序列事件构成的。在 iPhone OS中,用户可以通过轻点 Home屏幕上的图标来启动应用程序。在轻 点图标之后的不久,系统就会显示一个过渡图形,然后调用相应的 main函数 来启动应用程序。从这个点之后,大量的初始化工作就会交给 UIKit,由它装载 应用程序的 用户界面 和准备事件循环。在事件循环过程中, UIKit会将事件分发 给您的 定制对象 及响应应用程序发出的命令。当用户进行退出应用程序的操作 时, UIKit会通知应用程序,并开始应用程序的终止过程。 图 1-1显示了一个简化了的 iPhone应用程序生命周期。这个框图展示了发生在 应用程序启动到退出过程中的事件序列。在应用程序初始化和终止的时候, UIKit会向应用程序 委托 对象发送特定的消息,使其知道正在发生的事件。在事 件循环中, UIKit将事件派发给应用程序的定制事件处理器。有关初始化和终止 事件的如何处理的信息,将在随后的 “初始化和终止 ”部分进行讨论;事件处理 的过程则在 “事件处理周期 ”部分介绍,在后面的章节也还有更为详细的讨论。 图 1-1  应用程序的生命周期 主函数 在 iPhone的应用程序中, main函数仅在最小程度上被使用,应用程序运行所 需的大多数实际工作由 UIApplicationMain函数来处理。因此,当您在 Xcode中开始一个新的应用程序工程时,每个工程模板都会提供一个 main函 数的标准实现,该实现和 “处理关键的应用程序任务 ”部分提供的实现是一样 的。 main 例程只做三件事:创建一个 自动释放池 ,调用 UIApplicationMain函数,以及使用自动释放池。除了少数的例外,您永 远不应该改变这个函数的实现。 程序清单 1-1  iPhone应用程序的 main函数 #import int main(int argc, char *argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; int retVal = UIApplicationMain(argc, argv, nil, nil); [pool release]; return retVal; } 请注意: 自动释放池用于内存管理,它是 Cocoa的一种机制,用于延缓释放具 有一定功能的代码块中创建的对象。有关自动释放池的更多信息,请参见 Cocoa内存管理编程指南 ;如果需要了解与自动释放池有关的具体内存管理规 则,则请参见 “恰当地分配内存 ”部分。 程序清单的核心代码是 UIApplicationMain函数,它接收四个参数,并将 它们用于初始化应用程序。传递给该函数的缺省值并不需要修改,但是它们对 于应用程序启动的作用还是值得解释一下。除了传给 main函数的 argc和 argv之外,该函数还需要两个字符串参数,用于标识应用程序的首要类(即 应用程序对象所属的类)和应用程序委托类。如果首要类字符串的值为 nil, UIKit就缺省使用 UIApplication类;如果应用程序委托类为 nil, UIKit就 会将应用程序主 nib文件(针对通过 Xcode模板创建的应用程序)中的某个对 象假定为应用程序的委托对象。如果您将这些参数设置为非 nil值,则在应用 程序启动时, UIApplicationMain函数会创建一个与传入值相对应的类实 例,并将它用于既定的目的。因此,如果您的应用程序使用了 UIApplication类的定制子类(这种做法是不推荐的,但确实是可能的), 就需要在第三个参数指定该定制类的类名。 应用程序的委托 监控应用程序的高级行为是应用程序 委托 对象的责任,而应用程序委托对象是 您提供的定制类实例。委托是一种避免对复杂的 UIKit对象(比如缺省的 UIApplication对象)进行子类化的机制。在这种机制下,您可以不进行子 类化和方法重载,而是将自己的定制代码放到委托对象中,从而避免对复杂对 象进行修改。当您感兴趣的事件发生时,复杂对象会将消息发送给您定制的委 托对象。您可以通过这种 “挂钩 ”执行自己的定制代码,实现需要的行为。 重要提示: 委托模式的目的是使您在创建应用程序的时候省时省力,因此是非 常重要的设计模式。如果您需要概要了解 iPhone应用程序中使用的重要设计模 式,请参见 “基本设计模式 ”部分;如果需要对委托和其它 UIKit设计模式的详细 描述,则请参见 Cocoa基本原理指南 部分。 应用程序的委托对象负责处理几个关键的系统消息。每个 iPhone应用程序都必 须有应用程序委托对象,它可以是您希望的任何类的实例,但需要遵循 UIApplicationDelegate协议 ,该协议的方法定义了应用程序生命周期 中的某些挂钩,您可以通过这些方法来实现定制的行为。虽然您不需要实现所 有的方法,但是每个应用程序委托都应该实现 “处理关键的应用程序任务 ”部分 中描述的方法。 有关 UIApplicationDelegate 协议方法的更多信息请参见 UIApplicationDelegate协议参考 。 主 Nib文件 初始化的另一个任务是装载应用程序的主 nib文件 。如果应用程序的信息 属性 列表 (Info.plist)文件中含有 NSMainNibFile键,则作为初始化过程的 一个部分, UIApplication对象会装载该键指定的 nib文件。主 nib文件是 唯一一个自动装载的 nib文件,其它的 nib文件可以在稍后根据需要进行装载。 Nib文件是基于磁盘的资源文件,用于存储一或多个对象的快照。 iPhone应用 程序的主 nib文件通常包含一个窗口对象和一个应用程序委托对象,还可能包 含一个或多个管理窗口的其它重要对象。装载一个 nib文件会使该文件中的对 象被重新构造,从而将每个对象的磁盘表示转化为应用程序可以操作的内存对 象。从 nib文件中装载的对象和通过编程方式创建的对象之间没有区别。然 而,对于用户界面而言,以图形的方式(使用 Interface Builder程序)创建与 用户界面相关联的对象并将它们存储在 nib文件中通常比以编程的方式进行创 建更加方便。 有关 nib文件及其在 iPhone应用程序中如何使用的更多信息,请参见 “Nib文 件 ”部分,有关如何为应用程序指定主 nib文件的信息则请参见 “信息属性列 表 ”部分。 事件处理周期 在应用程序初始化之后, UIApplicationMain函数就会启动管理应用程序 事件和描画周期的基础组件,如图 1-2所示。在用户和设备进行交互的时候, iPhone OS会检测触摸事件,并将事件放入应用程序的事件队列。然后, UIApplication对象的事件处理设施会从队列的上部逐个取出事件,将它分 发到最适合对其进行处理的对象。举例来说,在一个按键上发生的触摸事件会 被分发到对应的按键对象。事件也可以被分发给 控制器对象 和应用程序中不直 接负责处理触摸事件的其它对象。 图 1-2  事件和描画周期 在 iPhone OS的多点触摸事件模型中,触摸数据被封装在事件对象 ( UIEvent)中。为了跟踪触摸动作,事件对象中包含一些触摸对象 ( UITouch),每个触摸对象都对应于一个正在触摸屏幕的手指。当用户把 手指放在屏幕上,然后四处移动,并最终离开屏幕的时候,系统通过对应的触 摸对象报告每个手指的变化。 在启动一个应用程序时,系统会为该程序创建一个进程和一个单一的线程。这 个初始线程成为应用程序的主线程, UIApplication对象正是在这个线程中 建立 主运行循环 及配置应用程序的事件处理代码。图 1-3显示了事件处理代码 和主运行循环的关系。系统发送的触摸事件会在队列中等待,直到被应用程序 的主运行循环处理。 图 1-3  在主运行循环中处理事件 请注意: 运行循环负责监视指定执行线程的输入源。当输入源有数据需要处理 的时候,运行循环就唤醒相应的线程,并将控制权交给输入源的处理器代码。 处理器在完成任务后将控制权交回运行循环,然后,运行循环就处理下一个事 件。如果没有其它事件,运行循环会使线程进入休眠状态。您可以通过 Foundation框架的 NSRunLoop类来安装自己的输入源,包括端口和定时器。 更多有关 NSRunLoop和运行循环的一般性讨论,请参见 线程编程指南 。 UIApplication对象用一个处理触摸事件的输入源来配置主运行循环,使触 摸事件可以被派发到恰当的响应者对象。响应者对象是继承自 UIResponder 类的对象,它实现了一或多个事件方法,以处理触摸事件不同阶段发生的事 件。应用程序的响应者对象包括 UIApplication、 UIWindow、 UIView、 及所有 UIView子类的实例。应用程序通常将事件派发给代表应用程序主窗口 的 UIWindow对象,然后由窗口对象将事件传送给它的 第一响应者 ,通常是发 生触摸事件的视图对象( UIView)。 除了定义事件处理方法之外, UIResponder类还定义了 响应者链 的编程结 构。响应者链是为实现 Cocoa协作事件处理而设计的机制,它由应用程序中一 组链接在一起的响应者对象组成,通常以第一响应者作为链的开始。当发生某 个事件时,如果第一响应者对象不能处理,就将它传递给响应者链中的下一个 对象。消息继续在链中传递 —从底层的响应者对象到诸如窗口、应用程序、和 应用程序委托这样的高级响应者对象 —直到事件被处理。如果事件最终没有被 处理,就会被丢弃。 进行事件处理的响应者对象可能发起一系列程序动作,结果导致应用程序重画 全部或部分用户界面(也可能导致其它结果,比如播放一个声音)。举例来 说,一个控键对象(也就是一个 UIControl的子类对象)在处理事件时向另 一个对象(通常是控制器对象,负责管理当前活动的视图集合)发送动作消 息。在处理这个动作消息时,控制器可能以某种方式改变用户界面或者视图的 位置,而这又要求某些视图对自身进行重画。如果这种情况发生,则视图和图 形基础组件会接管控制权,尽可能以最有效的方式处理必要的重画事件。 更多有关事件、响应者、和如何在定制对象中处理事件的信息,请参见 “事件处 理 ”部分;更多有关窗口及视图如何与事件处理机制相结合的信息,请参见 “视 图交互模型 ”部分;有关图形组件及视图如何被更新的更多信息,则请参见 “视 图描画周期 ”部分。 基本设计模式 UIKit框架的设计结合了很多在 Mac OS X Cocoa应用程序中使用的设计模式。 理解这些设计模式对于创建 iPhone应用程序是很关键的,我们值得为此花上几 分钟时间。下面部分将简要概述这些设计模式。 表 1-1  iPhone应用程序使用的设计模式 设计模 式 描述 模型 - 视图 - 控制器 模型 -视图 -控制器 (MVC)模式将您的代码分割为几个独立的部 分。 模型 部分定义应用程序的数据引擎,负责维护数据的完整 性; 视图 部分定义应用程序的用户界面,对显示在用户界面上的 数据出处则没有清楚的认识; 控制器 部分则充当模型和控制器的 桥梁,帮助实现数据和显示的更新。 委托 委托 模式可以对复杂对象进行修改而不需要子类化。与子类化不 同的是,您可以照常使用复杂对象,而将对其行为进行修改的定 制代码放在另一个对象中,这个对象就称为委托对象。复杂对象 需要在预先定义好的时点上调用委托对象的方法,使其有机会运 行定制代码。 目标 - 动作 控件通过 目标 -动作 模式将用户的交互通知给您的应用程序。当 用户以预先定义好的方式(比如轻点一个按键)进行交互时,控 件就会将消息(动作)发送给您指定的对象(目标)。接收到动 作消息后,目标对象就会以恰当的方式进行响应(比如在按动按 键时更新应用程序的状态)。 委托内 存模型 Objective-C使用引用计数模式来确定什么时候应该释放内存中 的对象。当一个对象刚刚被创建时,它的引用计数是 1。然后, 其它对象可以通过该对象的 retain、 release、或 autorelease方法来增加或减少引用计数。当对象的引用计数 变为 0时, Objective-C运行环境会调用对象的清理例程,然后解 除分配该对象。 有关这些设计模式更为详尽的讨论请参见 Cocoa基本原理指南 。 应用程序运行环境 iPhone OS的运行环境被设计为快速而安全的程序执行环境。下面的部分这个 运行环境的关键部分,并就如何在这个环境中进行操作提供一些指导。 启动过程快,使用时间短 iPhone OS设备的优势是它们的便捷性。用户通常从口袋里掏出设备,用上几 秒或几分钟,就又放回口袋中了。在这个过程中,用户可能会打电话、查找联 系人、改变正在播放的歌曲、或者取得一片信息。 在 iPhone OS中,每次只能有一个前台应用程序。这意味着每次用户在 Home 屏幕上轻点您的应用程序图标时,您的程序必须快速启动和初始化,以尽可能 减少延迟。如果您的应用程序花很长时间来启动,用户可能就不喜欢了。 除了快速启动,您的应用程序还必须做好快速退出的准备。每次用户离开您的 应用程序时,无论是按下 Home键还是通过软件提供的功能打开了另一个应用 程序, iPhone OS会通知您的应用程序退出。在那个时候,您需要尽快将未保 存的修改保存到磁盘上。如果您的应用程序退出的时间超过 5秒,系统可能会 立刻终止它的运行。 当用户切换到另一个应用程序时,虽然您的程序不是运行在后台,但是我们鼓 励您使它看起来好像是在后台运行。当您的程序退出时,除了对未保存的数据 进行保存之外,还应该保存当前的状态信息;而在启动时,则应该寻找这些状 态信息,并将程序恢复到最后一次使用时的状态。这样可以使用户回到最后一 次使用时的状态,使用户体验更加一致。以这种方式保存用户的当前位置还可 以避免每次启动都需要经过多个屏幕才能找到需要的信息,从而节省使用的时 间。 应用程序沙箱 由于安全的原因, iPhone OS将每个应用程序(包括其偏好设置信息和数据) 限制在文件系统的特定位置上。这个限制是安全特性的一部分,称为应用程序 的 “沙箱 ”。 沙箱 是一组细粒度的控制,用于限制应用程序对文件、偏好设置、 网络资源、和硬件等的访问。在 iPhone OS中,应用程序和它的数据驻留在一 个安全的地方,其它应用程序都不能进行访问。在应用程序安装之后,系统就 通过计算得到一个不透明的标识,然后基于应用程序的根目录和这个标识构建 一个指向应用程序家目录的路径。因此,应用程序的家目录具有如下结构: 1 /ApplicationRoot/ApplicationID/ 在安装过程中,系统会创建应用程序的家目录和几个关键的子目录,配置应用 程序沙箱,以及将应用程序的程序包拷贝到家目录上。将应用程序及其数据放 在一个特定的地方可以简化备份 -并 -恢复操作,还可以简化应用程序的更新及 卸载操作。有关系统为每个应用程序创建的专用目录、应用程序更新、及备 份 -并 -恢复操作的更多信息,请参见 “文件和数据管理 ”部分。 重要提示: 沙箱可以限制攻击者对其它程序和系统造成的破坏,但是不能防止 攻击的发生。换句话说,沙箱不能使您的程序避免恶意的直接攻击。举例来 说,如果在您的输入处理代码中有一个可利用的缓冲区溢出,而您又没有对用 户输入进行正当性检查,则攻击者可能仍然可以使您的应用程序崩溃,或者通 过这种漏洞来执行攻击者的代码。 虚拟内存系统 在本质上, iPhone OS使用与 Mac OS X同样的虚存系统。在 iPhone OS中,每 个程序都仍然有自己的虚拟地址空间,但其可用的虚拟内存受限于现有的物理 内存的数量(这和 Mac OS X不同)。这是因为当内存用满的时候, iPhone OS 并不将非永久内存页面( volatile pages)写入到磁盘。相反,虚拟内存系统 会根据需要释放永久内存( nonvolatile memory),确保为正在运行的应用 程序提供所需的空间。内存的释放是通过删除当前没有正在使用或包含只读内 容(比如代码页面)的内存页面来实现的,这样的页面可以在稍后需要使用的 时候重新装载到内存中。 如果内存还是不够,系统也可能向正在运行的应用程序发出 通告 ,要求它们释 放额外的内存。所有的应用程序都应该响应这种通告,并尽自己所能减轻系统 的内存压力。有关如何在应用程序中处理这种通告的更多信息,请参见 “观察低 内存警告 ”部分。 自动休眠定时器 iPhone OS试图省电的一个方法是使用自动休眠定时器。如果在一定的时间内 没有检测到触摸事件,系统最初会使屏幕变暗,并最终完全关闭屏幕。大多数 开发者都应该让这个定时器打开,但是,游戏和不使用触摸输入的应用程序开 发者可以禁用这个定时器,使屏幕在应用程序运行时不会变暗。将共享的 UIApplication对象的 idleTimerDisabled属性设置为 YES,就可以禁 用自动休眠定时器。 由于禁用休眠定时器会导致更大的电能消耗,所以开发者应该尽一切可能避免 这样做。只有地图程序、游戏、以及不依赖于触摸输入而又需要在设备屏幕上 显示内容的应用程序才应该考虑禁用休眠定时器。音频应用程序不需要禁用这 个定时器,因为在屏幕变暗之后,音频内容可以继续播放。如果您禁用了定时 器,请务必尽快重新激活它,使系统可以更省电。有关应用程序如何省电的其 它贴士,请参见 “减少电力消耗 ”部分。 应用程序的程序包 当您连编 iPhone程序时, Xcode会将它组织为 程序包 。 程序包 是文件系统中的 一个目录,用于将执行代码和相关资源集合在一个地方。 iPhone应用程序包中 包含应用程序的执行文件和应用程序需要用到的所有资源(比如应用程序图 标、其它图像、和本地化内容)。表 1-2列出了一个典型的 iPhone应用程序包 中的内容(为了便于说明,我们称之为 MyApp)。这个例子只是为了演示,表 中列出的一些文件可能并不出现在您自己的应用程序包中。 表 1-2  一个典型的应用程序包 文件 描述 MyApp 包含应用程序代码的执行文件,文件名是略去 .app后 缀的应用程序名。这个文件是必需的。 Settings.bun dle 设置程序包是一个文件包,用于将应用程序的偏好设 置加入到 Settings程序中。这种程序包中包含一些 属 性列表 和其它资源文件,用于配置和显示您的偏好设 置。更多信息请参见 “显示应用程序的偏好设置 ”部 分。 Icon.png 这是个 57 x 57像素的图标,显示在设备的 Home屏 幕上,代表您的应用程序。这个图标不应该包含任何 光亮效果。系统会自动为您加入这些效果。这个文件 是必须的。更多有关这个图像文件的信息,请参见 “应 用程序图标和启动图像 ”部分。 Icon- Settings.png 这是一个 29 x 29像素的图标,用于在 Settings程序 中表示您的应用程序。如果您的应用程序包含设置程 序包,则在 Settings程序中,这个图标会显示在您的 应用程序名的边上。如果您没有指定这个图标文件, 系统会将 Icon.png文件按比例缩小,然后用做代替 文件。有关这个图像文件的更多信息,青参见 “显示应 用程序的偏好设置 ”部分。 MainWindow.n ib 这是应用程序的主 nib文件 ,包含应用程序启动时装载 的缺省用户界面对象。典型情况下,这个 nib文件包含 应用程序的主窗口对象和一个应用程序 委托 对象实 例。其它界面对象则或者从其它 nib文件装载,或者在 应用程序中以编程的方式创建(主 nib文件的名称可以 通过 Info.plist文件中的 NSMainNibFile键来 指定,进一步的信息请参见 “信息属性列表 ”部分)。 Default.png 这是个 480 x 320像素的图像,在应用程序启动的时 候显示。系统使用这个文件作为临时的背景,直到应 用程序完成窗口和用户界面的装载。有关这个图像文 件的信息请参见 “应用程序图标和启动图像 ”部分。 iTunesArtwor k 这是个 512 x 512的图标,用于通过 ad-hoc方式发布 的应用程序。这个图标通常由 App Store来提供,但 是通过 ad-hoc方式分发的应用程序并不经由 App Store,所以在程序包必须包含这个文件。 iTunes用 这个图标来代表您的程序(如果您的应用程序在 App Store上发布,则在这个属性上指定的文件应该和提交 到 App Store的文件保持一致(通常是个 JPEG或 PNG 文件),文件名必须和左边显示的一样,而且不带文 件扩展名)。 Info.plist 这个文件也叫信息 属性列表 ,它是一个定义应用程序 键值的属性列表,比如程序包 ID、版本号、和显示名 称。进一步的信息请参见 “信息属性列表 ”部分。这个 文件是必需的。 sun.png (或其 它资源文件 ) 非本地化资源放在程序包目录的最上层(在这个例子 中, sun.png表示一个非本地化的图像)。应用程序 在使用非本地化资源时,不需要考虑用户选择的语言 设置。 en.lproj fr.lproj es.lproj 其它具体语言的工 程目录 本地化资源 放在一些子目录下,子目录的名称是 ISO 639-1定义的语言缩写加上 .lproj后缀组成的(比 如 en.lproj、 fr.lproj、和 es.lproj目录分别 包含英语、法语、和西班牙语的本地化资源)。更多 信息请参见 “国际化您的应用程序 ”部分。 iPhone应用程序应该是 国际化 的。程序支持的每一种语言都有一个对应的语 言 .lproj文件夹。除了为应用程序提供定制资源的本地化版本之外,您还可 以本地化您的应用程序图标( Icon.png)、缺省图像( Default.png)、 和 Settings图标( Icon-Settings.png),只要将同名文件放到具体语言 的工程目录就可以了。然而,即使您提供了本地化的版本,也还是应该在应用 程序包的最上层包含这些文件的缺省版本。当某些的本地化版本不存在的时 候,系统会使用缺省版本。 您可以通过 NSBundle类的方法或者与 CFBundleRef类型相关联的函数来获 取应用程序包中本地化和非本地化图形及声音资源的路径。举例来说,如果您 希望得到图像文件 sun.png(显示在 “响应中断 ”部分中)的路径并通过它创建 一个图像文件,则需要下面两行 Objective-C代码: NSString* imagePath = [[NSBundle mainBundle] pathForResource:@"sun" ofType:@"png"]; UIImage* sunImage = [[UIImage alloc] initWithContentsOfFile:imagePath]; 代码中的 mainBundle类方法用于返回一个代表应用程序包的对象。有关资 源装载的信息请参见 资源编程指南 。 信息属性列表 信息 属性列表 是一个名为 Info.plist的文件,通过 Xcode创建的每个 iPhone应用程序都包含一个这样的文件。属性列表中的键值对用于指定重要的 应用程序运行时配置信息。信息属性列表的元素被组织在一个层次结构中,每 个结点都是一个实体,比如数组、字典、字符串、或者其它数值类型。 在 Xcode中,您可以通过在 Project菜单中选择 Edit Active Target TargetName命令、然后在目标的 Info窗口中点击 Properties控件来访问信息 属性列表。 Xcode会显示如图 1-4所示的信息面板。 图 1-4  目标 Info窗口的属性面板 属性面板显示的是 程序包 的一些属性,但并不是所有属性都显示在上面。当您 选择 “Open Info.plist as File” 按键或在 Xcode工程中选择 Info.plist文件 时, Xcode会显示如图 1-5所示的属性列表编辑器窗口,您可以通过这个窗口 来编辑属性值和添加键 -值对。您还可以查看添加到 Info.plist文件中的实际键 名,具体操作是按住 Control键的同时点击编辑器中的信息属性列表项目,然 后选择上下文菜单中的 Show Raw Keys/Values命令。 图 1-5  信息属性列表编辑器 Xcode会自动设置某些属性的值,其它属性则需要显式设置。表 1-3列出了一 些重要的键,供您在自己的 Info.plist文件中使用(在缺省情况下, Xcode 不会直接显示实际的键名,因此,下表在括号中列出了这些键在 Xcode中显示 的字符串。您可以查看所有键的实际键名,具体做法是按住 Control键的同时 点击编辑器中的信息属性列表项目,然后选择上下文菜单中的 Show Raw Keys/Values命令)。有关属性列表文件可以包含的完整属性列表及系统如何 使用这些属性的信息,请参见 运行环境配置指南 。 表 1-3  Info.plist文件中重要的键 键 值 CFBundleDis playName (程 序包显示名 ) 显示在应用程序图标下方的名称。这个值应该本地化为 所有支持的语言。 CFBundleIde ntifier (程序 包标识 ) 这是由您提供的标识字符串,用于在系统中标识您的应 用程序。这个字符串必须是一个统一的类型标识符 ( UTI),仅包含字母数字( A-Z、 a-z、 0-9),连字 符( -),和句号( .);且应该使用反向 DNS格式。举 例来说,如果您的公司的域名为 Ajax.com,且您创建 的应用程序名为 Hello,则可以将字符串 com.Ajax.Hello作为应用程序包的标识。 程序包的标识用于验证应用程序的签名。 CFBundleURL Types (URL类 型 ) 这是应用程序能够处理的 URL类型数组。每个 URL类型 都是一个字典,定义一种应用程序能够处理的模式(如 http或 mailto)。应用程序可以通过这个属性来注 册定制的 URL模式。 CFBundleVer sion (程序包版 本号 ) 这是一个字符串,指定程序包的连编版本号。它的值是 单调递增的,由一或多个句号分隔的整数组成。这个值 不能被本地化。 LSRequiresI PhoneOS 这是一个 Boolean值,用于指示程序包是否只能运行在 iPhone OS 系统上。 Xcode自动加入这个键,并将它的 值设置为 true。您不应该改变这个键的值。 NSMainNibFi le (主 nib文件 的名称 ) 这是一个字符串,指定应用程序主 nib文件的名称。如 果您希望使用其它的 nib文件(而不是 Xcode为工程创 建的缺省文件)作为主 nib文件,可以将该 nib文件名关 联到这个键上。 nib文件名不应该包含 .nib扩展名。 UIStatusBar Style 这是个字符串,标识程序启动时状态条的风格。这个键 的值基于 UIApplication.h头文件中声明的 UIStatusBarStyle常量。缺省风格是 UIStatusBarStyleDefault。在启动完成后,应 用程序可以改变状态条的初始风格。 UIStatusBar Hidden 这个一个 Boolean值,指定在应用程序启动的最初阶段 是否隐藏状态条。将这个键值设置为 true将隐藏状态 条。缺省值为 false。 UIInterface Orientation 这是个字符串,标识应用程序用户界面的初始方向。这 个键的值基于 UIApplication.h头文件中声明的 UIInterfaceOrientation 常量。缺省风格是 UIInterfaceOrientationPortrait。 有关将应用程序启动为景观模式的更多信息,请参 见 “以景观模式启动 ”部分。 UIPrerender edIcon 这个一个 Boolean值,指示应用程序图标是否已经包含 发光和斜面效果。这个属性缺省值为 false。如果您不希 望系统在您的原图上加入这些效果,则将它设置为 true。 UIRequiredD eviceCapabi lities 这是个信息键,作用是使 iTunes和 App Store知道应用 程序运行需要依赖于哪些与设备相关的特性。 iTunes和 移动 App Store程序使用这个列表来避免将应用程序安 装到不支持所需特性的设备上。 这个键的值可以是一个 数组或者字典 如果您使用的是数 组,则数组中存在某个键就表示该键对应的特性是必需 的;如果您使用的是字典,则必须为每个键指定一个 Boolean值,表示该键是否需要。无论哪种情况,不包 含某个键表示该键对应的特性不是必需的。 如果您需要可包含在这个字典中的键列表,请参见 表 1-4。这个键在 iPhone OS 3.0及更高版本上才被支 持。 UIRequiresP ersistentWi Fi 这是个 Boolean值,用于通知系统应用程序是否使用 Wi-Fi网络进行通讯。如果您的应用程序需要在一段时 间内使用 Wi-Fi,则应该将这个键值设置为 true;否 则,为了省电,设备会在 30分钟内关闭 Wi-Fi连接。设 置这个标志还可以让系统在 Wi-Fi网络可用但未被使用 的时候显示网络选择对话框。这个键的缺省值是 false。 请注意,当设备处于闲置状态(也就是屏幕被锁定的状 态)时,这个属性的值为 true是没有作用的。这种情 况下,应用程序会被认为是不活动的,虽然它可能在某 些级别上还可以工作,但是没有 Wi-Fi连接。 UISupported ExternalAcc essoryProto cols 这是个字符串数组,标识应用程序支持的配件协议。配 件协议是应用程序和连接在 iPhone或 iPod touch上的 第三方硬件进行通讯的协议。系统使用这个键列出的协 议来识别当配件连接到设备上时可以打开的应用程序。 有关配件和协议的更多信息,请参见 “和配件通讯 ”部 分。这个键只在 iPhone OS 3.0和更高版本上支持。 UIViewGroup Opacity 这是个 Boolean值,用于指示 Core Animation子层是 否继承其超层的不透明特性。这个特性使开发者可以在 仿真器上进行更为复杂的渲染,但是对性能会有显著的 影响。如果属性列表上没有这个键,则其缺省值为 NO。 这个键只在 iPhone OS 3.0和更高版本上支持。 UIViewEdgeA ntialiasing 这是个 Boolean值,用于指示在描画不和像素边界对齐 的层时, Core Animation层是否进行抗锯齿处理。这 个特性使开发者可以在仿真器上进行更为复杂的渲染, 但是对性能会有显著的影响。如果属性列表上没有这个 键,则其缺省值为 NO。 这个键只在 iPhone OS 3.0和更高版本上支持。 如果信息属性文件中的属性值是显示在用户界面上的字符串,则应该进行本地 化,特别是当 Info.plist中的字符串值是与本地化语言子目录下 InfoPlist.strings文件中的字符串相关联的键时。更多信息请参见 “国际 化您的应用程序 ”部分。 表 1-4列出了和 UIRequiredDeviceCapabilities键相关联的数组或字 典中可以包含的键。您应该仅包含应用程序确实需要的键。如果应用程序可以 通过不执行某些代码路径来适应设备特性不存在的情况,则不需要使用对应的 键。 表 1-4  UIRequiredDeviceCapabilities键的字典键 键 描述 telephony 如果您的应用程序需要 Phone程序,则包含这个键。如果 您的应用程序需要打开 tel模式的 URL,则可能需要这个 特性。 sms 如果您的应用程序需要 Messages程序,则包含这个键。 如果您的应用程序需要打开 sms模式的 URL,则可能需要 这个特性。 still- camera 如果您的应用程序使用 UIImagePickerController 接口来捕捉设备照相机的图像时,需要包含这个键。 auto- focus- camera 如果您的应用程序需要设备照相机的自动对焦能力,则需 要包含这个键。虽然大多数开发者应该不需要,但是如果 您的应用程序支持微距摄影,或者需要更高锐度的图像以 进行某种处理,则可能需要包含这个键。 video- camera 如果您的应用程序使用 UIImagePickerController 接口来捕捉设备摄像机的视频时,需要包含这个键。 wifi 当您的应用程序需要设备的网络特性时,包含这个键。 accelerome ter 如果您的应用程序使用 UIAccelerometer接口来接收 加速计事件,则需要包含这个键。如果您的程序仅需要检 测设备的方向变化,则不需要。 location- services 如果您的应用程序使用 Core Location框架来访问设备的 当前位置,则需要包含这个键(这个键指的是一般的位置 服务特性。如果您需要 GPS级别的精度,则还应该包含 gps键)。 gps 如果您的应用程序需要 GPS(或者 AGPS)硬件,以获得 更高精度的位置信息,则包含这个键。如果您包含了这个 键,就应该同时包含 location-services键。如果您 的程序需要更高精度的位置数据,而不是由蜂窝网络或 Wi-fi信号提供的数据,则应该要求只接收 GPS数据。 magnetomet er 如果您的应用程序使用 Core Location框架接收与方向有 关的事件时,则需要包含这个键。 microphone 如果您的应用程序需要使用内置的麦克风或支持提供麦克 风的外设,则包含这个键。 opengles-1 如果您的应用程序需要使用 OpenGL ES 1.1 接口,则包 含这个键。 opengles-2 如果您的应用程序需要使用 OpenGL ES 2.0 接口,则包 含这个键。 应用程序图标和启动图像 显示在用户 Home屏幕上的图标文件的缺省文件名为 Icon.png(虽然通过 Info.plist文件中的 CFBundleIconFile属性可以进行重命名)。它应 该是一个位于 程序包 最上层目录的 PNG文件。应用程序图标应该是一个 57 x 57像素的图像,不带任何刨光和圆角斜面效果。典型情况下,系统在显示之前 会将这些效果应用到图标上。然而,在应用程序的 Info.plist文件中加入 UIPrerenderedIcon键可以重载这个行为,更多信息请参见 表 1-3。 请注意: 如果您以 ad-hoc的方式(而不是通过 App Store)将应用程序发布给 本地用户,则程序包中还应该包含一个 512 x 512像素版本的应用程序图标, 命名为 iTunesArtwork。在分发您的应用程序时, iTunes需要显示这个文 件提供的图标。 应用程序的启动图像文件的文件名为 Default.png。这个图像应该和应用程 序的初始界面比较相似;系统在应用程序准备好显示用户界面之前显示启动文 件,使用户觉得启动速度很快。启动图像也应该是 PNG图像文件,位于应用程 序包的顶层目录。如果应用程序是通过 URL启动的,则系统会寻找名为 Default-scheme.png的启动文件,其中 scheme是 URL的模式。如果该文 件不存在,才选择 Default.png文件。 将一个图像文件加入到 Xcode工程的具体做法是从 Project菜单中选择 Add to Project命令,在浏览器中定位目标文件,然后点击 Add按键。 请注意: 除了程序包顶层目录中的图标和启动图像,您还可以在应用程序中 具 体语言的工程子目录 下包含这些图像文件的本地化版本。更多有关应用程序本 地化资源的信息请参见 “国际化您的应用程序 ”部分。 Nib文件 nib文件 是一种数据文件,用于存储可在应用程序需要时使用的一些 “冻结 ”的对 象。大多数情况下,应用程序使用 nib文件来存储构成用户界面的窗口和视 图。当您将 nib文件载入应用程序时, nib装载代码会将文件中的内容转化为应 用程序可以操作的真正对象。通过这个机制, nib文件省去了用代码创建那些 对象的工作。 Interface Builder是一个可视化的设计环境,您可以用它来创建 nib文件。您 可以将标准对象(比如 UIKit框架中提供的窗口和视图)和 Xcode工程中的定制 对象放到 nib文件中。在 Interface Builder中创建视图层次相当简单,只需要 对视图对象进行简单拖拽就可以了。您也可以通过查看器窗口来配置每个对象 的属性,以及通过创建对象间的连接来定义它们在运行时的关系。您所做的改 变最终都会作为 nib文件的一部分存储到磁盘上。 在运行时,当您需要 nib文件中包含的对象时,就将 nib文件装载到程序中。典 型情况下,装载 nib文件的时机是当用户界面发生变化和需要在屏幕上显示某 些新视图的时候。如果您的应用程序使用视图控制器,则视图控制器会自动处 理 nib文件的装载过程,当然,您也可以通过 NSBundle类的方法自行装载。 有关如何设计应用程序用户界面的更多信息,请参见 iPhone用户界面指南 。有 关如何创建 nib文件的信息则参见 Interface Builder用户指南 。 处理关键的应用程序任务 本部分将描述几个所有 iPhone应用程序都应该处理的任务。这些任务是整个应 用程序生命周期的一部分,因此也是将应用程序集成到 iPhone OS系统的重要 方面。在最坏的情况下,没有很好地处理其中的某些任务甚至可能会导致应用 程序被操作系统终止。 初始化和终止 在初始化和终止过程中, UIApplication类会向应用程序的 委托 发送恰当的 消息,使其执行必要的任务。虽然系统并不要求您的应用程序响应这些消息, 但是,几乎所有的 iPhone应用程序都应该处理这些消息。初始化是您为应用程 序准备用户界面及使其进入初始运行状态的阶段。类似地,在终止阶段,您应 该把未保存的数据和关键的应用程序状态写入磁盘。 由于一个 iPhone应用程序必须在其它应用程序启动之前退出,所以花在初始化 和终止阶段的执行时间要尽可能少。初始化阶段并不适合装载大的、却又不需 要马上使用的数据结构。在开始阶段,您的目标应该是尽可能快地显示应用程 序的用户界面,最好是使它进入最后一次退出的状态。如果您的应用程序在启 动过程中需要更多的时间来装载网络数据,或者执行一些可能很慢的任务,则 应该首先显示出用户界面并运行起来,然后在后台线程中执行速度慢的任务。 这样,您就有机会向用户显示进度条和其它反馈信息,指示应用程序正在装载 必要的数据,或者正在执行重要的任务。 表 1-5列举出 UIApplicationDelegate协议 定义的方法,您在应用程序委 托中需要实现这些协议方法,以处理初始化和终止的事务。表中还列出了您在 每个方法中应该执行的关键事务。 表 1-5  应用程序委托的责任 委托方法 描述 applicationDidFinis hLaunching: 使用这个方法来将应用程序恢复到上一个会 话的状态。您也可以在这个方法中执行应用 程序数据结构和用户界面的定制初始化。 applicationWillTerm inate: 使用这个方法来将未存数据或关键的应用程 序状态存入磁盘。您也可以在这个方法中执 行额外的清理工作,比如删除临时文件。 响应中断 除了 Home按键可以终止您的应用程序之外,系统也可以暂时中断您的应用程 序,使用户得以响应一些重要的事件。举例来说,应用程序可能被呼入的电 话、 SMS信息、日历警告、或者设备上的 Sleep按键所打断。按下 Home按键会 终止您的应用程序,而上述这些中断则只是暂时的。如果用户忽略这些中断, 您的应用程序可以象之前那样继续运行;然而,如果用户决定接电话或回应 SMS信息,系统就会开始终止您的程序。 图 1-6显示了在电话、 SMS信息、或者日历警告到来时发生的事件序列。紧接 在图后面的步骤说明更为详细地描述了事件序列的关键点,包括您在响应每个 事件时应该做的事项。这个序列并不反映当用户按下 Sleep/Wake按键时发生 的情景;该场景的事件序列在步骤说明之后的部分进行描述。 图 1-6  中断过程的事件流程 1. 系统检测到有电话、 SMS信息、或者日历警告发生。 2. 系统调用应用程序 委托 的 applicationWillResignActive:方 法,同时禁止将触摸事件发送给您的应用程序。 中断会导致应用程序暂 时失去控制权。如果控制权的丢失会影响程序的行为或导致不好的用户 体验,您就应该在委托方法中采取恰当的步骤进行规避。举例来说,如 果您的程序是个游戏,就应该暂停。您还应该禁用定时器、降低 OpenGL的帧率(如果正在使用 OpenGL的话),通常还应该使应用程序 进行休眠状态。在这休眠状态下,您的应用程序继续运行,但是不应该 做任何重要的工作。 3. 系统显示一个带有事件信息的警告窗口。用户可以选择忽略或响应该事 件。 4. 如果用户忽略该事件,系统就调用应用程序委托的 applicationDidBecomeActive:方法,并重新开始向应用程序传 递触摸事件。 您可以在这个方法中重新激活定时器、提高 OpenGL的帧 率、以及将应用程序从休眠状态唤醒。对于处于暂停状态的游戏,您应 该考虑使它停在当时的状态上,等待用户做好重新玩的准备。举例来 说,您可以显示一个警告窗口,而窗口中带有重新开始的控件。 5. 如果用户选择响应该事件(而不是忽略),则系统会调用应用程序委托 的 applicationWillTerminate:方法。您的应用程序应该正常终 止,保存所有必要的上下文信息,使应用程序在下一次启动的时候可以 回到同样的位置。 在您的应用程序终止之后,系统就开始启动负责中断 的应用程序。 根据用户对中断的不同响应,系统可能在中断结束之后再次启动您的应用程 序。举例来说,如果用户接听一个电话并在完成后挂断,则系统会重新启动您 的应用程序;如果用户在接听电话过程中回到 Home屏幕或启动另一个程序, 则系统就不再启动您的应用程序了。 重要提示: 当用户接听电话并在通话过程中重新启动您的应用程序时,状态条 的高度会变大,以反映当前用户正在通话中。类似地,当用户结束通话的时 候,状态条的高度会缩回正常尺寸。您的应用程序应该为状态条高度的变化做 好准备,并据此调整内容区域的尺寸。视图控制器会自动处理这个行为,然 而,如果您通过代码进行用户界面的布局,就需要在视图布局以及通过 layoutSubviews方法处理动态布局变化时考虑状态条的高度。 在运行您的应用程序时,如果用户按下设备的休眠 /唤醒按键,系统会调用应 用程序委托的 applicationWillResignActive:方法,停止触摸事件的 派发,然后使设备进入休眠状态。之后,当用户唤醒设备时,系统会调用应用 程序委托的 applicationDidBecomeActive:方法,并再次开始向应用程 序派发事件。如同处理其它中断一样,您应该使用这些方法来使应用程序进入 休眠状态(或者暂停游戏)及再次唤醒它们。在休眠时,您的应用程序应该尽 可能少用电力。 观察低内存警告 当系统向您的应用程序发送低内存警告时,您需要加以注意。当可用内存的数 量降低到安全阈值以下时, iPhone OS会通知最前面的应用程序。如果您的应 用程序收到这种警告,就必须尽可能多地释放内存,即释放不再需要的对象或 清理易于在稍后进行重建的缓存。 UIKit提供如下几种接收低内存警告的方法: ■ 在应用程序委托中实现 applicationDidReceiveMemoryWarning:方法。 ■ 在您的 UIViewController子类中实现 didReceiveMemoryWarning方法。 ■ 注册 UIApplicationDidReceiveMemoryWarningNotification 通告 。 一旦收到上述的任何警告,您的处理代码就应该立即响应,释放所有不需要的 内存。视图控制器应该清除当前离屏的视图对象,您的应用程序委托则应该释 放尽可能多的数据结构,或者通知其它应用程序对象释放其拥有的内存。 如果您的定制对象知道一些可清理的资源,则可以让该对象注册 UIApplicationDidReceiveMemoryWarningNotification通告, 并在通告处理器代码中直接释放那些资源。如果您通过少数对象来管理大多数 可清理的资源,且适合清理所有的这些资源,则同样可以让这些对象进行注 册。但是,如果您有很多可清理的对象,或者仅希望释放这些对象的一个子 集,则在您的应用程序委托中进行释放可能更好一些。 重要提示: 和系统的应用程序一样,您的应用程序总是需要处理低内存警告, 即使在测试过程中没有收到那些警告,也一样要进行处理。系统在处理请求时 会消耗少量的内存。在检测到低内存的情况时,系统会将低内存警告发送给所 有正在运行的进程(包括您的应用程序),而且可能终止某些后台程序(如果 必要的话),以减轻内存的压力。如果释放后内存仍然不够 —可能因为您的应 用程序发生泄露或消耗太多内存 —系统仍然可能会终止您的应用程序。 定制应用程序的行为 有几种方法可以对基本的应用程序行为进行定制,以提供您希望的用户体验。 本文的下面部分将描述一些必须在应用程序级别进行的定制。 以景观模式启动 为了配合 Home屏幕的方向, iPhone OS的应用程序通常以肖像模式启动。如 果您的应用程序既可以以景观模式运行,也可以以肖像模式运行,那么,一开 始应该总是以纵向模式启动,然后由视图控制器根据设备的方向旋转用户界 面。但是,如果您的应用程序只能以景观模式启动,则必须执行下面的步骤, 使它一开始就以景观模式启动。 ■ 在应用程序的 Info.plist文件中加入 UIInterfaceOrientation 键,并将它的值设置为景观模式。您可以将这个键值设置为 UIInterfaceOrientationLandscapeLeft 或者 UIInterfaceOrientationLandscapeRight。 ■ 以景观模式布局您的视图,并确保正确设置视图的自动尺寸调整选项。 ■ 重载视图控制器的 shouldAutorotateToInterfaceOrientation:方法,使它仅 在期望的景观方向时返回 YES,而在肖像方向时返回 NO。 重要提示: 上面描述的步骤假定您的应用程序使用视图控制器来管理视图层 次。视图控制器为处理方向改变和复杂的视图相关事件提供了大量的基础设 施。如果您的应用程序不使用视图控制器 —游戏和其它基于 OpenGL ES的应用 程序可能是这样的 —就必须根据需要旋转绘图表面(或者调整绘图命令),以 便将您的内容以景观模式展示出来。 UIInterfaceOrientation属性提示 iPhone OS在启动时应该配置应用程 序状态条(如果有的话)的方向,就象配置 视图控制器 管理下的视图方向一 样。在 iPhone OS 2.1及更高版本的系统中,视图控制器会尊重这个属性,将 视图的初始方向设置为指定的方向。使用这个属性相当于在 applicationDidFinishLaunching: 方法的一开始执行 UIApplication的 setStatusBarOrientation:animated:方法。 请注意: 在 v2.1之前的 iPhone OS系统中,如果要以景观模式启动基于视图控 制器的应用程序,需要在上文描述的所有步骤的基础上对应用程序根视图的转 换矩阵进行一个 90度的旋转。在 iPhone OS 2.1之前,视图控制器并不会根据 UIInterfaceOrientation键的值自动进行旋转,当然在 iPhone OS 2.1 及更高版本的系统中不需要这个步骤。 和其它应用程序进行通讯 如果一个应用程序支持一些已知类型的 URL,您就可以通过对应的 URL模式和 该程序进行通讯。然而,在大多数情况下, URL只是用于简单地启动一个应用 程序并显示一些和调用方有关的信息。举例来说,对于一个用于管理地址信息 的应用程序,您就可以在发送给它的 URL中包含一个 Maps程序可以处理的地 址,以便显示相应的位置。这个级别的通讯为用户创造一个集成度高得多的环 境,减少应用程序重新实现设备上其它程序已经实现的功能的必要性。 苹果内置支持 http、 mailto、 tel、和 sms这些 URL模式,还支持基于 http的、指向 Maps、 YouTube、和 iPod程序的 URL。应用程序也可以自己注 册定制的 URL模式。您的应用程序可以和其它应用程序通讯,具体方法是用正 确格式的内容创建一个 NSURL对象,然后将它传给共享 UIApplication对 象 openURL:方法。 openURL:方法会启动注册接收该 URL类型的应用程序, 并将 URL传给它。当用户最终退出该应用程序时,系统通常会重新启动您的应 用程序,但并不总是这样。系统会考虑用户在 URL处理程序中的动作及在用户 看来返回您的应用程序是否合理,然后做出决定。 下面的代码片断展示了一个程序如何请求另一个程序提供的服务(假定这个例 子中的 “todolist”是由应用程序注册的定制模式): NSURL *myURL = [NSURL URLWithString:@"todolist://www.acme.com?Quarterly %20Report#200806231300"]; [[UIApplication sharedApplication] openURL:myURL]; 重要提示: 如果您的 URL类型包含的模式和苹果定义的一样,则启动的是苹果 提供的程序,而不是您的程序。如果有多个第三方的应用程序注册处理同样的 URL模式,则该类型的 URL由哪个程序处理是没有定义的。 如果您的应用程序定义了自己的 URL模式,则应该实现对该模式进行处理的方 法,具体信息在 “实现定制的 URL模式 ”部分中进行描述。有关系统支持的 URL 处理,包括如何处理 URL的格式,请参见 苹果的 URL模式参考 。 实现定制的 URL模式 您可以为自己的应用程序注册包含定制模式的 URL类型。定制的 URL模式是第 三方应用程序和其它程序及系统进行交互的机制。通过定制的 URL模式,应用 程序可以将自己的服务提供给其它程序。 注册定制的 URL模式 在为您的应用程序注册 URL类型时,必须指定 CFBundleURLTypes属性的子 属性,我们已经在 “ 信息属性列表 ” 部分中介绍过这个属性了。 CFBundleURLTypes属性是应用程序的 Info.plist文件中的一个字典数 组,每个字典负责定义一个应用程序支持的 URL类型。表 1-6描述了 CFBundleURLTypes字典的键和值。 表 1-6  CFBundleURLTypes属性的键和值 键 值 CFBundleURLNa me 这是个字符串,表示 URL类型的抽象名。为了确保其 唯一性,建议您使用反向 DNS风格的标识,比如 com.acme.myscheme。 这里提供的 URL类型名是一个指向本地化字符串的 键,该字符串位于本地化语言包子目录中的 InfoPlist.strings文件中。本地化字符串是 人类可识别的 URL类型名称,用相应的语言来表示。 CFBundleURLSc hemes 这是个 URL模式的数组,表示归属于这个 URL类型的 URL。每个模式都是一个字符串。属于指定 URL类型 的 URL都带有它们的模式组件。 图 1-7显示了一个正在用内置的 Xcode编辑器编辑的 Info.plist文件。在这 个图中,左列中的 URL类型入口相当于您直接加入到 Info.plist文件的 CFBundleURLTypes键。类似地, “URL identifier”和 “URL Schemes”入口 相当于 CFBundleURLName和 CFBundleURLSchemes键。 图 1-7  在 Info.plist文件中定义一个定制的 URL模式 您在对 CFBundleURLTypes属性进行定义,从而注册带有定制模式的 URL类 型之后,可以通过下面的方式来进行测试: 1. 连编、安装、和运行您的应用程序。 2. 回到 Home屏幕,启动 Safari(在 iPhone仿真器上,在菜单上选择 Hardware > Home命令就可以回到 Home屏幕)。 3. 在 Safari的地址栏中,键入使用定制模式的 URL。 4. 确认您的应用程序是否启动,以及应用程序委托是否收到 application:handleOpenURL:消息。 处理 URL请求 应用程序委托在 application:handleOpenURL:方法中处理传递给应用 程序的 URL请求。如果您已经为自己的应用程序注册了定制的 URL模式,则务 必在 委托 中实现这个方法。 基于定制模式的 URL采用的协议是请求服务的应用程序能够理解的。 URL中包 含一些注册模式的应用程序期望得到的信息,这些信息是该程序在处理或响应 URL请求时需要的。传递给 application:handleOpenURL:方法的 NSURL对象表示的是 Cocoa Touch框架中的 URL。 NSURL遵循 RFC 1808规 范,该类中包含一些方法,用于返回 RFC 1808定义的各个 URL要素,包括用 户名、密码、请求、片断、和参数字符串。与您注册的定制模式相对应的 “协 议 ”可以使用这些 URL要素来传递各种信息。 在程序清单 1-2显示的 application:handleOpenURL:方法实现中,传 入的 URL对象在其请求和片断部分带有具体应用程序的信息。应用程序委托抽 出这些信息 —在这个例子中,是指一个 to-do任务的名称和到期日 —并根据这 些信息创建应用程序的模型对象。 程序清单 1-2  处理基于定制模式的 URL请求 - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *) url { if ([[url scheme] isEqualToString:@"todolist"]) { ToDoItem *item = [[ToDoItem alloc] init]; NSString *taskName = [url query]; if (!taskName || ![self isValidTaskString:taskName]) { // must have a task name [item release]; return NO; } taskName = [taskName stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; item.toDoTask = taskName; NSString *dateString = [url fragment]; if (!dateString || [dateString isEqualToString:@"today"]) { item.dateDue = [NSDate date]; } else { if (![self isValidDateString:dateString]) { [item release]; return NO; } // format: yyyymmddhhmm (24-hour clock) NSString *curStr = [dateString substringWithRange:NSMakeRange(0, 4)]; NSInteger yeardigit = [curStr integerValue]; curStr = [dateString substringWithRange:NSMakeRange(4, 2)]; NSInteger monthdigit = [curStr integerValue]; curStr = [dateString substringWithRange:NSMakeRange(6, 2)]; NSInteger daydigit = [curStr integerValue]; curStr = [dateString substringWithRange:NSMakeRange(8, 2)]; NSInteger hourdigit = [curStr integerValue]; curStr = [dateString substringWithRange:NSMakeRange(10, 2)]; NSInteger minutedigit = [curStr integerValue]; NSDateComponents *dateComps = [[NSDateComponents alloc] init]; [dateComps setYear:yeardigit]; [dateComps setMonth:monthdigit]; [dateComps setDay:daydigit]; [dateComps setHour:hourdigit]; [dateComps setMinute:minutedigit]; NSCalendar *calendar = [NSCalendar currentCalendar]; NSDate *itemDate = [calendar dateFromComponents:dateComps]; if (!itemDate) { [dateComps release]; [item release]; return NO; } item.dateDue = itemDate; [dateComps release]; } [(NSMutableArray *)self.list addObject:item]; [item release]; return YES; } return NO; } 请务必对传入的 URL输入进行验证。如果您希望了解如何避免 URL处理的相关 问题,请参见 安全编码指南 文档中的 验证输入 部分。如果要了解苹果定义的 URL模式,请参见 苹果的 URL模式参考 。 显示应用程序的偏好设置 如果您的应用程序通过偏好设置来控制其行为的不同方面,那么,以何种方式 向用户提供偏好设置就取决于它们是否为程序的必需部分。 ■ 如果偏好设置是程序使用的必需部分(且直接实现起来足够简单),那 么应该直接通过应用程序的定制界面来呈现。 ■ 如果偏好设置不是必需的,且要求相对复杂的界面,则应该通过系统的 Settings程序来呈现。 在确定一组偏好设置是否为程序的必需部分时,请考虑您为程序设计的使用模 式。如果您希望用户相对频繁地修改偏好设置,或者这些偏好设置对程序的行 为具有相对重要的影响,则可能就是必需部分。举例来说,游戏中的设置通常 都是玩游戏的必需部分,或者是用户希望快速改变的项目。然而,由于 Settings程序是一个独立的程序,所以只能用于处理用户不频繁访问的偏好设 置。 如果您选择在应用程序内进行偏好设置管理,则可以自行定义用户界面及编写 代码来实现。但是,如果您选择使用 Settings程序,则必须提供一个设置包 ( Settings Bundle)来进行管理。 设置包 是位于应用程序的程序包目录最顶层的定制资源,它是一个封装了的目 录,名字为 Settings.bundle。设置包中包含一些具有特别格式的数据文 件(及其支持资源),其作用是告诉 Settings程序如何显示您的偏好设置。这 些文件还告诉 Settings程序应该把结果值存储在偏好设置数据库的什么位置 上,以便应用程序随后可以通过 NSUserDefaults或 CFPreferences API 来进行访问。 如果您通过设置包来实现偏好设置管理,则还应该提供一个定制的图标。 Settings程序会在您的应用程序包的最顶层寻找名为 Icon-Settings.png 的图像文件,并将该图像显示在应用程序名称的边上。该文件应该是一个 29 x 29像素的 PNG图像文件。如果您没有在应用程序包的最顶层提供这个文件,则 Settings程序会缺省使用缩放后的应用程序图标( Icon.png)。 有关如何为应用程序创建设置包的更多信息,请参见 “应用程序的偏好设置 ”部 分。 关闭屏幕锁定 如果一个基于 iPhone OS的设备在某个特定时间段中没有接收到触摸事件,就 会关闭屏幕,并禁用触摸传感器。以这种方式锁定屏幕是省电的重要方法。因 此,除非您确实需要在应用程序中避免无意的行为,否则应该总是打开屏幕锁 定功能。举例来说,如果您的应用程序不接收屏幕事件,而是使用其它特性 (比如加速计)来进行输入,则可能需要禁用屏幕锁定功能。 将共享的 UIApplication对象的 idleTimerDisabled属性设置为 YES, 就可以禁止屏幕锁定。请务必在程序不需要禁止屏幕锁定功能时将该属性重置 为 NO。举例来说,您可能在用户玩游戏的时候禁止了屏幕锁定,但是,当用户 处于配置界面或没有处于游戏活跃状态时,应该重新打开这个功能。 国际化您的应用程序 理想情况下, iPhone应用程序显示给用户的文本、图像、和其它内容都应该本 地化为多种语言。比如,警告对话框中显示的文本就应该以用户偏好的语言显 示。为工程准备特定语言的本地化内容的过程就称为 国际化 。工程中需要本地 化的候选组件包括: ■ 代码生成的文本,包括与具体区域设置有关的日期、时间、和数字格 式。 ■ 静态文本 —比如装载到 web视图、用于显示应用程序帮助的 HTML文件。 ■ 图标(包括您的应用程序图标)及其它包含文本或具体文化意义的图 像。 ■ 包含发声语言的声音文件。 ■ Nib文件 。 通过 Settings程序,用户可以从 Language偏好设置视图(参见图 1-8)中选 择希望在用户界面上看到的语言。您可以访问 General设置,然后在 International组中找到该视图。 图 1-8  语言偏好设置视图 用户选择的语言和 程序包 中的一个子目录相关联,该子目录名由两个部分组 成,分别是 ISO 639-1定义的语言码和 .lproj后缀。您还可以对语言码进行 修改,使之包含具体的地区,方法是在后面(在下划线之后)加入 ISO 3166-1定义的区域指示符。举例来说,如果要指定美国英语的本地化资源, 程序包中的子目录应该命名为 en_US.lproj。我们约定,本地化语言子目录 称为 lproj文件夹。 请注意: 您也可以使用 ISO 639-2语言码,而不一定使用 ISO 639-1的定义。 有关语言和区域代码的信息,请参见 国际化编程主题 文档中的 “语言和地域的指 定 ”部分。 一个 lproj文件夹中包含所有指定语言(还可能包含指定地区)的本地化内 容。您可以用 NSBundle类或 CFBundleRef封装类型提供的工具来(在应用 程序的 lproj文件夹)定位当前选定语言的本地化资源。列表 1-3给出一个包 含英语( en)本地化内容的目录。 列表 1-3  本地化语言子目录的内容 en.lproj/ InfoPlist.strings Localizable.strings sign.png 这个例子目录有下面几个项目: ■ InfoPlist.strings文件,包含与 Info.plist文件中特定键(比 如 CFBundleDisplayName)相关联的本地化字符串值。比如,一个 英文名称为 Battleship的应用程序,其 CFBundleDisplayName键在 fr.lproj子目录的 InfoPlist.strings文件中有如下的入口: ■ CFBundleDisplayName = "Cuirassé"; ■ Localizable.strings文件,包含应用程序代码生成的字符串的本 地化版本。 ■ 本例子中的 sign.png,是一个包含本地化图像的文件。 为了本地化,我们需要国际化代码中的字符串,具体做法是用 NSLocalizedString宏来代替字符串。这个宏的定义如下: NSString *NSLocalizedString(NSString *key, NSString *comment); 第一个参数是一个唯一的键,指向给定 lproj 文件夹中 Localizable.strings文件里的一个本地化字符串;第二个参数是一个注 释,说明字符串如何使用,因此可以为翻译人员提供额外的上下文。举例来 说,假定您正在设置用户界面中一个标签( UILabel对象)的内容,则下面 的代码可以国际化该标签的文本: label.text = NSLocalizedString(@"City", @"Label for City text field"); 然后,您就可以为给定语言创建一个 Localizable.strings文件,并将它 加入到相应的 lproj文件夹中。对于上文例子中的键,该文件中应该有如下入 口: "City" = "Ville"; 请注意: 另一种方法是在代码中恰当的地方插入 NSLocalizedString调 用,然后运行 genstrings 命令行工具。该工具会生成一个 Localizable.strings文件的模板,包含每个需要翻译的键和注释。更多 有关 genstrings的信息,请参见 genstrings(1)的 man页面。 更多有关国际化的信息,请参见 国际化编程主题 。 性能和响应速度的调优 在应用程序开发过程的每一步,您都应该考虑自己所做的设计对应用程序总体 性能的影响。由于 iPhone和 iPod touch设备的移动本质, iPhone应用程序的 操作环境受到更多的限制。本文的下面部分将描述在开发过程中应该考虑哪些 因素。 不要阻塞主线程 您应该认真考虑在应用程序主线程上执行的任务。主线程是应用程序处理触摸 事件和其它用户输入的地方。为了确保应用程序总是可以响应用户,我们不应 该在主线程中执行运行时间很长或可能无限等待的任务,比如访问网络的任 务。相反,您应该将这些任务放在后台线程。一个推荐的方法是将每个任务都 封装在一个操作对象中,然后加入操作队列。当然,您也可以自己创建显式的 线程。 将任务转移到后台可以使您的主线程继续处理用户输入,这对于应用程序的启 动和退出尤其重要。在这些时候,系统期望您的应用程序及时响应事件。如果 应用程序的主线程在启动过程中被阻塞住了,系统甚至可能在启动完成之前将 它杀死;如果主线程在退出时被阻塞了,则应用程序可能来不及保存关键用户 数据就被杀死了。 更多有关如何使用操作对象和线程的信息,请参见 线程编程指南 。 有效地使用内存 由于 iPhone OS的虚存模型并不包含磁盘交换区空间,所以应用程序在更大程 度上受限于可供使用的内存。对内存的大量使用会严重降低系统的性能,可能 导致应用程序被终止。因此,在设计阶段,您应该把减少应用程序的内存开销 放在较高优先级上。 应用程序的可用内存和相对性能之间有直接的联系。可用内存越少,系统在处 理未来的内存请求时就越可能出问题。如果发生这种情况,系统总是先把代码 页和其它非易失性资源从内存中移除。但是,这可能只是暂时的修复,特别是 当系统在短时间后又再次需要那些资源的时候。相反,您需要尽可能使内存开 销最小化,并及时清除自己使用的内存。 本文的下面部分将就如何有效使用内存和在只有少量内存时如何反应方面提供 更多的指导。 减少应用程序的内存印迹 表 1-7列出一些如何减少应用程序总体内存印迹的技巧。在开始时将内存印迹 降低了,随后就可以有更多的空间用于需要操作的数据。 表 1-7  减少应用程序内存印迹的技巧 技巧 采取的措施 消除内存泄露 由于内存是 iPhone OS的关键资源,所以您的应用程序不 应该有任何的内存泄露。存在内存泄露意味着应用程序在 之后可能没有足够的内存。您可以用 Instruments程序来 跟踪代码中的泄露,该程序既可以用于仿真器,也可以用 于实际的设备。有关如何使用 Instruments的更多信息, 请参见 Instruments用户指南 。 使资源文件尽 可能小 文件驻留在磁盘中,但在使用时需要载入内存。 属性列表 文件和图像文件是通过简单的处理就可以节省空间的两种 资源类型。您可以通过 NSPropertyListSerialization类将属性列表文 件存储为二进制格式,从而减少它们的使用空间;对于图 像,可以将所有图像文件压缩得尽可能小( PNG图像是 iPhone应用程序的推荐图像格式,可以用 pngcrush工 具来进行压缩)。 使用 Core Data 或 SQLite来处理 大的数据集合 如果您的应用程序需要操作大量的结构化数据,请将它存 储在 Core Data的持久存储或 SQLite数据库,而不是使用 扁平文件。 Core Data和 SQLite都提供了管理大量数据的 有效方法,不需要将整个数据一次性地载入内存。 Core Data的支持是在 iPhone OS 3.0系统上引入的。 延缓装载资源 在真正需要资源文件之前,永远不应该进行装载。预先载 入资源文件表面看好象可以节省时间,但实际上会使应用 程序很快变慢。此外,如果您最终没有用到那些资源,预 先载入将只是浪费内存。 将程序连编为 Thumb格式 加入 -mthumb开关可以将代码的尺寸减少最多达 35%。 但是,对于具有大量浮点数运算的代码模块,请务必将这 个选项关闭,因为对那样的模块使用 Thumb反而会导致 性能的下降。 恰当地分配内存 iPhone应用程序使用 委托内存模式 ,因此,您必须显式保持和释放内存。表 1-8列出了一些在程序中分配内存的技巧。 表 1-8  分配内存的技巧 技巧 采取的措施 减少自动释放对 象的使用 减少自动释放对象的使用 为资源设置尺寸 限制 避免装载大的资源文件,如果有更小的文件可用的 话。请用适合于 iPhone OS设备的恰当尺寸图像来代替 高清晰度的图像。如果您必须使用大的资源文件,需 要考虑仅装载当前需要的部分。举例来说,您可以通 过 mmap和 munmap函数来将文件的一部分载入内存或 从内存卸载,而不是操作整个文件。有关如何将文件 映射到内存的更多信息,请参见 文件系统性能指南 。 避免无边界的问 题集 无边界的问题集可能需要计算任意大量的数据。如果 该集合需要的内存比当前系统能提供的还要多,则您 的应用程序可能无法进行计算。您的应用程序应该尽 可能避免处理这样的集合,而将它们转化为内存使用 极限已知的问题。 有关如何在 iPhone应用程序中 分配内存 及使用自动释放池的详细信息,请参见 Cocoa基本原理指南 文档的 Cocoa对象 部分。 浮点数学运算的考虑 iPhone–OS设备上的处理器有能力在硬件上处理浮点数计算。如果您目前的程 序使用基于软件的定点数数学库进行计算,则应该考虑对代码进行修改,转向 使用浮点数数学库。典型情况下,基于硬件的浮点数计算比对应的基于软件的 定点数计算快得多。 重要提示: 当然,如果您的代码确实广泛地使用浮点数计算,请记住不要使 用 -mthumb选项来编译代码。 Thumb选项可以减少代码模块的尺寸,但是也 会降低浮点计算代码的性能。 减少电力消耗 移动设备的电力消耗一直是个问题。 iPhone OS的电能管理系统保持电能的方 法是关闭当前未被使用的硬件功能。此外,要避免 CPU密集型和高图形帧率的 操作。您可以通过优化如下组件的使用来提高电池的寿命: ■ CPU ■ Wi-Fi和基带 (EDGE, 3G)无线信号 ■ Core Location框架 ■ 加速计 ■ 磁盘 您的优化目标应该是以尽可能有效的方式完成大多数的工作。您应该总是采用 Instruments和 Shark工具对应用程序的算法进行优化。但是,很重要的一点 是,即使最优化的算法也可能对设备的电池寿命造成负面的影响。因此,在写 代码的时候应该考虑如下的原则: ■ 避免需要轮询的工作,因为轮询会阻止 CPU进入休眠状态。您可以通过 NSRunLoop或者 NSTimer类来规划需要做的工作,而不是使用轮询。 ■ 尽一切可能使共享的 UIApplication对象的 idleTimerDisabled 属性值保持为 NO。当设备处于不活动状态一段时间后,空闲定时器会关 闭设备的屏幕。如果您的应用程序不需要设备屏幕保持打开状态,就让 系统将它关闭。如果关闭屏幕给您的应用程序的体验带来负面影响,则 需要通过修改代码来消除那些影响,而不是不必要地关闭空闲定时器。 ■ 尽可能将任务合并在一起,以便使空闲时间最大化。每隔一段时间就间 歇性地执行部分任务比一次性完成相同数量的所有任务开销更多的电 能。间歇性地执行任务会阻止系统在更长时间内无法关闭硬件。 ■ 避免过度访问磁盘。举例来说,如果您需要将状态信息保存在磁盘上, 则仅当该状态信息发生变化时才进行保存,或者尽可能将状态变化合并 保存,以避免短时间频繁进行磁盘写入操作。 ■ 不要使屏幕描画速度比实际需求更快。从电能消耗的角度看,描画的开 销很大。不要依赖硬件来压制应用程序的帧率,而是应该根据程序实际 需要的帧率来进行帧的描画。 ■ 如果你通过 UIAccelerometer类来接收常规的加速计事件,则当您不 再需要那些事件时,要禁止这些事件。类似地,请将事件传送的频率设 置为满足应用程序需要的最小值。更多信息请参见 “访问加速计事件 ”部 分。 您向网络传递的数据越多,就需要越多的电能来进行无线发射。事实上,访问 网络是您所能进行的最耗电的操作,您应该遵循下面的原则,使网络访问最小 化: ■ 仅在需要的时候连接外部网络,不要对服务器进行轮询。 ■ 当您需要连接网络时,请仅传递完成工作所需要的最少数据。请使用紧 凑的数据格式,不要包含可被简单忽略的额外数据。 ■ 尽可能快地以群发( in burst)方式传递数据包,而不是拉长数据传输的 时间。当系统检测到设备没有活动时,就会关闭 Wi-Fi和蜂窝无线信号。 您的应用程序以较长时间传输数据比以较短时间传输同样数量的数据要 消耗更多的电能。 ■ 尽可能通过 Wi-Fi无线信号连接网络。 Wi-Fi耗电比基带无线少,是推荐 的方式。 ■ 如果您通过 Core Location框架收集位置数据,则请尽可能快地禁止位置 更新,以及将位置过滤器和精度水平设置为恰当的值。 Core Location通 过可用的 GPS、蜂窝、和 Wi-Fi网络来确定用户的位置。虽然 Core Location已经努力使无线信号的使用最小化了,但是,设置恰当的精度 和过滤器的值可以使 Core Location在不需要位置服务的时候完全关闭硬 件。更多信息请参见 “获取用户的当前位置 ”部分。 代码的优化 和 iPhone OS一起推出的还有几个应用程序的优化工具。它们中的大部分都运 行在 Mac OS X上,适合于调整运行在仿真器上的代码的某些方面。举例来 说,您可以通过仿真器来消除内存泄露,确保总的内存开销尽可能小。借助这 些工具,您还可以排除代码中可能由低效算法或已知瓶颈引起的计算热点。 在仿真器上进行代码优化之后,还应该在设备上用 Instruments程序进行进一 步优化。在实际设备上运行代码是对其进行完全优化的唯一方式。因为仿真器 运行在 Mac OS X上,而运行 Mac OS X的系统具有更快的 CPU和更多的可用内 存,所以其性能通常比实际设备的性能好很多。在实际设备上用 Instruments 跟踪代码可能会发现额外的性能瓶颈,您需要进行优化。 更多有关 Instruments的使用信息,请参见 Instruments用户指南 。 窗口和视图 窗口和视图是为 iPhone应用程序构造用户界面的可视组件。窗口为内容显示提 供背景平台,而视图负责绝大部分的内容描画,并负责响应用户的交互。虽然 本章讨论的概念和窗口及视图都相关联,但是讨论过程更加关注视图,因为视 图对系统更为重要。 视图对 iPhone应用程序是如此的重要,以至于在一个章节中讨论视图的所有方 面是不可能的。本章将关注窗口和视图的基本属性、各个属性之间的关系、以 及在应用程序中如何创建和操作这些属性。本章不讨论视图如何响应触摸事件 或如何描画定制内容,有关那些主题的更多信息,请分别参见 “事件处理 ”和 “图 形和描画 ”部分。 什么是窗口和视图? 和 Mac OS X一样, iPhone OS通过窗口和视图在屏幕上展现图形内容。虽然窗 口和视图对象之间在两个平台上有很多相似性,但是具体到每个平台上,它们 的作用都有轻微的差别。 UIWindow的作用 和 Mac OS X的应用程序有所不同, iPhone应用程序通常只有一个 窗口 ,表示 为一个 UIWindow类的实例。您的应用程序在启动时创建这个窗口(或者从 nib文件 进行装载),并往窗口中加入一或多个视图,然后将它显示出来。窗 口显示出来之后,您很少需要再次引用它。 在 iPhone OS中,窗口对象并没有像关闭框或标题栏这样的视觉装饰,用户不 能直接对其进行关闭或其它操作。所有对窗口的操作都需要通过其编程接口来 实现。应用程序可以借助窗口对象来进行事件传递。窗口对象会持续跟踪当前 的第一响应者对象,并在 UIApplication对象提出请求时将事件传递它。 还有一件可能让有经验的 Mac OS X开发者觉得奇怪的事是 UIWindow类的继 承关系。在 Mac OS X中, NSWindow的父类是 NSResponder;而在 iPhone OS中, UIWindow的父类是 UIView。因此,窗口在 iPhone OS中也是一个视 图对象。不管其起源如何,您通常可以将 iPhone OS上的窗口和 Mac OS X的窗 口同样对待。也就是说,您通常不必直接操作 UIWindow对象中与视图有关的 属性变量 。 在创建应用程序窗口时,您应该总是将其初始的边框尺寸设置为整个屏幕的大 小。如果您的窗口是从 nib文件装载得到, Interface Builder并不允许创建比 屏幕尺寸小的窗口;然而,如果您的窗口是通过编程方式创建的,则必须在创 建时传入期望的边框矩形。除了屏幕矩形之外,没有理由传入其它边框矩形。 屏幕矩形可以通过 UIScreen对象来取得,具体代码如下所示: UIWindow* aWindow = [[[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]] autorelease]; 虽然 iPhone OS支持将一个窗口叠放在其它窗口的上方,但是您的应用程序永 远不应创建多个窗口。系统自身使用额外的窗口来显示系统状态条、重要的警 告、以及位于应用程序窗口上方的其它消息。如果您希望在自己的内容上方显 示警告,可以使用 UIKit提供的警告视图,而不应创建额外的窗口。 UIView是作用 视图 是 UIView类的实例,负责在屏幕上定义一个矩形区域。在 iPhone的应用 程序中,视图在展示用户界面及响应用户界面交互方面发挥关键作用。每个视 图对象都要负责渲染视图矩形区域中的内容,并响应该区域中发生的触碰事 件。这一双重行为意味着视图是应用程序与用户交互的重要机制。在一个基于 模型 -视图 -控制器 的应用程序中,视图对象明显属于视图部分。 除了显示内容和处理事件之外,视图还可以用于管理一或多个子视图。 子视图 是指嵌入到另一视图对象边框内部的视图对象,而被嵌入的视图则被称为父视 图或 超视图 。视图的这种布局方式被称为 视图层次 ,一个视图可以包含任意数 量的子视图,通过为子视图添加子视图的方式,视图可以实现任意深度的嵌 套。视图在视图层次中的组织方式决定了在屏幕上显示的内容,原因是子视图 总是被显示在其父视图的上方;这个组织方法还决定了视图如何响应事件和变 化。每个父视图都负责管理其直接的子视图,即根据需要调整它们的位置和尺 寸,以及响应它们没有处理的事件。 由于视图对象是应用程序和用户交互的主要途径,所以需要在很多方面发挥作 用,下面是其中的一小部分: ■ 描画和动画 1 视图负责对其所属的矩形区域进行描画。 2 某些视图 属性变量 可以以动画的形式过渡到新的值。 ■ 布局和子视图管理 1 视图管理着一个子视图列表。 2 视图定义了自身相对于其父视图的尺寸调整行为。 3 必要时,视图可以通过代码调整其子视图的尺寸和位置。 4 视图可以将其坐标系统下的点转换为其它视图或窗口坐标系统下的 点。 ■ 事件处理 1 视图可以接收触摸事件。 2 视图是响应者链的参与者。 在 iPhone应用程序中,视图和视图控制器紧密协作,管理若干方面的视图行 为。视图控制器的作用是处理视图的装载与卸载、处理由于设备旋转导致的界 面旋转,以及和用于构建复杂用户界面的高级导航对象进行交互。更多这方面 的信息请参见 “视图控制器的作用 ”部分。 本章的大部分内容都着眼于解释视图的这些作用,以及说明如何将您自己的定 制代码关联到现有的 UIView行为中。 UIKit的视图类 UIView类定义了视图的基本行为,但并不定义其视觉表示。相反, UIKit通过 其子类来为像文本框、按键、及工具条这样的标准界面元素定义具体的外观和 行为。图 2-1显示了所有 UIKit视图类的层次框图。除了 UIView和 UIControl类是例外,这个框图中的大多数视图都设计为可直接使用,或者 和 委托对象 结合使用。 图 2-1  视图的类层次 这个视图层次可以分为如下几个大类: ■ 容器 容器视图用于增强其它视图的功能,或者为视图内容提供额外的视 觉分隔。比如, UIScrollView类可以用于显示因内容太大而无法显示 在一个屏幕上的视图。 UITableView类是 UIScrollView类的子类, 用于管理数据列表。表格的行可以支持选择,所以通常也用于层次数据 的导航 —比如用于挖掘一组有层次结构的对象。 UIToolbar对象则是一个特殊类型的容器,用于为一或多个类似于按键 的项提供视觉分组。工具条通常出现在屏幕的底部。 Safari、 Mail、和 Photos程序都使用工具条来显示一些按键,这些按键代表经常使用的命 令。工具条可以一直显示,也可以根据应用程序的需要进行显示。 ■ 控件 控件用于创建大多数应用程序的用户界面。控件是一种特殊类型的 视图,继承自 UIControl超类,通常用于显示一个具体的值,并处理修 改这个值所需要的所有用户交互。控件通常使用标准的系统范式(比如 目标 -动作模式和委托模式)来通知应用程序发生了用户交互。控件包括 按键、文本框、滑块、和切换开关。 ■ 显示视图 控件和很多其它类型的视图都提供了交互行为,而另外一些视 图则只是用于简单地显示信息。具有这种行为的 UIKit类包括 UIImageView、 UILabel、 UIProgressView、 UIActivityIndicatorView。 ■ 文本和 web视图 文本和 web视图为应用程序提供更为高级的显示多行文 本的方法。 UITextView类支持在滚动区域内显示和编辑多行文本;而 UIWebView类则提供了显示 HTML内容的方法,通过这个类,您可以将 图形和高级的文本格式选项集成到应用程序中,并以定制的方式对内容 进行布局。 ■ 警告视图和动作表单 警告视图和动作表单用于即刻取得用户的注意。它 们向用户显示一条消息,同时还有一或多个可选的按键,用户通过这些 按键来响应消息。警告视图和动作表单的功能类似,但是外观和行为不 同。举例来说, UIAlertView类在屏幕上弹出一个蓝色的警告框,而 UIActionSheet类则从屏幕的底部滑出动作框。 ■ 导航视图 页签条和导航条和 视图控制器 结合使用,为用户提供从一个屏 幕到另一个屏幕的导航工具。在使用时,您通常不必直接创建 UITabBar和 UINavigationBar的项,而是通过恰当的控制器接口或 Interface Builder来对其进行配置。 ■ 窗口 窗口提供一个描画内容的表面,是所有其它视图的根容器。每个应 用程序通常都只有一个窗口。更多信息请参见 “UIWindow的作用 ”部分。 除了视图之外, UIKit还提供了视图控制器,用于管理这些对象。更多信息请参 见 “视图控制器的作用 ”部分。 视图控制器的作用 运行在 iPhone OS上的应用程序在如何组织内容和如何将内容呈现给用户方面 有很多选择。含有很多内容的应用程序可以将内容分为多个屏幕。在运行时, 每个屏幕的背后都是一组视图对象,负责显示该屏幕的数据。一个屏幕的视图 后面是一个 视图控制器 其作用是管理那些视图上显示的数据,并协调它们和应 用程序其它部分的关系。 UIViewController类负责创建其管理的视图及在低内存时将它们从内容中 移出。视图控制器还为某些标准的系统行为提供自动响应。比如,在响应设备 方向变化时,如果应用程序支持该方向,视图控制器可以对其管理的视图进行 尺寸调整,使其适应新的方向。您也可以通过视图控制器来将新的视图以模式 框的方式显示在当前视图的上方。 除了基础的 UIViewController类之外, UIKit还包含很多高级子类,用于 处理平台共有的某些高级接口。特别需要提到的是,导航控制器用于显示多屏 具有一定层次结构的内容;而页签条控制器则支持用户在一组不同的屏幕之间 切换,每个屏幕都代表应用程序的一种不同的操作模式。 有关如何通过视图控制器管理用户界面上视图的更多信息,请参见 iPhone OS 的视图控制器编程指南 。 视图架构和几何属性 由于视图是 iPhone应用程序的焦点对象,所以对视图与系统其它部分的交互机 制有所了解是很重要的。 UIKit中的标准视图类为应用程序免费提供相当数量的 行为,还提供了一些定义良好的集成点,您可以通过这些集成点来对标准行为 进行定制,完成应用程序需要做的工作。 本文的下面部分将解释视图的标准行为,并说明哪些地方可以集成您的定制代 码。如果需要特定类的集成点信息,请参见该类的参考文档。您可以从 UIKit框 架参考 中取得所有类参考文档的列表。 视图交互模型 任何时候,当用户和您的程序界面进行交互、或者您的代码以编程的方式进行 某些修改时, UIKit内部都会发生一个复杂的事件序列。在事件序列的一些特定 的点上, UIKit会调用您的视图类,使它们有机会代表应用程序进行事件响应。 理解这些调用点是很重要的,有助于理解您的视图对象和系统在哪里进行结 合。图 2-2显示了从用户触击屏幕到图形系统更新屏幕内容这一过程的基本事 件序列。以编程方式触发事件的基本步骤与此相同,只是没有最初的用户交 互。 图 2-2  UIKit和您的视图对象之间的交互 下面的步骤说明进一步刨析了 图 2-2中的事件序列,解释了序列的每个阶段都 发生了什么,以及应用程序可能如何进行响应。 1. 用户触击屏幕。 2. 硬件将触击事件报告给 UIKit框架。 3. UIKit框架将触击信息封装为一个 UIEvent对象,并派发给恰当的视图 (有关 UIKit如何将事件递送给您的视图的详细解释,请参见 “事件的传 递 ”部分)。 4. 视图的事件处理方法可以通过下面的方式来响应事件: ■ 调整视图或其子视图的 属性变量 (边框、边界、透明度等)。 ■ 将视图(或其子视图)标识为需要修改布局。 ■ 将视图(或其子视图)标识为布局需要重画。 ■ 将数据发生的变化通报给 控制器 。 5. 当然,上述的哪些事情需要做及调用什么方法来完成是由视图来决定 的。 6. 如果视图被标识为需要重新布局, UIKit 就调用视图的 layoutSubviews方法。 您可以在自己的定制视图中重载这个方法, 以便调整子视图的尺寸和位置。举例来说,如果一个视图具有很大的滚 动区域,就需要使用几个子视图来 “平铺 ”,而不是创建一个内存很可能装 不下的大视图。在这个方法的实现中,视图可以隐藏所有不需显示在屏 幕上的子视图,或者在重新定位之后将它们用于显示新的内容。作为这 个过程的一部分,视图也可以将用于 “平铺 ”的子视图标识为需要重画。 7. 如果视图的任何部分被标识为需要重画, UIKit就调用该视图的 drawRect:方法。 UIKit只对那些需要重画的视图调用这个方法。在这 个方法的实现中,所有视图都应该尽可能快地重画指定的区域,且都应 该只重画自己的内容,不应该描画子视图的内容。在这个调用点上,视 图不应该尝试进一步改变其属性或布局。 8. 所有更新过的视图都和其它可视内容进行合成,然后发送给图形硬件进 行显示。 9. 图形硬件将渲染完成的内容转移到屏幕。 请注意: 上述的更新模型主要适用于采纳内置视图和描画技术的应用程序。如 果您的应用程序使用 OpenGL ES来描画内容,则通常要配置一个全屏的视图, 然后直接在 OpenGL的图形上下文中进行描画。您的视图仍然需要处理触碰事 件,但不需要对子视图进行布局或者实现 drawRect:方法。有关 OpenGL ES 的更多信息,请参见 “用 OpenGL ES进行描画 ”部分。 基于上述的步骤说明可以看出, UIKit为您自己定制的视图提供如下主要的结合 点: 1. 下面这些事件处理方法: ■ touchesBegan:withEvent: ■ touchesMoved:withEvent: ■ touchesEnded:withEvent: ■ touchesCancelled:withEvent: 2. layoutSubviews方法 3. drawRect:方法 大多数定制视图通过实现这些方法来得到自己期望的行为。您可能不需要重载 所有方法,举例来说,如果您实现的视图是固定尺寸的,则可能不需要重载 layoutSubviews方法。类似地,如果您实现的视图只是显示简单的内容, 比如文本或图像,则通常可以通过简单地嵌入 UIImageView和 UILabel对 象作为子视图来避免描画。 重要的是要记住,这些是主要的结合点,但不是全部。 UIView类中有几个方 法的设计目的就是让子类重载的。您可以通过查阅 UIView类参考 中的描述来了 解哪些方法可以被重载。 视图渲染架构 虽然您通过视图来表示屏幕上的内容,但是 UIView类自身的很多基础行为却 严重依赖于另一个对象。 UIKit中每个视图对象的背后都有一个 Core Animation层对象,它是一个 CALayer类的实例,该类为视图内容的布局和 渲染、以及合成和动画提供基础性的支持。 和 Mac OS X(在这个平台上 Core Animation支持是可选的)不同的是, iPhone OS将 Core Animation集成到视图渲染实现的核心。虽然 Core Animation发挥核心作用,但是 UIKit在 Core Animation上面提供一个透明的 接口层,使编程体验更为流畅。这个透明的接口使开发者在大多数情况下不必 直接访问 Core Animation的层,而是通过 UIView的方法和 属性声明 取得类 似的行为。然而,当 UIView类没有提供您需要的接口时, Core Animation 就变得重要了,在那种情况下,您可以深入到 Core Animation层,在应用程 序中实现一些复杂的渲染。 本文的下面部分将介绍 Core Animation技术,描述它通过 UIView类为您提 供的一些功能。有关如何使用 Core Animation进行高级渲染的更多信息,请 参见 Core Animation编程指南 。 Core Animation基础 Core Animation利用了硬件加速和架构上的优化来实现快速渲染和实时动 画。当视图的 drawRect:方法首次被调用时,层会将描画的结果捕捉到一个 位图中,并在随后的重画中尽可能使用这个缓存的位图,以避免调用开销很大 的 drawRect:方法。这个过程使 Core Animation得以优化合成操作,取得期 望的性能。 Core Animation把和视图对象相关联的层存储在一个被称为 层树 的层次结构 中。和视图一样,层树中的每个层都只有一个父亲,但可以嵌入任意数量的子 层。缺省情况下,层树中对象的组织方式和视图在视图层次中的组织方式完全 一样。但是,您可以在层树中添加层,而不同时添加相应的视图。当您希望实 现某种特殊的视觉效果、而又不需要在视图上保持这种效果时,就可能需要这 种技术。 实际上,层对象是 iPhone OS渲染和布局系统的推动力,大多数视图属性实际 上是其层对象属性的一个很薄的封装。当您(直接使用 CALayer对象)修改 层树上层对象的属性时,您所做的改变会立即反映在层对象上。但是,如果该 变化触发了相应的动画,则可能不会立即反映在屏幕上,而是必须随着时间的 变化以动画的形式表现在屏幕上。为了管理这种类型的动画, Core Animation额外维护两组层对象,我们称之为 表示树 和 渲染树 。 表示树反映的是层在展示给用户时的当前状态。假定您对层值的变化实行动 画,则在动画开始时,表示层反映的是老的值;随着动画的进行, Core Animation会根据动画的当前帧来更新表示树层的值;然后,渲染树就和表示 树一起,将变化渲染在屏幕上。由于渲染树运行在单独的进程或线程上,所以 它所做的工作并不影响应用程序的主运行循环。虽然层树和表示树都是公开 的,但是渲染树的接口是私有。 在视图后面设置层对象对描画代码的性能有很多重要的影响。使用层的好处在 于视图的大多数几何变化都不需要重画。举例来说,改变视图的位置和尺寸并 需要重画视图的内容,只需简单地重用层缓存的位图就可以了。对缓存的内容 实行动画比每次都重画内容要有效得多。 使用层的缺点在于层是额外的缓存数据,会增加应用程序的内存压力。如果您 的应用程序创建太多的视图,或者创建多个很大的视图,则可能很快就会出现 内存不够用的情形。您不用担心在应用程序中使用视图,但是,如果有现成的 视图可以重用,就不要创建新的视图对象。换句话说,您应该设法使内存中同 时存在的视图对象数量最小。 有关 Core Animation的进一步概述、对象树、以及如何创建动画,请参见 Core Animation编程指南 。 改变视图的层 在 iPhone OS系统中,由于视图必须有一个与之关联的层对象,所以 UIView 类在初始化时会自动创建相应的层。您可以通过视图的 layer属性访问这个 层,但是不能在视图创建完成后改变层对象。 如果您希望视图使用不同类型的层,必须重载其 layerClass类方法 ,并在 该方法中返回您希望使用的层对象。使用不同层类的最常见理由是为了实现一 个基于 OpenGL的应用程序。为了使用 OpenGL描画命令,视图下面的层必须 是 CAEAGLLayer类的实例,这种类型的层可以和 OpenGL渲染调用进行交 互,最终在屏幕上显示期望的内容。 重要提示: 您永远不应修改视图层的 delegate属性,该属性用于存储一个指 向视图的指针,应该被认为是私有的。类似地,由于一个视图只能作为一个层 的 委托 ,所以您必须避免将它作为其它层对象的委托,否则会导致应用程序崩 溃。 动画支持 iPhone OS的每个视图后面都有一个层对象,这样做的好处之一是使视图内容 更加易于实现动画。请记住,动画并不一定是为了在视觉上吸引眼球,它可以 将应用程序界面变化的上下文呈现给用户。举例来说,当您在屏幕转移过程中 使用过渡时,过渡本身就向用户指示屏幕之间的联系。系统自动支持了很多经 常使用的动画,但您也可以为界面上的其它部分创建动画。 UIView类的很多属性都被设计为可动画的( animatable)。 可动画的 属性是 指当属性从一个值变为另一个值的时候,可以半自动地支持动画。您仍然必须 告诉 UIKit希望执行什么类型的动画,但是动画一旦开始, Core Animation就 会全权负责。 UIView对象中支持动画的属性有如下几个: 1 frame 2 bounds 3 center 4 transform 5 alpha 虽然其它的视图属性不直接支持动画,但是您可以为其中的一部分显式创建动 画。显式动画要求您做很多管理动画和渲染内容的工作,通过使用 Core Animation提供的基础设施,这些工作仍然可以得到良好的性能。 有关如何通过 UIView类创建动画的更多信息,请参见 “实现视图动画 ”部分; 有关如何创建显式动画的更多信息,则请参见 Core Animation编程指南 。 视图坐标系统 UIKit中的坐标是基于这样的坐标系统:以左上角为坐标的原点,原点向下和向 右为坐标轴正向。坐标值由浮点数来表示,内容的布局和定位因此具有更高的 精度,还可以支持与分辨率无关的特性。 图 2-3显示了这个相对于屏幕的坐标 系统,这个坐标系统同时也用于 UIWindow和 UIView类。视图坐标系统的方 向和 Quartz及 Mac OS X使用的缺省方向不同,选择这个特殊的方向是为了使 布局用户界面上的控件及内容更加容易。 图 2-3  视图坐标系统 您在编写界面代码时,需要知道当前起作用的坐标系统。每个窗口和视图对象 都维护一个自己本地的坐标系统。视图中发生的所有描画都是相对于视图本地 的坐标系统。但是,每个视图的边框矩形都是通过其父视图的坐标系统来指 定,而事件对象携带的坐标信息则是相对于应用程序窗口的坐标系统。为了方 便, UIWindow和 UIView类都提供了一些方法,用于在不同对象之间进行坐 标系统的转换。 虽然 Quartz使用的坐标系统不以左上角为原点,但是对于很多 Quartz调用来 说,这并不是问题。在调用视图的 drawRect:方法之前, UIKit会自动对描画 环境进行配置,使左上角成为坐标系统的原点,在这个环境中发生的 Quartz调 用都可以正确地在视图中描画。您唯一需要考虑不同坐标系统之间差别的场合 是当您自行通过 Quartz建立描画环境的时候。 更多有关坐标系统、 Quartz、和描画的一般信息,请参见 “图形和描画 ”部分。 边框、边界、和中心的关系 视图对象通过 frame、 bounds、和 center属性声明 来跟踪自己的大小和位 置。 frame属性包含一个矩形,即 边框矩形 ,用于指定视图相对于其父视图坐 标系统的位置和大小。 bounds属性也包含一个矩形,即 边界矩形 ,负责定义 视图相对于本地坐标系统的位置和大小。虽然边界矩形的原点通常被设置为 (0, 0),但这并不是必须的。 center属性包含边框矩形的 中心点 。 在代码中,您可以将 frame、 bounds、和 center属性用于不同的目的。边 界矩形代表视图本地的坐标系统,因此,在描画和事件处理代码中,经常借助 它来取得视图中发生事件或需要更新的位置。中心点代表视图的中心,改变中 心点一直是移动视图位置的最好方法。边框矩形是一个通过 bounds和 center属性计算得到的便利值,只有当视图的变换属性被设置恒等变换时, 边框矩形才是有效的。 图 2-4显示了边框矩形和边界矩形之间的关系。右边的整个图像是从视图的 (0, 0)开始描画的,但是由于边界的大小和整个图像的尺寸不相匹配,所以位于边 界矩形之外的图像部分被自动裁剪。在视图和它的父视图进行合成的时候,视 图在其父视图中的位置是由视图边框矩形的原点决定的。在这个例子中,该原 点是 (5, 5)。结果,视图的内容就相对于父视图的原点向下向右移动相应的尺 寸。 图 2-4  视图的边框和边界之间的关系 如果没有经过变换,视图的位置和大小就由上述三个互相关联的属性决定的。 当您在代码中通过 initWithFrame:方法创建一个视图对象时,其 frame属 性就会被设置。该方法同时也将 bounds矩形的原点初始化为 (0.0, 0.0),大小 则和视图的边框相同。然后 center属性会被设置为边框的中心点。 虽然您可以分别设置这些属性的值,但是设置其中的一个属性会引起其它属性 的改变,具体关系如下: ■ 当您设置 frame属性时, bounds属性的大小会被设置为与 frame属性 的大小相匹配的值, center属性也会被调整为与新的边框中心点相匹 配的值。 ■ 当您设置 center属性时, frame的原点也会随之改变。 ■ 当您设置 bounds矩形的大小时, frame矩形的大小也会随之改变。 您可以改变 bounds的原点而不影响其它两个属性。当您这样做时,视图会显 示您标识的图形部分。在 图 2-4中,边界的原点被设置为 (0.0, 0.0)。在图 2-5 中,该原点被移动到 (8.0, 24.0)。结果,显示出来的是视图图像的不同部分。 但是,由于边框矩形并没有改变,新的内容在父视图中的位置和之前是一样 的。 图 2-5  改变视图的边界 请注意: 缺省情况下,视图的边框并不会被父视图的边框裁剪。如果您希望让 一个视图裁剪其子视图,需要将其 clipsToBounds属性设置为 YES。 坐标系统变换 在视图的 drawRect:方法中常常借助坐标系统变换来进行描画。而在 iPhone OS系统中,您还可以用它来实现视图的某些视觉效果。举例来说, UIView类 中包含一个 transform属性声明 ,您可以通过它来对整个视图实行各种类型 的平移、比例缩放、和变焦缩放效果。缺省情况下,这个属性的值是一个恒等 变换,不会改变视图的外观。在加入变换之前,首先要得到该属性中存储的 CGAffineTransform结构,用相应的 Core Graphics函数实行变换,然后 再将修改后的变换结构重新赋值给视图的 transform属性。 请注意: 当您将变换应用到视图时,所有执行的变换都是相对于视图的中心 点。 平移一个视图会使其所有的子视图和视图本身的内容一起移动。由于子视图的 坐标系统是继承并建立在这些变化的基础上的,所以比例缩放也会影响子视图 的描画。有关如何控制视图内容缩放的更多信息,请参见 “内容模式和比例缩 放 ”部分。 重要提示: 如果 transform属性的值不是恒等变换,则 frame属性的值就是 未定义的,必须被忽略。在设置变换属性之后,请使用 bounds和 center属 性来获取视图的位置和大小。 有关如何在 drawRect:方法中使用变换的信息,请参见 “坐标和坐标变换 ”部 分;有关用于修改 CGAffineTransform 结构的函数,则请参见 CGAfneTransform参考 。 内容模式与比例缩放 当您改变视图的边界,或者将一个比例因子应用到视图的 transform属性声 明 时,边框矩形会发生等量的变化。根据内容模式的不同,视图的内容也可能 被缩放或重新定位,以反映上述的变化。视图的 contentMode属性决定了边 界变化和缩放操作作用到视图上产生的效果。缺省情况下,这个属性的值被设 置为 UIViewContentModeScaleToFill,意味着视图内容总是被缩放, 以适应新的边框尺寸。作为例子,图 2-6显示了当视图的水平缩放因子放大一 倍时产生的效果。 图 2-6 使用 scale-to-fill内容模式缩放视图 视图内容的缩放仅在首次显示视图的时候发生,渲染后的内容会被缓存在视图 下面的层上。当边界或缩放因子发生变化时, UIKit并不强制视图进行重画,而 是根据其内容模式决定如何显示缓存的内容。图 2-7比较了在不同的内容模式 下,改变视图边界或应用不同的比例缩放因子时产生的结果。 图 2-7  内容模式比较 对视图应用一个比例缩放因子总是会使其内容发生缩放,而边界的改变在某些 内容模式下则不会发生同样的结果。不同的 UIViewContentMode常量(比 如 UIViewContentModeTop和 UIViewContentModeBottomRight) 可以使当前的内容在视图的不同角落或沿着视图的不同边界显示,还有一种模 式可以将内容显示在视图的中心。在这些模式的作用下,改变边界矩形只会简 单地将现有的视图内容移动到新的边界矩形中对应的位置上。 当您希望在应用程序中实现尺寸可调整的控件时,请务必考虑使用内容模式。 这样做可以避免控件的外观发生变形,以及避免编写定制的描画代码。按键和 分段控件( segmented control)特别适合基于内容模式的描画。它们通常使 用几个图像来创建控件外观。除了有两个固定尺寸的盖帽图像之外,按键可以 通过一个可伸展的、宽度只有一个像素的中心图像来实现水平方向的尺寸调 整。它将每个图像显示在自己的图像视图中,而将可伸展的中间图像的内容模 式设置为 UIViewContentModeScaleToFill,使得在尺寸调整时两端的 外观不会变形。更为重要的是,每个图像视图的关联图像都可以由 Core Animation来缓存,因此不需要编写描画代码就可以支持动画,从而使大大提 高了性能。 内容模式通常有助于避免视图内容的描画,但是当您希望对缩放和尺寸调整过 程中的视图外观进行特别的控制时,也可以使用 UIViewContentModeRedraw模式。将视图的内容模式设置为这个值可以 强制 Core Animation使视图的内容失效,并调用视图的 drawRect:方法,而 不是自动进行缩放或尺寸调整。 自动尺寸调整行为 当您改变视图的边框矩形时,其内嵌子视图的位置和尺寸往往也需要改变,以 适应原始视图的新尺寸。如果视图的 autoresizesSubviews属性声明 被设 置为 YES,则其子视图会根据 autoresizingMask属性的值自动进行尺寸调 整。简单配置一下视图的自动尺寸调整掩码常常就能使应用程序得到合适的行 为;否则,应用程序就必须通过重载 layoutSubviews方法来提供自己的实 现。 设置视图的自动尺寸调整行为的方法是通过位 OR操作符将期望的自动尺寸调 整常量连结起来,并将结果赋值给视图的 autoresizingMask属性。表 2-1 列举了自动尺寸调整常量,并描述这些常量如何影响给定视图的尺寸和位置。 举例来说,如果要使一个视图和其父视图左下角的相对位置保持不变,可以加 入 UIViewAutoresizingFlexibleRightMargin 和 UIViewAutoresizingFlexibleTopMargin常量,并将结果赋值给 autoresizingMask属性。当同一个轴向有多个部分被设置为可变时,尺寸 调整的裕量会被平均分配到各个部分上。 表 2-1  自动尺寸调整掩码常量 自动尺寸调整掩码 描述 UIViewAutoresizi ngNone 这个常量如果被设置,视图将不进行自动尺寸调 整。 UIViewAutoresizi ngFlexibleHeight 这个常量如果被设置,视图的高度将和父视图的 高度一起成比例变化。否则,视图的高度将保持 不变。 UIViewAutoresizi ngFlexibleWidth 这个常量如果被设置,视图的宽度将和父视图的 宽度一起成比例变化。否则,视图的宽度将保持 不变。 UIViewAutoresizi ngFlexibleLeftMa rgin 这个常量如果被设置,视图的左边界将随着父视 图宽度的变化而按比例进行调整。否则,视图和 其父视图的左边界的相对位置将保持不变。 UIViewAutoresizi ngFlexibleRightM argin 这个常量如果被设置,视图的右边界将随着父视 图宽度的变化而按比例进行调整。否则,视图和 其父视图的右边界的相对位置将保持不变。 UIViewAutoresizi ngFlexibleBottom Margin 这个常量如果被设置,视图的底边界将随着父视 图高度的变化而按比例进行调整。否则,视图和 其父视图的底边界的相对位置将保持不变。 UIViewAutoresizi ngFlexibleTopMar gin 这个常量如果被设置,视图的上边界将随着父视 图高度的变化而按比例进行调整。否则,视图和 其父视图的上边界的相对位置将保持不变。 图 2-8为这些常量值的位置提供了一个图形表示。如果这些常量之一被省略, 则视图在相应方向上的布局就被固定;如果某个常量被包含在掩码中,在该方 向的视图布局就就灵活的。 图 2-8  视图的自动尺寸调整掩码常量 如果您通过 Interface Builder配置视图,则可以用 Size查看器的 Autosizing控 制来设置每个视图的自动尺寸调整行为。上图中的灵活宽度及高度常量和 Interface Builder中位于同样位置的弹簧具有同样的行为,但是空白常量的行 为则是正好相反。换句话说,如果要将灵活右空白的自动尺寸调整行为应用到 Interface Builder的某个视图,必须使相应方向空间的 Autosizing控制为空, 而不是放置一个支柱。幸运的是, Interface Builder通过动画显示了您的修改 对视图自动尺寸调整行为的影响。 如果视图的 autoresizesSubviews属性被设置为 NO,则该视图的直接子 视图的所有自动尺寸调整行为将被忽略。类似地,如果一个子视图的自动尺寸 调整掩码被设置为 UIViewAutoresizingNone,则该子视图的尺寸将不会 被调整,因而其直接子视图的尺寸也不会被调整。 请注意: 为了使自动尺寸调整的行为正确,视图的 transform属性必须设置 为恒等变换;其它变换下的尺寸自动调整行为是未定义的。 自动尺寸调整行为可以适合一些布局的要求,但是如果您希望更多地控制视图 的布局,可以在适当的视图类中重载 layoutSubviews方法。有关视图布局 管理的更多信息,请参见 “响应布局的变化 ”部分。 创建和管理视图层次 管理用户界面的视图层次是开发应用程序用户界面的关键部分。视图的组织方 式不仅定义了应用程序的视觉外观,而且还定义了应用程序如何响应变化。视 图层次中的父 -子关系可以帮助我们定义应用程序中负责处理触摸事件的对象 链。当用户旋转设备时,父 -子关系也有助于定义每个视图的尺寸和位置是如 何随着界面方向的变化而变化的。 图 2-9显示了一个简单的例子,说明如何通过视图的分层来创建期望的视觉效 果。在 Clock程序中,页签条和导航条视图,以及定制视图混合在一起,实现 了整个界面。 图 2-9  Clock程序的视图层 如果您探究 Clock程序中视图之间的关系,就会发现它们很像 “改变视图的 层 ”部分中显示的关系,窗口对象是应用程序的页签条、导航条、和定制视图的 根视图。 图 2-10  Clock程序的视图层次 在 iPhone应用程序的开发过程中,有几种建立视图层次的方法,包括基于 Interface Builder的可视化方法和通过代码编程的方法。本文的下面部分将向 您介绍如何装配视图层次,以及如何在建立视图层次之后寻找其中的视图,还 有如何在不同的视图坐标系统之间进行转换。 创建一个视图对象 创建 视图 对象的最简单方法是使用 Interface Builder进行制作,然后将视图对 象从作成的 nib文件 载入内存。在 Interface Builder的图形环境中,您可以将 新的视图从库中拖出,然后放到窗口或另一个视图中,以快速建立需要的视图 层次。 Interface Builder使用的是活的视图对象,因此,当您用这个图形环境 构建用户界面时,所看到的就是运行时装载的外观,而且不需要为视图层次中 的每个视图编写单调乏味的 内存分配和初始化 代码。 如果您不喜欢 Interface Builder和 nib文件,也可以通过代码来创建视图。创 建一个新的视图对象时,需要为其分配内存,并向该对象发送一个 initWithFrame:消息,以对其进行初始化。举例来说,如果您要创建一个 新的 UIView类的实例作为其它视图的容器,则可以使用下面的代码: CGRect viewRect = CGRectMake(0, 0, 100, 100); UIView* myView = [[UIView alloc] initWithFrame:viewRect]; 请注意: 虽然所有系统提供的视图对象都支持 initWithFrame:消息,但是 其中的一部分可能有自己偏好的 初始化 方法,您应该使用那些方法。有关定制 初始化方法的更多信息,请参见相应的类参考文档。 您在视图初始化时指定的边框矩形代表该视图相对于未来父视图的位置和大 小。在将视图显示于屏幕上之前,您需要将它加入到窗口或其它视图中。在这 个时候, UIKit会根据您指定的边框矩形将视图放置到其父视图的相应位置中。 有关如何将视图添加到视图层次的信息,请参见 “添加和移除子视图 ”部分。 添加和移除子视图 Interface Builder是建立视图层次的最便利工具,因为它可以让您看到视图在 运行时的外观。在界面制作完成后,它将视图对象及其层次关系保存在 nib文 件中。在运行时,系统会按照 nib文件的内容为应用程序重新创建那些对象和 关系。当一个 nib文件被装载时,系统会自动调用重建视图层次所需要的 UIView方法。 如果您不喜欢通过 Interface Builder和 nib文件来创建视图层次,则可以通过 代码来 创建 。如果一个视图必须具有某些子视图才能工作,则应该在其 initWithFrame:方法中进行对其创建,以确保子视图可以和视图一起被显 示和 初始化 。如果子视图是应用程序设计的一部分(而不是视图工作必需 的),则应该在视图的初始化代码之外进行创建。在 iPhone程序中,有两个地 方最常用于创建视图和子视图,它们是应用程序 委托 对象的 applicationDidFinishLaunching:方法和 视图控制器 的 loadView 方法。 您可以通过下面的方法来操作视图层次中的视图对象: ■ 调用父视图的 addSubview:方法来添加视图,该方法将一个视图添加 到子视图列表的最后。 ■ 调用父视图的 insertSubview:...方法可以在父视图的子视图列表 中间插入视图。 ■ 调用父视图的 bringSubviewToFront: 、 sendSubviewToBack: 、或 exchangeSubviewAtIndex:withSubviewAtIndex:方法可以 对父视图的子视图进行重新排序。使用这些方法比从父视图中移除子视 图并再次插入要快一些。 ■ 调用子视图(而不是父视图)的 removeFromSuperview方法可以将 子视图从父视图中移除。 在添加子视图时, UIKit会根据子视图的当前边框矩形确定其在父视图中的初始 位置。您可以随时通过修改子视图的 frame属性声明 来改变其位置。缺省情况 下,边框位于父视图可视边界外部的子视图不会被裁剪。如果您希望激活裁剪 功能,必须将父视图的 clipsToBounds属性设置为 YES。 程序清单 2-1 显示了一个应用程序委托对象的 applicationDidFinishLaunching:方法示例。在这个例子中,应用程 序委托在启动时通过代码创建全部的用户界面。界面中包含两个普通的 UIView对象,用于显示基本颜色。每个视图都被嵌入到窗口中,窗口也是 UIView 的一个子类,因此可以作为父视图。父视图会保持它们的子视图,因 此这个方法释放了新创建的视图对象,以避免重复保持。 程序清单 2-1  创建一个带有视图的窗口 - (void)applicationDidFinishLaunching:(UIApplication *)application { // Create the window object and assign it to the // window instance variable of the application delegate. window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; window.backgroundColor = [UIColor whiteColor]; // Create a simple red square CGRect redFrame = CGRectMake(10, 10, 100, 100); UIView *redView = [[UIView alloc] initWithFrame:redFrame]; redView.backgroundColor = [UIColor redColor]; // Create a simple blue square CGRect blueFrame = CGRectMake(10, 150, 100, 100); UIView *blueView = [[UIView alloc] initWithFrame:blueFrame]; blueView.backgroundColor = [UIColor blueColor]; // Add the square views to the window [window addSubview:redView]; [window addSubview:blueView]; // Once added to the window, release the views to avoid the // extra retain count on each of them. [redView release]; [blueView release]; // Show the window. [window makeKeyAndVisible]; } 重要提示: 在内存管理方面,可以将子视图考虑为其它的集合对象。特别是当 您通过 addSubview:方法将一个视图作为子视图插入时,父视图会对其进行 保持操作。反过来,当您通过 removeFromSuperview方法将子视图从父视 图移走时,子视图会被自动释放。在将视图加入视图层次之后释放该对象可以 避免多余的保持操作,从而避免内存泄露。 有关 Cocoa内存管理约定的更多信息,请参见 Cocoa内存管理编程指南 。 当您为某个视图添加子视图时, UIKit会向相应的父子视图发送几个消息,通知 它们当前发生的状态变化。您可以在自己的定制视图中对诸如 willMoveToSuperview: 、 willMoveToWindow: 、 willRemoveSubview: 、 didAddSubview: 、 didMoveToSuperview、和 didMoveToWindow这样的方法进行重载,以 便在事件发生的前后进行必要的处理,并根据发生的变化更新视图的状态信 息。 在视图层次建立之后,您可以通过视图的 superview属性来取得其父视图, 或者通过 subviews 属性取得视图的子视图。您也可以通过 isDescendantOfView:方法来判定一个视图是否在其父视图的视图层中。 一个视图层次的根视图没有父视图,因此其 superview属性被设置为 nil。 对于当前被显示在屏幕上的视图,窗口对象通常是整个视图层次的根视图。 您可以通过视图的 window属性来取得指向其父窗口(如果有的话)的指针, 如果视图还没有被链接到窗口上,则该属性会被设置为 nil。 视图层次中的坐标转换 很多时候,特别是处理事件的时候,应用程序可能需要将一个相对于某边框的 坐标值转换为相对于另一个边框的值。例如,触摸事件通常使用基于窗口指标 系统的坐标值来报告事件发生的位置,但是视图对象需要的是相对于视图本地 坐标的位置信息,两者可能是不一样的。 UIView类定义了下面这些方法,用 于在不同的视图本地坐标系统之间进行坐标转换: 1 convertPoint:fromView: 2 convertRect:fromView: 3 convertPoint:toView: 4 convertRect:toView: convert...:fromView:方法将指定视图的坐标值转换为视图本地坐标系 统的坐标值; convert...:toView:方法则将视图本地坐标系统的坐标值 转换为指定视图坐标系统的坐标值。如果传入 nil作为视图引用参数的值,则 上面这些方法会将视图所在窗口的坐标系统作为转换的源或目标坐标系统。 除了 UIView的转换方法之外, UIWindow类也定义了几个转换方法。这些方 法和 UIView的版本类似,只是 UIView定义的方法将视图本地坐标系统作为 转换的源或目标坐标系统,而 UIWindow的版本则使用窗口坐标系统。 1 convertPoint:fromWindow: 2 convertRect:fromWindow: 3 convertPoint:toWindow: 4 convertRect:toWindow: 当参与转换的视图没有被旋转,或者被转换的对象仅仅是点的时候,坐标转换 相当直接。如果是在旋转之后的视图之间转换矩形或尺寸数据,则其几何结构 必须经过合理的改变,才能得到正确的结果坐标。在对矩形结构进行转换时, UIView类假定您希望保证原来的屏幕区域被覆盖,因此转换后的矩形会被放 大,其结果是使放大后的矩形(如果放在对应的视图中)可以完全覆盖原来的 矩形区域。图 2-11显示了将 rotatedView对象的坐标系统中的矩形转换到 其超类( outerView)坐标系统的结果。 图 2-11  对旋转后视图中的值进行转换 对于尺寸信息, UIView简单地将它处理为分别相对于源视图和目标视图 (0.0, 0.0)点的偏移量。虽然偏移量保持不变,但是相对于坐标轴的差额会随着视图 的旋转而移动。在转换尺寸数据时, UIKit总是返回正的数值。 标识视图 UIView类中包含一个 tag属性。借助这个属性,您可以通过一个整数值来标 识一个视图对象。您可以通过这个属性来唯一标识视图层次中的视图,以及在 运行时进行视图的检索(基于 tag标识的检索比您自行遍历视图层次要快)。 tag属性的缺省值为 0。 您可以通过 UIView的 viewWithTag:方法来检索标识过的视图。该方法从 消息的接收者自身开始,通过深度优先的方法来检索接收者的子视图。 在运行时修改视图 应用程序在接收用户输入时,需要通过调整自己的用户界面来进行响应。应用 程序可能重新排列界面上的视图、刷新屏幕上模型数据已被改变的视图、或者 装载一组全新的视图。在决定使用哪种技术时,要考虑您的用户界面,以及您 希望实现什么。但是,如何初始化这些技术对于所有应用程序都是一样的。本 章的下面部分将描述这些技术,以及如何通过这些技术在运行时更新您的用户 界面。 请注意: 如果您需要了解 UIKit如何在框架内部和您的定制代码之间转移事件和 消息的背景信息,请在继续阅读本文之前查阅 “视图交互模型 ”部分。 实现视图动画 动画为用户界面在不同状态之间的迁移过程提供流畅的视觉效果。在 iPhone OS中,动画被广泛用于视图的位置调整、尺寸变化、甚至是 alpha值的变化 (以实现淡入淡出的效果)。动画支持对于制作易于使用的应用程序是至关重 要的,因此, UIKit直接将它集成到 UIView类中,以简化动画的创建过程。 UIView类定义了几个内在支持 动画 的 属性声明 —也就是说,当这些属性值发 生变化时,视图为其变化过程提供内建的动画支持。虽然执行动画所需要的工 作由 UIView类自动完成,但您仍然必须在希望执行动画时通知视图。为此, 您需要将改变给定属性的代码包装在一个动画块中。 动画块 从调用 UIView的 beginAnimations:context:类方法开始,而以 调用 commitAnimations类方法作为结束。在这两个调用之间,您可以配置 动画的参数和改变希望实行动画的属性值。一旦调用 commitAnimations方 法, UIKit就会开始执行动画,即把给定属性从当前值到新值的变化过程用动画 表现出来。动画块可以被嵌套,但是在最外层的动画块提交之前,被嵌套的动 画不会被执行。 表 2-2列举了 UIView类中支持动画的属性。 表 2-2  支持动画的属性 属性 描述 frame 视图的边框矩形,位于父视图的坐标系中。 bounds 视图的边界矩形,位于视图的坐标系中。 center 边框的中心,位于父视图的坐标系中。 transform 视图上的转换矩阵,相对于视图边界的中心。 alpha 视图的 alpha值,用于确定视图的透明度。 配置动画的参数 除了在动画块中改变属性值之外,您还可以对其它参数进行配置,以确定您希 望得到的动画行为。为此,您可以调用下面这些 UIView的类方法: ■ 用 setAnimationStartDate: 方法来设置动画在 commitAnimations方法返回之后的发生日期。缺省行为是使动画立 即在动画线程中执行。 ■ 用 setAnimationDelay: 方法来设置实际发生动画和 commitAnimations方法返回的时间点之间的间隔。 ■ 用 setAnimationDuration:方法来设置动画持续的秒数。 ■ 用 setAnimationCurve:方法来设置动画过程的相对速度,比如动画 可能在启示阶段逐渐加速,而在结束阶段逐渐减速,或者整个过程都保 持相同的速度。 ■ 用 setAnimationRepeatCount:方法来设置动画的重复次数。 ■ 用 setAnimationRepeatAutoreverses:方法来指定动画在到达 目标值时是否自动反向播放。您可以结合使用这个方法和 setAnimationRepeatCount:方法,使各个属性在初始值和目标值 之间平滑切换一段时间。 commitAnimations类方法在调用之后和动画开始之前立刻返回。 UIKit在 一个独立的、和应用程序的主事件循环分离的线程中执行动画。 commitAnimations方法将动画发送到该线程,然后动画就进入线程中的队 列,直到被执行。缺省情况下,只有在当前正在运行的动画块执行完成后, Core Animation才会启动队列中的动画。但是,您可以通过向动画块中的 setAnimationBeginsFromCurrentState:类方法传入 YES来重载这 个行为,使动画立即启动。这样做会停止当前正在执行的动画,而使新动画在 当前状态下开始执行。 缺省情况下,所有支持动画的属性在动画块中发生的变化都会形成动画。如果 您希望让动画块中发生的某些变化不产生动画效果,可以通过 setAnimationsEnabled:方法来暂时禁止动画,在完成修改后才重新激 活动画。在调用 setAnimationsEnabled:方法并传入 NO值之后,所有的 改变都不会产生动画效果,直到用 YES值再次调用这个方法或者提交整个动画 块时,动画才会恢复。您可以用 areAnimationsEnabled方法来确定当前 是否激活动画。 配置动画的委托 您可以为动画块分配一个 委托 ,并通过该委托接收动画开始和结束的消息。当 您需要在动画开始前和结束后立即执行其它任务时,可能就需要这样做。您可 以通过 UIView的 setAnimationDelegate:类方法来设置委托,并通过 setAnimationWillStartSelector: 和 setAnimationDidStopSelector:方法来指定接收消息的选择器方法。 消息处理方法的形式如下: - (void)animationWillStart:(NSString *)animationID context:(void *)context; - (void)animationDidStop:(NSString *)animationID finished: (NSNumber *)finished context:(void *)context; 上面两个方法的 animationID和 context参数和动画块开始时传给 beginAnimations:context:方法的参数相同: ■ animationID - 应用程序提供的字符串,用于标识一个动画块中的动 画。 ■ context - 也是应用程序提供的对象,用于向委托对象传递额外的信 息。 setAnimationDidStopSelector:选择器方法还有一个参数 —即一个布 尔值。如果动画顺利完成,没有被其它动画取消或停止,则该值为 YES。 响应布局的变化 任何时候,当视图的布局发生改变时, UIKit会激活每个视图的自动尺寸调整行 为,然后调用各自的 layoutSubviews方法,使您有机会进一步调整子视图 的几何尺寸。下面列举的情形都会引起视图布局的变化: ■ 视图边界矩形的尺寸发生变化。 ■ 滚动视图的内容偏移量 —也就是可视内容区域的原点 —发生变化。 ■ 和视图关联的转换矩阵发生变化。 ■ 和视图层相关联的 Core Animation子层组发生变化。 ■ 您的应用程序调用视图的 setNeedsLayout或 layoutIfNeeded方 法来强制进行布局。 ■ 您的应用程序调用视图背后的层对象的 setNeedsLayout方法来强制 进行布局。 子视图的初始布局由视图的自动尺寸调整行为来负责。应用这些行为可以保证 您的视图接近其设计的尺寸。有关自动尺寸调整行为如何影响视图的尺寸和位 置的更多信息,请参见 “自动尺寸调整行为 ”部分。 有些时候,您可能希望通过 layoutSubviews方法来手工调整子视图的布 局,而不是完全依赖自动尺寸调整行为。举例来说,如果您要实现一个由几个 子视图元素组成的定制控件,则可以通过手工调整子视图来精确控制控件在一 定尺寸范围内的外观。还有,如果一个视图表示的滚动内容区域很大,可以选 择将内容显示为一组平铺的子视图,在滚动过程中,可以回收离开屏幕边界的 视图,并在填充新内容后将它重新定位,使它成为下一个滚入屏幕的视图。 请注意: 您也可以用 layoutSubviews方法来调整作为子层链接到视图层的 定制 CALayer对象。您可以通过对隐藏在视图后面的层层次进行管理,实现 直接基于 Core Animation的高级动画。有关如何通过 Core Animation管理层 层次的更多信息,请参见 Core Animation编程指南 。 在编写布局代码时,请务必在应用程序支持的每个方向上都进行测试。对于同 时支持景观方向和肖像方向的应用程序,必须确认其是否能正确处理两个方向 上的布局。类似地,您的应用程序应该做好处理其它系统变化的准备,比如状 态条高度的变化,如果用户在使用您的应用程序的同时接听电话,然后再挂 断,就会发生这种变化。在挂断时,负责管理视图的 视图控制器 可能会调整视 图的尺寸,以适应缩小的状态条。之后,这样的变化会向下渗透到应用程序的 其它视图。 重画视图的内容 有些时候,应用程序数据模型的变化会影响到相应的用户界面。为了反映这些 变化,您可以将相应的视图标识为需要刷新(通过调用 setNeedsDisplay 或 setNeedsDisplayInRect:方法)。和简单创建一个图形上下文并进行 描画相比,将视图标识为需要刷新的方法使系统有机会更有效地执行描画操 作。举例来说,如果您在某个运行周期中将一个视图的几个区域标识为需要刷 新,系统就会将这些需要刷新的区域进行合并,并最终形成一个 drawRect: 方法的调用。结果,只需要创建一个图形上下文就可以描画所有这些受影响的 区域。这个做法比连续快速创建几个图形上下文要有效得多。 实现 drawRect:方法的视图总是需要检查传入的矩形参数,并用它来限制描 画操作的范围。因为描画是开销相对昂贵的操作,以这种方式来限制描画是提 高性能的好方法。 缺省情况下,视图在几何上的变化并不自动导致重画。相反,大多数几何变化 都由 Core Animation来自动处理。具体来说,当您改变视图的 frame、 bounds、 center、或 transform属性时, Core Animation会将相应的几 何变化应用到与视图层相关联的缓存位图上。在很多情况下,这种方法是完全 可以接受的,但是如果您发现结果不是您期望得到的,则可以强制 UIKit对视图 进行重画。为了避免 Core Animation自动处理几何变化,您可以将视图的 contentMode属性声明 设置为 UIViewContentModeRedraw。更多有关 内容模式的信息,请参见 “内容模式和比例缩放 ”部分。 隐藏视图 您可以通过改变视图的 hidden属性声明 来隐藏或显示视图。将这个属性设置 为 YES会隐藏视图,设置为 NO则可以显示视图。对一个视图进行隐藏会同时隐 藏其内嵌的所有子视图,就好象它们自己的 hidden属性也被设置一样。 当您隐藏一个视图时,该视图仍然会保留在视图层次中,但其内容不会被描 画,也不会接收任何触摸事件。由于隐藏视图仍然存在于视图层次中,所以会 继续参与自动尺寸调整和其它布局操作。如果被隐藏的视图是当前的第一响应 者,则该视图会自动放弃其自动响应者的状态,但目标为第一响应者的事件仍 然会传递给隐藏视图。有关响应者链的更多信息,请参见 “响应者对象和响应者 链 ”部分。 创建一个定制视图 UIView类为在屏幕上显示内容及处理触摸事件提供了潜在的支持,但是除了 在视图区域内描画带有 alpha值的背景色之外, UIView类的实例不做其它描 画操作,包括其子视图的描画。如果您的应用程序需要显示定制的内容,或以 特定的方式处理触摸事件,必须创建 UIView的定制子类。 本章的下面部分将描述一些定制视图对象可能需要实现的关键方法和行为。有 关子类化的更多信息,请参见 UIView类参考 。 初始化您的定制视图 您定义的每个新的视图对象都应该包含 initWithFrame:初始化 方法。该方 法负责在创建对象时对类进行初始化,使之处于已知的状态。在通过代码创建 您的视图实例时,需要使用这个方法。 程序清单 2-2显示了标准的 initWithFrame:方法的一个框架实现。该实现 首先调用继承自超类的实现,然后初始化类的实例变量和状态信息,最后返回 初始化完成的对象。您通常需要首先执行超类的实现,以便在出现问题时可以 简单地终止自己的初始化代码,返回 nil。 程序清单 2-2  初始化一个视图的子类 - (id)initWithFrame:(CGRect)aRect { self = [super initWithFrame:aRect]; if (self) { // setup the initial properties of the view ... } return self; } 如果您从 nib文件 中装载定制视图类的实例,则需要知道:在 iPhone OS中, 装载 nib的代码并不通过 initWithFrame:方法来实例化新的视图对象,而 是通过 NSCoding协议 定义的 initWithCoder:方法来进行。 即使您的视图采纳了 NSCoding协议, Interface Builder也不知道它的定制属 性,因此不知道如何将那些属性编码到 nib文件中。所以,当您从 nib文件装载 定制视图时, initWithCoder:方法不具有进行正确初始化所需要的信息。 为了解决这个问题,您可以在自己的类中实现 awakeFromNib方法,特别用 于从 nib文件装载的定制类。 描画您的视图内容 当您改变视图内容时,可以通过 setNeedsDisplay 或 setNeedsDisplayInRect:方法来将需要重画的部分通知给系统。在应用 程序返回运行循环之后,会对所有的描画请求进行合并,计算界面中需要被更 新的部分;之后就开始遍历视图层次,向需要更新的视图发送 drawRect:消 息。遍历的起点是视图层次的根视图,然后从后往前遍历其子视图。在可视边 界内显示定制内容的视图必须实现其 drawRect:方法,以便对该内容进行渲 染。 在调用视图的 drawRect:方法之前, UIKit会为其配置描画的环境,即创建一 个图形上下文,并调整其坐标系统和裁剪区,使之和视图的坐标系统及边界相 匹配。因此,在您的 drawRect:方法被调用时,您可以使用 UIKit的类和函 数、 Quartz的函数、或者使用两者相结合的方法来直接进行描画。需要的话, 您可以通过 UIGraphicsGetCurrentContext函数来取得当前图形上下文 的指针,实现对它的访问。 重要提示: 只有当定制视图的 drawRect:方法被调用的期间,当前图形上下 文才是有效的。 UIKit可能为该方法的每个调用创建不同的图形上下文,因此, 您不应该对该对象进行缓存并在之后使用。 程序清单 2-3显示了 drawRect:方法的一个简单实现,即在视图边界描画一 个 10像素宽的红色边界。由于 UIKit描画操作的实现也是基于 Quartz,所以您 可以像下面这样混合使用不同的描画调用来得到期望的结果。 程序清单 2-3  一个描画方法 - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); CGRect myFrame = self.bounds; CGContextSetLineWidth(context, 10); [[UIColor redColor] set]; UIRectFrame(myFrame); } 如果您能确定自己的描画代码总是以不透明的内容覆盖整个视图的表面,则可 以将视图的 opaque属性声明 设置为 YES,以提高描画代码的总体效率。当您 将视图标识为不透明时, UIKit会避免对该视图正下方的内容进行描画。这不仅 减少了描画开销的时间,而且减少内容合成需要的工作。然而,只有当您能确 定视图提供的内容为不透明时,才能将这个属性设置为 YES;如果您不能保证 视图内容总是不透明,则应该将它设置为 NO。 提高描画性能(特别是在滚动过程)的另一个方法是将视图的 clearsContextBeforeDrawing属性设置为 NO。当这个属性被设置为 YES时, UIKIt会在调用 drawRect:方法之前,把即将被该方法更新的区域填 充为透明的黑色。将这个属性设置为 NO可以取消相应的填充操作,而由应用程 序负责完全重画传给 drawRect:方法的更新矩形中的部分。这样的优化在滚 动过程中通常是一个好的折衷。 响应事件 UIView类是 UIResponder的一个子类,因此能够接收用户和视图内容交互 时产生的触摸事件。触摸事件从发生触摸的视图开始,沿着响应者链进行传 递,直到最后被处理。视图本身就是响应者,是响应者链的参与者,因此可以 收到所有关联子视图派发给它们的触摸事件。 处理触摸事件的视图通常需要实现下面的所有方法,更多细节请参见 “事件处 理 ”部分: 1 touchesBegan:withEvent: 2 touchesMoved:withEvent: 3 touchesEnded:withEvent: 4 touchesCancelled:withEvent: 请记住,在缺省情况下,视图每次只响应一个触摸动作。如果用户将第二个手 指放在屏幕上,系统会忽略该触摸事件,而不会将它报告给视图对象。如果您 希望在视图的事件处理器方法中跟踪多点触摸手势,则需要重新激活多点触摸 事件,具体方法是将视图的 multipleTouchEnabled属性声明 设置为 YES。 某些视图,比如标签和图像视图,在初始状态下完全禁止事件处理。您可以通 过改变视图的 userInteractionEnabled属性值来控制视图是否可以对事 件进行处理。当某个耗时很长的操作被挂起时,您可以暂时将这个属性设置为 NO,使用户无法对视图的内容进行操作。为了阻止事件到达您的视图,还可以 使用 UIApplication对象的 beginIgnoringInteractionEvents和 endIgnoringInteractionEvents方法。这些方法影响的是整个应用程 序的事件分发,而不仅仅是某个视图。 在处理触摸事件时, UIKit会通过 UIView的 hitTest:withEvent:和 pointInside:withEvent:方法来确定触摸事件是否发生在指定的视图 上。虽然很少需要重载这些方法,但是您可以通过重载来使子视图无法处理触 摸事件。 视图对象的清理 如果您的视图类 分配 了任何内存、存储了任何对象的引用、或者持有在释放视 图时也需要 被释放 的资源,则必须实现其 dealloc方法。当您的视图对象的 保持数为零、且视图本身即将被解除分配时,系统会调用其 dealloc方法。 您在这个方法的实现中应该释放视图持有的对象和资源,然后调用超类的实 现,如程序程序清单 2-4所示。 程序清单 2-4  实现 dealloc方法 - (void)dealloc { // Release a retained UIColor object [color release]; // Call the inherited implementation [super dealloc]; } 事件处理 本章将描述 iPhone OS系统中的事件类型,并解释如何处理这些事件。文中还 将讨论如何在应用程序内部或不同应用程序间通过 UIPasteboard类提供的 设施进行数据的拷贝和粘贴,该类是 iPhone OS 3.0引入的。 iPhone OS支持两种类型的事件:即触摸事件或运动事件。在 iPhone OS 3.0 中, UIEvent类已经被扩展为不仅可以包含触摸事件和运动事件,还可以容 纳将来可能引入的其它事件类型。每个事件都有一个与之关联的事件类型和子 类型,可以通过 UIEvent的 type和 subtype属性声明 进行访问,类型既包 括触摸事件,也包括运动事件。在 iPhone OS 3.0上,子类型只有一种,即摇 摆 -运动子类型( UIEventSubtypeMotionShake)。 触摸事件 iPhone OS中的触摸事件基于多点触摸模型。用户不是通过鼠标和键盘,而是 通过触摸设备的屏幕来操作对象、输入数据、以及指示自己的意图。 iPhone OS将一个或多个和屏幕接触的手指识别为 多点触摸序列 的一部分,该序列从第 一个手指碰到屏幕开始,直到最后一个手指离开屏幕结束。 iPhone OS通过一 个多点触摸序列来跟踪与屏幕接触的手指,记录每个手指的触摸特征,包括手 指在屏幕上的位置和发生触摸的时间。应用程序通常将特定组合的触摸识别为 手势,并以用户直觉的方式来进行响应,比如对收缩双指距离的手势,程序的 响应是缩小显示的内容;对轻拂屏幕的手势,则响应为滚动显示内容。 请注意: 手指在屏幕上能达到的精度和鼠标指针有很大的不同。当用户触击屏 幕时,接触区域实际上是椭圆形的,而且比用户想像的位置更靠下一点。根据 触摸屏幕的手指、手指的尺寸、手指接触屏幕的力量、手指的方向、以及其它 因素的不同,其 “接触部位 ”的尺寸和形状也有所不同。底层的多点触摸系统会 分析所有的这些信息,为您计算出单一的触点。 很多 UIKit类对多点触摸事件的处理方式不同于它的对象实例,特别是像 UIButton和 UISlider这样的 UIControl的子类。这些子类的对象 —被称 为控件对象 —只接收特定类型的手势,比如触击或向特定方向拖拽。控件对象 在正确配置之后,会在某种手势发生后将动作消息发送给目标对象。其它的 UIKit类则在其它的上下文中处理手势,比如 UIScrollView可以为表格视图 和具有很大内容区域的文本视图提供滚动行为。 某些应用程序可能不需要直接处理事件,它们可以依赖 UIKit类实现的行为。但 是,如果您创建了 UIView的 定制子类 —这是 iPhone OS系统开发的常见模式 —且希望该视图响应特定的触摸事件,就需要实现处理该事件所需要的代码。 而且,如果您希望一个 UIKit对象以不同的方式响应事件,就必须创建框架类的 子类,并重载相应的事件处理方法。 事件和触摸 在 iPhone OS中, 触摸 动作是指手指碰到屏幕或在屏幕上移动,它是一个 多点 触摸序列 的一部分。比如,一个 pinch-close手势就包含两个触摸动作:即屏 幕上的两个手指从相反方向靠近对方。一些单指手势则比较简单,比如触击、 双击、或轻拂(即用户快速碰擦屏幕)。应用程序也可以识别更为复杂的手 势,举例来说,如果一个应用程序使用具有转盘形状的定制控件,用户就需要 用多个手指来 “转动 ”转盘,以便进行某种精调。 事件 是当用户手指触击屏幕及在屏幕上移动时,系统不断发送给应用程序的对 象。事件对象为一个多点触摸序列中所有触摸动作提供一个快照,其中最重要 的是特定视图中新发生或有变化的触摸动作。一个多点触摸序列从第一个手指 碰到屏幕开始,其它手指随后也可能触碰屏幕,所有手指都可能在屏幕上移 动。当最后一个手指离开屏幕时,序列就结束了。在触摸的每个阶段,应用程 序都会收到事件对象。 触摸信息有时间和空间两方面,时间方面的信息称为阶段( phrase),表示触 摸是否刚刚开始、是否正在移动或处于静止状态,以及何时结束 —也就是手指 何时从屏幕举起(参见图 3-1)。触摸信息还包括当前在视图或窗口中的位置 信息,以及之前的位置信息(如果有的话)。当一个手指接触屏幕时,触摸就 和某个窗口或视图关联在一起,这个关联在事件的整个生命周期都会得到维 护。如果有多个触摸同时发生,则只有和同一个视图相关联的触摸会被一起处 理。类似地,如果两个触摸事件发生的间隔时间很短,也只有当它们和同一个 视图相关联时,才会被处理为多触击事件。 图 3-1 多点触摸序列和触摸阶段 在 iPhone OS中,一个 UITouch对象表示一个触摸,一个 UIEvent对象表示 一个事件。事件对象中包含与当前多点触摸序列相对应的所有触摸对象,还可 以提供与特定视图或窗口相关联的触摸对象(参见图 3-2)。在一个触摸序列 发生的过程中,对应于特定手指的触摸对象是持久的,在跟踪手指运动的过程 中, UIKit会对其进行修改。发生改变的触摸 属性变量 有触摸阶段、触摸在视图 中的位置、发生变化之前的位置、以及时间戳。事件处理代码通过检查这些属 性的值来确定如何响应事件。 图 3-2 UIEvent对象及其 UITouch对象间的关系 系统可能随时取消多点触摸序列,进行事件处理的应用程序必须做好正确响应 的准备。事件的取消可能是由于重载系统事件引起的,电话呼入就是这样的例 子。 事件的传递 系统将事件按照特定的路径传递给可以对其进行处理的对象。如 “核心应用程序 架构 ”部分描述的那样,当用户触摸设备屏幕时, iPhone OS会将它识别为一组 触摸对象,并将它们封装在一个 UIEvent对象中,放入当前应用程序的事件 队列中。事件对象将特定时刻的多点触摸序列封装为一些触摸对象。负责管理 应用程序的 UIApplication单件 对象将事件从队列的顶部取出,然后派发给 其它对象进行处理。典型情况下,它会将事件发送给应用程序的键盘焦点窗口 —即拥有当前用户事件焦点的窗口,然后代表该窗口的 UIWindow对象再将它 发送给第一响应者进行处理(第一响应者在 “响应者对象和响应者链 ”部分中描 述)。 应用程序通过触碰测试( hit-testing)来寻找事件的第一响应者,即通过递归 调用视图层次中视图对象的 hitTest:withEvent:方法来确认发生触摸的 子视图。触摸对象的整个生命周期都和该视图互相关联,即使触摸动作最终移 动到该视图区域之外也是如此。 “事件处理技巧 ”部分对触碰测试在编程方面的 一些隐含意义进行讨论。 UIApplication对象和每个 UIWindow对象都在 sendEvent:方法(两个 类都声明了这个方法)中派发事件。由于这些方法是事件进入应用程序的通 道,所以,您可以从 UIApplication或 UIWindow派生出子类,重载 其 sendEvent:方法,实现对事件的监控或执行特殊的事件处理。但是,大多 数应用程序都不需要这样做。 响应者对象和响应者链 响应者对象 是可以响应事件并对其进行处理的对象。 UIResponder是所有响 应者对象的基类,它不仅为事件处理,而且也为常见的响应者行为定义编程接 口。 UIApplication、 UIView、和所有从 UIView派生出来的 UIKit类(包 括 UIWindow)都直接或间接地继承自 UIResponder类。 第一响应者 是应用程序中当前负责接收触摸事件的响应者对象(通常是一个 UIView对象)。 UIWindow对象以消息的形式将事件发送给第一响应者,使 其有机会首先处理事件。如果第一响应者没有进行处理,系统就将事件(通过 消息)传递给响应者链中的下一个响应者,看看它是否可以进行处理。 响应者链 是一系列链接在一起的响应者对象,它允许响应者对象将处理事件的 责任传递给其它更高级别的对象。随着应用程序寻找能够处理事件的对象,事 件就在响应者链中向上传递。响应者链由一系列 “下一个响应者 ”组成,其顺序 如下: 1. 第一响应者将事件传递给它的视图控制器(如果有的话),然后是它的 父视图。 2. 类似地,视图层次中的每个后续视图都首先传递给它的视图控制器(如 果有的话),然后是它的父视图。 3. 最上层的容器视图将事件传递给 UIWindow对象。 4. UIWindow对象将事件传递给 UIApplication单件对象。 如果应用程序找不到能够处理事件的响应者对象,则丢弃该事件。 响应者链中的所有响应者对象都可以实现 UIResponder的某个事件处理方 法,因此也都可以接收事件消息。但是,它们可能不愿处理或只是部分处理某 些事件。如果是那样的话,它们可以将事件消息转送给下一个响应者,方法大 致如下: - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch* touch = [touches anyObject]; NSUInteger numTaps = [touch tapCount]; if (numTaps < 2) { [self.nextResponder touchesBegan:touches withEvent:event]; } else { [self handleDoubleTap:touch]; } } 请注意: 如果一个响应者对象将一个多点触摸序列的初始阶段的事件处理消息 转发给下一个响应者(在 touchesBegan:withEvent:方法中) , 就应该 同样转发该序列的其它事件处理消息。 动作消息的处理也使用响应者链。当用户对诸如按键或分页控件这样的 UIControl对象进行操作时,控件对象(如果正确配置的话)会向目标对象 发送动作消息。但是,如果目标对象被指定为 nil,应用程序就会像处理事件 消息那样,把该动作消息路由给第一响应者。如果第一响应者没有进行处理, 再发送给其下一个响应者,以此类推,将消息沿着响应者链向上传递。 调整事件的传递 UIKit为应用程序提供了一些简化事件处理、甚至完全关闭事件流的编程接口。 下面对这些方法进行总结: ■ 关闭事件的传递。 缺省情况下,视图会接收触摸事件。但是,您可以将 其 userInteractionEnabled属性声明 设置为 NO,关闭事件传递的 功能。隐藏或透明的视图也不能接收事件。 ■ 在一定的时间内关闭事件的传递。 应用程序可以调用 UIApplication 的 beginIgnoringInteractionEvents方法,并在随后调用 endIgnoringInteractionEvents方法来实现这个目的。前一个 方法使应用程序完全停止接收触摸事件消息,第二个方法则重启消息的 接收。某些时候,当您的代码正在执行动画时,可能希望关闭事件的传 递。 ■ 打开多点触摸的传递。 缺省情况下,视图只接收多点触摸序列的第一个 触摸事件,而忽略所有其它事件。如果您希望视图处理多点触摸,就必 须使它启用这个功能。在代码或 Interface Builder的查看器窗口中将视 图的 multipleTouchEnabled属性设置为 YES,就可以实现这个目 标。 ■ 将事件传递限制在某个单独的视图上。 缺省情况下,视图的 exclusiveTouch属性被设置为 NO。将这个属性设置为 YES会使相应 的视图具有这样的特性:即当该视图正在跟踪触摸动作时,窗口中的其 它视图无法同时进行跟踪,它们不能接收到那些触摸事件。然而,一个 标识为 “独占触摸 ”的视图不能接收与同一窗口中其它视图相关联的触摸事 件。如果一个手指接触到一个独占触摸的视图,则仅当该视图是窗口中 唯一一个跟踪手指的视图时,触摸事件才会被传递。如果一个手指接触 到一个非独占触摸的视图,则仅当窗口中没有其它独占触摸视图跟踪手 指时,该触摸事件才会被传递。 ■ 将事件传递限制在子视图上 。一个定制的 UIView类可以通过重载 hitTest:withEvent:方法来将多点触摸事件的传递限制在它的子视 图上。这个技巧的讨论请参见 “事件处理技巧 ”部分。 处理多点触摸事件 为了处理多点触摸事件, UIView的定制子类(比较不常见的还有 UIApplication 或 UIWindow 的定制子类)必须至少实现一个 UIResponder的事件处理方法。本文的下面部分将对这些方法进行描述,讨 论处理常见手势的方法,并展示一个处理复杂多点触摸事件的响应者对象实 例,以及就事件处理的某些技术提出建议。 事件处理方法 在一个多点触摸序列发生的过程中,应用程序会发出一系列事件消息。为了接 收和处理这些消息,响应者对象的类必须至少实现下面这些由 UIResponder 类声明的方法之一: - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 在给定的触摸阶段中,如果发生新的触摸动作或已有的触摸动作发生变化,应 用程序就会发送这些消息: ■ 当一个或多个手指触碰屏幕时,发送 touchesBegan:withEvent:消 息。 ■ 当一个或多个手指在屏幕上移动时,发送 touchesMoved:withEvent:消息。 ■ 当一个或多个手指离开屏幕时,发送 touchesEnded:withEvent:消 息。 ■ 当触摸序列被诸如电话呼入这样的系统事件所取消时,发送 touchesCancelled:withEvent:消息。 上面这些方法都和特定的触摸阶段(比如 UITouchPhaseBegan)相关联, 该信息存在于 UITouch对象的 phase属性声明 中。 每个与事件处理方法相关联的消息都有两个参数。第一个参数是一个 UITouch对象的 集合 ,表示给定阶段中新的或者发生变化的触摸动作;第二 个参数是一个 UIEvent对象,表示这个特定的事件。您可以通过这个事件对 象得到与之相关联的所有触摸对象( allTouches),或者发生在特定的视 图或窗口上的触摸对象子集。其中的某些触摸对象表示自上次事件消息以来没 有发生变化,或虽然发生变化但处于不同阶段的触摸动作。 为了处理给定阶段的事件,响应者对象常常从传入的集合参数中取得一或多个 UITouch对象,然后考察这些对象的属性或取得它们的位置(如果需要处理 所有触摸对象,可以向该 NSSet对象发送 anyObject消息)。 UITouch类 中有一个名为 locationInView:的重要方法,如果传入 self参数值,它会 给出触摸动作在响应者坐标系统中的位置(假定该响应者是一个 UIView对 象,且传入的视图参数不为 nil)。另外,还有一个与之平行的方法,可以给 出触摸动作之前位置( previousLocationInView:)。 UITouch实例的 属性还可以给出发生多少次触碰( tapCount)、触摸对象的创建或最后一次 变化发生在什么时间( timestamp)、以及触摸处于什么阶段( phase)。 响应者类并不是必须实现上面列出的所有三个事件方法。举例来说,如果它只 对手指离开屏幕感兴趣,则只需要实现 touchesEnded:withEvent:方法 就可以了。 在一个多点触摸序列中,如果响应者在处理事件时创建了某些持久对象,则应 该实现 touchesCancelled:withEvent:方法,以便当系统取消该序列的 时候对其进行 清理 。多点触摸序列的取消常常发生在应用程序的事件处理遭到 外部事件 —比如电话呼入 —破坏的时候。请注意,响应者对象同样应该在收到 多点触摸序列的 touchesEnded:withEvent:消息时清理之前创建的对象 ( “事件处理技巧 ”部分讨论了如何确定一个序列中的最后一个 touch-up事 件)。 处理单个和多个触碰手势 iPhone应用程序中一个很常见的手势是触击:即用户用手指触碰一个对象。响 应者对象可以以一种方式响应单击,而以另外一种方式响应双击,甚至可能以 第三种方式响应三次触击。您可以通过考察 UITouch对象的 tapCount属性 声明 值来确定用户在一个响应者对象上的触击次数, 取得这个值的最好地方是 touchesBegan:withEvent: 和 touchesEnded:withEvent:方法。在很多情况下,我们更倾向于后者, 因为它与用户手指离开屏幕的阶段相对应。在触摸结束阶段 ( UITouchPhaseEnded)考察触击的次数可以确定手指是真的触击,而不 是其它动作,比如手指接触屏幕后拖动的动作。 程序清单 3-1展示了如何检测某个视图上是否发生双击。 程序清单 3-1  检测双击手势 - (void) touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { UITouch *touch = [touches anyObject]; if ([touch tapCount] == 2) { CGPoint tapPoint = [theTouch locationInView:self]; // Process a double-tap gesture } } 当一个响应者对象希望以不同的方式响应单击和双击事件时,就会出现复杂的 情况。举例来说,单击的结果可能是选定一个对象,而双击则可能是显示一个 编辑视图,用于编辑被双击的对象。那么,响应者对象如何知道一个单击不是 另一个双击的起始部分呢?我们接下来解释响应者对象如何借助上文刚刚描述 的事件处理方法来处理这种情况: 1. 在 touchesEnded:withEvent:方法中,当触击次数为一时,响应者 对象就向自身发送一个 performSelector:withObject:afterDelay:消息,其中的 选 择器 标识由响应者对象实现的、用于处理单击手势的方法;第二个参数 是一个 NSValue或 NSDictionary对象,用于保存相关的 UITouch对 象;时延参数则表示单击和双击手势之间的合理时间间隔。 请注意: 使 用一个 NSValue对象或字典来保存触摸对象是因为它们会保持传入的对 象。然而,您自己在进行事件处理时,不应该对 UITouch对象进行保 持。 2. 在 touchesBegan:withEvent:方法中,如果触击次数为二,响应者 对象会向自身发送一个 cancelPreviousPerformRequestsWithTarget:消息,取消当 前被挂起和延期执行的调用。如果触碰次数不为二,则在指定的延时之 后,先前步骤中由选择器标识的方法就会被调用,以处理单击手势。 3. 在 touchesEnded:withEvent:方法中,如果触碰次数为二,响应者 会执行处理双击手势的代码。 检测碰擦手势 水平和垂直的碰擦( Swipe)是简单的手势类型,您可以简单地在自己的代码 中进行跟踪,并通过它们执行某些动作。为了检测碰擦手势,您需要跟踪用户 手指在期望的坐标轴方向上的运动。碰擦手势如何形成是由您自己来决定的, 也就是说,您需要确定用户手指移动的距离是否足够长,移动的轨迹是否足够 直,还有移动的速度是否足够快。您可以保存初始的触碰位置,并将它和后续 的 touch-moved事件报告的位置进行比较,进而做出这些判断。 程序清单 3-2展示了一些基本的跟踪方法,可以用于检测某个视图上发生的水 平碰擦。在这个例子中,视图将触摸的初始位置存储在名为 startTouchPosition的成员变量中。随着用户手指的移动,清单中的代 码将当前的触摸位置和起始位置进行比较,确定是否为碰擦手势。如果触摸在 垂直方向上移动得太远,就会被认为不是碰擦手势,并以不同的方式进行处 理。但是,如果手指继续在水平方向上移动,代码就继续将它作为碰擦手势来 处理。一旦碰擦手势在水平方向移动得足够远,以至于可以认为是完整的手势 时,处理例程就会触发相应的动作。检测垂直方向上的碰擦手势可以用类似的 代码,只是把 x和 y方向的计算互换一下就可以了。 程序清单 3-2  在视图中跟踪碰擦手势 #define HORIZ_SWIPE_DRAG_MIN 12 #define VERT_SWIPE_DRAG_MAX 4 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; startTouchPosition = [touch locationInView:self]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [touches anyObject]; CGPoint currentTouchPosition = [touch locationInView:self]; // If the swipe tracks correctly. if (fabsf(startTouchPosition.x - currentTouchPosition.x) >= HORIZ_SWIPE_DRAG_MIN && fabsf(startTouchPosition.y - currentTouchPosition.y) <= VERT_SWIPE_DRAG_MAX) { // It appears to be a swipe. if (startTouchPosition.x < currentTouchPosition.x) [self myProcessRightSwipe:touches withEvent:event]; else [self myProcessLeftSwipe:touches withEvent:event]; } else { // Process a non-swipe event. } } 处理复杂的多点触摸序列 触击和碰擦是简单的手势。如何处理更为复杂的多点触摸序列 —实际上是解析 应用程序特有的手势 —取决于应用程序希望完成的具体目标。您可以跟踪所有 阶段的所有触摸动作,记录触摸对象中发生变化的 属性变量 ,并正确地改变内 部的状态。 说明如何处理复杂的多点触摸序列的最好方法是通过实例。程序清单 3-3展示 一个定制的 UIView对象如何通过在屏幕上动画移动 “Welcome”标语牌来响应 用户手指的移动,以及如何通过改变欢迎标语的语言来响应用户的双击手势 (例子中的代码来自一个名为 MoveMe的示例工程,进一步考察该工程可以更 好地理解事件处理的上下文)。 程序清单 3-3  处理复杂的多点触摸序列 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[event allTouches] anyObject]; // Only move the placard view if the touch was in the placard view if ([touch view] != placardView) { // On double tap outside placard view, update placard's display string if ([touch tapCount] == 2) { [placardView setupNextDisplayString]; } return; } // "Pulse" the placard view by scaling up then down // Use UIView's built-in animation [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.5]; CGAffineTransform transform = CGAffineTransformMakeScale(1.2, 1.2); placardView.transform = transform; [UIView commitAnimations]; [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.5]; transform = CGAffineTransformMakeScale(1.1, 1.1); placardView.transform = transform; [UIView commitAnimations]; // Move the placardView to under the touch [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration:0.25]; placardView.center = [self convertPoint:[touch locationInView:self] fromView:placardView]; [UIView commitAnimations]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[event allTouches] anyObject]; // If the touch was in the placardView, move the placardView to its location if ([touch view] == placardView) { CGPoint location = [touch locationInView:self]; location = [self convertPoint:location fromView:placardView]; placardView.center = location; return; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *touch = [[event allTouches] anyObject]; // If the touch was in the placardView, bounce it back to the center if ([touch view] == placardView) { // Disable user interaction so subsequent touches don't interfere with animation self.userInteractionEnabled = NO; [self animatePlacardViewToCenter]; return; } } 请注意: 对于通过描画自身的外观来响应事件的定制视图,在事件处理方法中 通常应该只是设置描画状态,而在 drawRect:方法中执行所有的描画操作。 如果需要了解更多关于描画视图内容的方法,请参见 “图形和描画 ”部分。 事件处理技巧 下面是一些事件处理技巧,您可以在自己的代码中使用。 ■ 跟踪 UITouch对象的变化 在事件处理代码中,您可以将触摸状态的相关 位置保存下来,以便在必要时和变化之后的 UITouch实例进行比较。作 为例子,假定您希望将每个触摸对象的最后位置和其初始位置进行比 较,则在 touchesBegan:withEvent: 方法中,您可以通过 locationInView:方法得到每个触摸对象的初始位置,并以 UITouch对象的地址作为键,将它们存储在 CFDictionaryRef封装 类型中;然后,在 touchesEnded:withEvent:方法中,可以通过传 入 UITouch对象的地址取得该对象的初始位置,并将它和当前位置进行 比较(您应该使用 CFDictionaryRef类型,而不是 NSDictionary 对象 ,因为后者需要对其存储的项目进行拷贝,而 UITouch类并不采纳 NSCopying协议,该协议在对象拷贝过程中是必须的)。 ■ 对子视图或层上的触摸动作进行触碰测试 定制视图可以用 UIView的 hitTest:withEvent:方法或 CALayer的 hitTest:方法来寻找接 收触摸事件的子视图或层,进而正确地处理事件。下面的例子用于检测 定制视图的层中的 “Info” 图像是否被触碰。 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { CGPoint location = [[touches anyObject] locationInView:self]; CALayer *hitLayer = [[self layer] hitTest:[self convertPoint:location fromView:nil]]; if (hitLayer == infoImage) { [self displayInfo]; } } ▪ 如果您有一个携带子视图的定制视图,就需要明确自己是希望在子视图 的级别上处理触摸事件,还是在父视图的级别上进行处理。如果子视图 没有实现 touchesBegan:withEvent: 、 touchesEnded:withEvent: 、或者 touchesMoved:withEvent:方法,则这些消息就会沿着响应者链被 传播到父视图。然而,由于多次触碰和多点触摸事件与发生这些动作所 在的子视图是互相关联的,所以父视图不会接收到这些事件。为了保证 能接收到所有的触摸事件,父视图必须重载 hitTest:withEvent:方 法,并在其中返回其本身,而不是它的子视图。 ■ 确定多点触摸序列中最后一个手指何时离开 当您希望知道一个多点触摸 序列中的最后一个手指何时从视图离开时,可以将传入的集合参数中包 含的 UITouch对象数量和 UIEvent参数对象中与该视图关联的触摸对 象数量相比较。请看下面的例子: ■ - (void)touchesEnded:(NSSet*)touches withEvent: (UIEvent*)event { ■ if ([touches count] == [[event touchesForView:self] count]) { ■ // last finger has lifted.... ■ } ■ } ▪ 运动事件 当用户以特定方式移动设备,比如摇摆设备时, iPhone或者 iPod touch会产生 运动事件。运动事件源自设备加速计。系统会对加速计的数据进行计算,如果 符合某种模式,就将它解释为手势,然后创建一个代表该手势的 UIEvent对 象,并发送给当前活动的应用程序进行处理。 请注意: 在 iPhone 3.0上,只有摇摆设备的动作会被解释为手势,并形成运动 事件。 运动事件比触摸事件简单得多。系统只是告诉应用程序动作何时开始及何时结 束,而不包括在这个过程中发生的每个动作的时间。而且,触摸事件中包含一 个触摸对象的 集合 及其相关的状态,而运动事件中除了事件类型、子类型、和 时间戳之外,没有其它 状态 。系统以这种方式来解析运动手势,避免和方向变 化事件造成冲突。 为了处理运动事件, UIResponder 的子类必须实现 motionBegan:withEvent:或 motionEnded:withEvent:方法之一, 或者同时实现这两个方法。举例来说,如果用户希望赋以水平摆动和垂直摆动 不同的意义,就可以在 motionBegan:withEvent:方法中将当前加速计轴 的值缓存起来,并将它们和 motionEnded:withEvent:消息传入的值相比 较,然后根据不同的结果进行动作。响应者还应该实现 motionCancelled:withEvent:方法,以便响应系统发出的运动取消的 事件。有些时候,这些事件会告诉您整个动作根本不是一个正当的手势。 应用程序及其键盘焦点窗口会将运动事件传递给窗口的第一响应者。如果第一 响应者不能处理,事件就沿着响应者链进行传递,直到最终被处理或忽略,这 和触摸事件的处理相类似(详细信息请参见 “事件的传递 ”部分)。但是,摆动 事件和触摸事件有一个很大的不同,当用户开始摆动设备时,系统就会通过 motionBegan:withEvent:消息的方式向第一响应者发送一个运动事件, 如果第一响应者不能处理,该事件就在响应者链中传递;如果摆动持续的时间 小于 1秒左右,系统就会向第一响应者发送 motionEnded:withEvent:消 息;但是,如果摆动时间持续更长,如果系统确定当前的动作不是摆动,则第 一响应者会收到一个 motionCancelled:withEvent:消息。 如果摆动事件沿着响应者链传递到窗口而没有被处理,且 UIApplication的 applicationSupportsShakeToEdit属性被设置为 YES,则 iPhone OS 会显示一个带有撤消( Undo)和重做( Redo)的命令。缺省情况下,这个属 性的值为 NO。 拷贝、剪切、和粘贴操作 在 iPhone OS 3.0之后,用户可以在一个应用程序上拷贝文本、图像、或其它 数据,然后粘贴到当前或其它应用程序的不同位置上。比如,您可以从某个电 子邮件中拷贝一个地址,然后粘贴到 Contacts程序的地址域中。目前, UIKit 框架 在 UITextView、 UITextField、和 UIWebView类中实现了拷贝 -剪 切 -粘贴支持。如果您希望在自己的应用程序中得到这个行为,可以使用这些 类的对象,或者自行实现。 本文的下面部分将描述 UIKit中用于拷贝、剪切、和粘贴操作的编程接口,并解 释其用法。 请注意: 与拷贝和粘贴操作相关的使用指南,请参见 iPhone人机界面指南 文档 中的 “支持拷贝和粘贴 ”部分。 UIKit中支持拷贝 -粘贴操作的设施 UIKit框架提供几个类和一个 非正式协议 ,用于为应用程序中的拷贝、剪切、和 粘贴操作提供方法和机制。具体如下: ■ UIPasteboard类提供了粘贴板的接口。粘贴板是用于在一个应用程序 内或不同应用程序间进行数据共享的受保护区域。该类提供了读写剪贴 板上数据项目的方法。 ■ UIMenuController类可以在选定的拷贝、剪切、和粘贴对象的上下 方显示一个编辑菜单。编辑菜单上的命令可以有拷贝、剪切、粘贴、选 定、和全部选定。 ■ UIResponder 类声明了 canPerformAction:withSender: 方 法。响应者类可以实现这个方法,以根据当前的上下文显示或移除编辑 菜单上的命令。 ■ UIResponderStandardEditActions非正式协议声明了处理拷 贝、剪切、粘贴、选定、和全部选定命令的接口。当用户触碰编辑菜单 上的某个命令时,相应的 UIResponderStandardEditActions方 法就会被调用。 粘贴板的概念 粘贴板是同一应用程序内或不同应用程序间交换数据的标准化机制。粘贴板最 常见的的用途是处理拷贝、剪贴、和粘贴操作: ■ 当用户在一个应用程序中选定数据并选择拷贝(或剪切)菜单命令时, 被选择的数据就会被放置在粘贴板上。 ■ 当用户选择粘贴命令时(可以在同一或不同应用程序中),粘贴板上的 数据就会被拷贝到当前应用程序上。 在 iPhone OS中,粘贴板也用于支持查找( Find)操作。此外,还可以用于在 不同应用程序间通过定制的 URL类型传输数据(而不是通过拷贝、剪切、和粘 贴命令,关于这个技巧的信息请参见 “和其它应用程序间的通讯 ”部分。 无论是哪种操作,您通过粘贴板执行的基本任务是读写粘贴板数据。虽然这些 任务在概念上很简单,但是它们屏蔽了很多重要的细节。复杂的原因主要在于 数据的表现方式可能有很多种,而这个复杂性又引入了效率的考虑。本文的下 面部分将对这些以及其它的问题进行讨论。 命名粘贴板 粘贴板可能是公共的,也可能是私有的。公共粘贴板被称为 系统粘贴板 ;私有 粘贴板则由应用程序自行创建,因此被称为 应用程序粘贴板 。粘贴板必须有唯 一的名字。 UIPasteboard定义了两个系统粘贴板,每个都有自己的名字和 用途: ■ UIPasteboardNameGeneral用于剪切、拷贝、和粘贴操作,涉及到 广泛的数据类型。您可以通过该类的 generalPasteboard类方法来 取得代表通用( General)粘贴板的 单件 对象。 ■ UIPasteboardNameFind用于检索操作。当前用户在检索条 ( UISearchBar)键入的字符串会被写入到这个粘贴板中,因此可以 在不同的应用程序中共享。您可以通过调用 pasteboardWithName:create:类方法 ,并在名字参数中传入 UIPasteboardNameFind值来取得代表检索粘贴板的对象。 典型情况下,您只需使用系统定义的粘贴板就够了。但在必要时,您也可以通 过 pasteboardWithName:create:方法来创建自己的应用程序粘贴板。 如果您调用 pasteboardWithUniqueName方法, UIPasteboard会为您 提供一个具有唯一名称的应用程序粘贴板。您可以通过其 name属性声明 来取 得这个名称。 粘贴板的持久保留 您可以将粘贴板标识为持久保留,使其内容在当前使用的应用程序终止后继续 存在。不持久保留的粘贴板在其创建应用程序退出后就会被移除。系统粘贴板 是持久保留的,而应用程序粘贴板在缺省情况下是不持久保留的。将其应用程 序粘贴板的 persistent属性设置为 YES可以使其持久保留。当持久粘贴板的 拥有者程序被用户卸载时,其自身也会被移除。 粘贴板的拥有者和数据项 最后将数据放到粘贴板的对象被称为该粘贴板的 拥有者 。放到粘贴板上的每一 片数据都称为一个粘贴板 数据项 。粘贴板可以保有一个或多个数据项。应用程 序可以放入或取得期望数量的数据项。举例来说,假定用户在视图中选择的内 容包含一些文本和一个图像,粘贴板允许您将文本和图像作为不同的数据项进 行拷贝。从粘贴板读取多个数据项的应用程序可以选择只读取被支持的数据项 (比如只是文本,而不支持图像)。 重要提示: 当一个应用程序将数据写入粘贴板时,即使只是单一的数据项,该 数据也会取代粘贴板的当前内容。虽然您可能使用 UIPasteboard的 addItems:方法来添加项目,但是该写入方法并不会将那些项目加入到粘贴 板当前内容之后。 数据的表示和 UTI 粘贴板操作经常在不同的应用程序间执行。系统并不要求应用程序了解对方的 信息,包括对方可以处理的数据种类。为了最大化潜在的数据分享能力,粘贴 板可以保留同一个数据项的多种 表示 。例如,一个富文本编辑器可以提供被拷 贝数据的 HTML、 PDF、和纯文本表示。粘贴板上的一个数据项包括应用程序 可为该数据提供的所有表示。 粘贴板数据项的每种表示通常都有一个唯一类型标识符( Unique Type Identifier,缩写为 UTI)。 UTI简单定义为一个唯一标识特定数据类型的字符 串。 UTI提供了一个标识数据类型的常用手段。如果您希望支持一个定制的数 据类型,就必须为其创建一个唯一的标识符。为此,您可以用反向 DNS表示法 来定义类型标识字符串,以确保其唯一性。例如,您可以用 com.myCompany.myApp.myType来表示一个定制的类型标识。更多有关 UTI的信息请参见 统一类型标识符概述 。 作为例子,假定一个应用程序支持富文本和图像的选择,它可能希望将富文本 和 Unicode版本的选定文本,以及选定图像的不同表示放到粘贴板上。在这样 的场景下,每个数据项的每种表示都和它自己的数据一起保存,如图 3-3所 示。 图 3-3  粘贴板及其表示 一般情况下,为了最大化潜在的共享可能性,粘贴板数据项应该包括尽可能多 的表示。 粘贴板的读取程序必须找到最适合自身能力(如果有的话)的数据类型。通常 情况下,这意味着选择内涵最丰富的可用类型。举例来说,一个文本编辑器可 能为被拷贝的数据提供 HTML(富文本)和纯文本表示,支持富文本的应用程 序应该选择 HTML表示,而只支持纯文本的应用程序则应该选择纯文本的表 示。 变化记数 变化记数是每个粘贴板都有的变量,它随着每次粘贴板内容的变化而递增 —特 别是发生增加、修改、或移除数据项的时候。应用程序可以通过考察变化记数 (通过 changeCount属性)来确定粘贴板的当前数据是否和最后一次取得的 数据相同。每次变化记数递增时,粘贴板都会向对此感兴趣的观察者发送通 告。 选择和菜单管理 在拷贝或剪切视图中的某些内容之前,必须首先选择 “某些内容 ”。它可能是一 些文本、一个图像、一个 URL、一种颜色、或者其它类型的数据,包括 定制对 象 。为了在定制视图中实现拷贝 -和 -粘贴行为,您必须自行管理该视图中对象 的选择。如果用户通过特定的触摸手势(比如双击)来选择视图中的对象,您 就必须处理该事件,即在程序内部记录该选择(同时取消之前的选择),可能 还要在视图中指示新的选择。如果用户可以在视图中选择多个对象,然后进行 拷贝 -剪切 -粘贴操作,您就必须实现多选的行为。 请注意: 触摸事件及其处理技巧在 “触摸事件 ”部分进行讨论。 当应用程序确定用户请求了编辑菜单时 —可能就是一个选择的动作 —您应该执 行下面的步骤来显示菜单: 1. 调用 UIMenuController的 sharedMenuController类方法来取 得 全局的 ,即菜单控制器实例。 2. 计算选定内容的边界,并用得到的边界矩形调用 setTargetRect:inView:方法。系统会根据选定内容与屏幕顶部和 底部的距离,将编辑菜单显示在该矩形的上方或下方。 3. 调用 setMenuVisible:animated:方法(两个参数都传入 YES), 在选定内容的上方或下方动画显示编辑菜单。 程序清单 3-4演示了如何在 touchesEnded:withEvent:方法的实现中显 示编辑菜单(注意,例子中省略了处理选择的代码)。在这个代码片段中,定 制视图还向自己发送一个 becomeFirstResponder消息,确保自己在随后 的拷贝、剪切、和粘贴操作中是第一响应者。 程序清单 3-4  显示编辑菜单 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *theTouch = [touches anyObject]; if ([theTouch tapCount] == 2 && [self becomeFirstResponder]) { // selection management code goes here... // bring up editing menu. UIMenuController *theMenu = [UIMenuController sharedMenuController]; CGRect selectionRect = CGRectMake(currentSelection.x, currentSelection.y, SIDE, SIDE); [theMenu setTargetRect:selectionRect inView:self]; [theMenu setMenuVisible:YES animated:YES]; } } 初始的菜单包含所有的命令,因此第一响应者提供了相应的 UIResponderStandardEditActions方法的实现( copy:、 paste: 等)。但是在菜单被显示之前,系统会向第一响应者发送一个 canPerformAction:withSender:消息。在很多情况下,第一响应者就 是定制视图的本身。在该方法的实现中,响应者考察给定的命令(由第一个参 数传入的 选择器 表示)是否适合当前的上下文。举例来说,如果该选择器是 paste:,而粘贴板上没有该视图可以处理的数据,则响应者应该返回 NO,以 便禁止粘贴命令。如果第一响应者没有实现 canPerformAction:withSender:方法,或者没有处理给定的命令,该 消息就会进入响应者链。 程序清单 3-5展示了 canPerformAction:withSender:方法的一个实 现。该实现首先寻找和 copy:、 copy:、及 paste:选择器相匹配的消息,并 根据当前选择的上下文激活或禁用拷贝、剪切、和粘贴菜单命令。对于粘贴命 令,还考虑了粘贴板的内容。 程序清单 3-5  有条件地激活菜单命令 - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { BOOL retValue = NO; ColorTile *theTile = [self colorTileForOrigin:currentSelection]; if (action == @selector(paste:) ) retValue = (theTile == nil) && [[UIPasteboard generalPasteboard] containsPasteboardTypes: [NSArray arrayWithObject:ColorTileUTI]]; else if ( action == @selector(cut:) || action == @selector(copy:) ) retValue = (theTile != nil); else retValue = [super canPerformAction:action withSender:sender]; return retValue; } 请注意,这个方法的最后一个 else子句调用了超类的实现,使超类有机会处 理子类忽略的命令。 还要注意,操作一个菜单命令可能会改变其它菜单命令的上下文。比如,当用 户选择视图中的所有对象时,拷贝和剪切命令就应该被包含在菜单中。在这种 情况下,虽然菜单仍然可见,但是响应者可以调用菜单控制器的 update方 法,使第一响应者的 canPerformAction:withSender:再次被调用。 拷贝和剪切选定的内容 当用户触碰编辑菜单上的拷贝或剪切命令时,系统会分别调用响应者对象的 copy:或 cut:方法。通常情况下,第一响应者 —也就是您的定制视图 —会实 现这些方法,但如果没有实现的话,该消息会按正常的方式进入响应者链。请 注意, UIResponderStandardEditActions非正式协议 声明了这些方 法。 请注意: 由于 UIResponderStandardEditActions是非正式协议,应用 程序中的任何类都可以实现它的方法。但是,为了使命令可以按缺省的方式在 响应者链上传递,实现这些方法的类应该继承自 UIResponder类,且应该被 安装到响应者链中。 在 copy:或 cut:消息的响应代码中,您需要把和选定内容相对应的对象或数 据以尽可能多的表示形式写入到粘贴板上。这个操作涉及到如下这些步骤(假 定只有一个的粘贴板数据项): 1. 标识或取得和选定内容相对应的对象或二进制数据。 二进制数据必须封 装在 NSData对象 中。其它可以写入到粘贴板的对象必须是 属性列表 对 象 —也就是说,必须是下面这些类的对象: NSString、 NSArray、 NSDictionary、 NSDate、 NSNumber、或者 NSURL(有关属性列 表对象的更多信息,请参见 属性列表编程指南 )。 2. 可能的话,请为对象或数据生成一或多个其它的表示。 举例来说,在之 前提到的为选定图像创建 UIImage对象的步骤中,您可以通过 UIImageJPEGRepresentation 或 UIImagePNGRepresentation函数将图像转换为不同的表示。 3. 取得粘贴板对象。 在很多情况下,使用通用粘贴板就可以了。您可以通 过 generalPasteboard类方法 来取得该对象。 4. 为写入到粘贴板数据项的每个数据表示分配一个合适的 UTI。 这个主题 的讨论请参见 “粘贴板的概念 ”部分。 5. 将每种表示类型的数据写入到第一个粘贴板数据项中: ■ 向粘贴板对象发送 setData:forPasteboardType:消息可以 写入数据对象。 ■ 向粘贴板对象发送 setValue:forPasteboardType:消息可 以写入属性列表对象。 6. 对于剪切( cut:方法)命令,需要从应用程序的 数据模型 中移除选定内 容所代表的对象,并更新视图。 程序清单 3-6展示了 copy:和 cut:方法的一个实现。 cut:方法调用了 copy:方法,然后从视图和数据模型中移除选定的对象。注意, copy:方法 对 定制对象 进行归档,目的是得到一个 NSData对象,以便作为参数传递给粘 贴板的 setData:forPasteboardType:方法。 程序清单 3-6  拷贝和剪切操作 - (void)copy:(id)sender { UIPasteboard *gpBoard = [UIPasteboard generalPasteboard]; ColorTile *theTile = [self colorTileForOrigin:currentSelection]; if (theTile) { NSData *tileData = [NSKeyedArchiver archivedDataWithRootObject:theTile]; if (tileData) [gpBoard setData:tileData forPasteboardType:ColorTileUTI]; } } - (void)cut:(id)sender { [self copy:sender]; ColorTile *theTile = [self colorTileForOrigin:currentSelection]; if (theTile) { CGPoint tilePoint = theTile.tileOrigin; [tiles removeObject:theTile]; CGRect tileRect = [self rectFromOrigin:tilePoint inset:TILE_INSET]; [self setNeedsDisplayInRect:tileRect]; } } 粘贴选定内容 当用户触碰编辑菜单上的粘贴命令时,系统会调用响应者对象的 paste:方 法。通常情况下,第一响应者 —也就是您的定制视图 —会实现这些方法,但如 果没有实现的话,该消息会按正常的方式进入响应者链。 paste:方法在 UIResponderStandardEditActions非正式协议 中声明。 在 paste: 消息的响应代码中,您可以从粘贴板中读取应用程序支持的表示, 然后将被粘贴对象加入到应用程序的 数据模型 中,并将新对象显示在用户指定 的视图位置上。这个操作涉及到如下这些步骤(假定只有单一的粘贴板数据 项): 1. 取得粘贴板对象。 在很多情况下,使用通用粘贴板就可以了,您可以通 过 generalPasteboard类方法 来取得该对象。 2. 确认第一个粘贴板数据项是否包含应用程序可以处理的表示,这可以通 过调用 containsPasteboardTypes: 方法,或者调用 pasteboardTypes方法并考察其返回的类型数组来实现。 请注意, 您在 canPerformAction:withSender:方法的实现中应该已经执 行过这个步骤。 3. 如果粘贴板的第一个数据项包含应用程序可以处理的数据,则可以调用 下面的方法来读取: ■ dataForPasteboardType:,如果要读取的数据被封装为 NSData对象 ,就可以使用这个方法。 ■ valueForPasteboardType:,如果要读取的数据被封装为 属 性列表 对象,请使用这个方法(请参见 “拷贝和剪切选定的内容 ”部 分)。 4. 将对象加入到应用程序的数据模型中。 5. 将对象的表示显示在用户界面中用户指定的位置上。 程序清单 3-7是 paste:方法的一个实现实例,该方法执行与 cut:及 copy: 方法相反的操作。示例中的视图首先确认粘贴板是否包含自身支持的定制表示 数据,如果是的话,就读取该数据并将它加入到应用程序的数据模型中,然后 将视图的一部分 —当前选定区域 —标识为需要重画。 程序清单 3-7  将粘贴板的数据粘贴到选定位置上 - (void)paste:(id)sender { UIPasteboard *gpBoard = [UIPasteboard generalPasteboard]; NSArray *pbType = [NSArray arrayWithObject:ColorTileUTI]; ColorTile *theTile = [self colorTileForOrigin:currentSelection]; if (theTile == nil && [gpBoard containsPasteboardTypes:pbType]) { NSData *tileData = [gpBoard dataForPasteboardType:ColorTileUTI]; ColorTile *theTile = (ColorTile *)[NSKeyedUnarchiver unarchiveObjectWithData:tileData]; if (theTile) { theTile.tileOrigin = self.currentSelection; [tiles addObject:theTile]; CGRect tileRect = [self rectFromOrigin:currentSelection inset:TILE_INSET]; [self setNeedsDisplayInRect:tileRect]; } } } 消除编辑菜单 在您实现的 cut:、 copy:、或 paste:命令返回后,编辑菜单会被自动隐 藏。通过下面的代码使它保持可见: [UIMenuController setMenuController].menuVisible = YES; 系统可能在任何时候隐藏编辑菜单,比如当显示警告信息或用户触碰屏幕其它 区域时,编辑菜单就会被隐藏。如果您有某些状态或屏幕显示需要依赖于编辑 菜单是否显示的话,就应该侦听 UIMenuControllerWillHideMenuNotification通告,并执行恰当 的动作。 图形和描画 高质量的图形是应用程序用户界面的重要组成部分。提供高质量的图形不仅会 使应用程序具有好的的外观,还会使它看起来象是系统的自然扩展。 iPhone OS为创建高质量的图形提供两种路径:即通过 OpenGL进行渲染,或者通过 Quartz、 Core Animation、和 UIKit进行渲染。 OpenGL框架 主要适用于游戏或要求高帧率的应用程序开发。它是一组基于 C 语言的接口,用于在桌面电脑上创建 2D和 3D内容。 iPhone OS通过 OpenGL ES框架来支持 OpenGL描画,该框架同时支持 OpenGL ES 2.0和 OpenGL ES v1.1。 OpenGL ES是特别为嵌入式硬件系统设计的,和桌面版本的 OpenGL有 很多不同。 对于希望采用更为面向对象的方法进行描画的开发者, iPhone OS提供了 Quartz、 Core Animation、还有 UIKit中的图形支持。 Quartz是主要的描画 接口,支持基于路径的描画、抗锯齿渲染、渐变填充模式、图像、颜色、坐标 空间变换、以及 PDF文档的创建、显示、和分析。 UIKit为 Quartz的图像和颜 色操作提供了 Objective-C的封装。 Core Animation为很多 UIKit的视图 属性 声明 的动画效果提供底层支持,也可以用于实现定制的动画。 本章将为 iPhone应用程序的描画过程提供一个概览,同时介绍描画技术的一些 具体描画技巧。本章还为如何优化 iPhone OS平台的描画代码提供一些指导原 则和小贴士。 UIKit的图形系统 在 iPhone OS上,所有的描画 —无论是否采用 OpenGL、 Quartz、 UIKit、或者 Core Animation—都发生在 UIView对象的区域内。 视图 定义描画发生的屏 幕区域。如果您使用系统提供的视图,描画工作会自动得到处理;然而,如果 您定义自己的定制视图,则必须自行提供描画代码。对于使用 OpenGL进行描 画的应用程序,一旦建立了渲染表面,就必须使用 OpenGL指定的描画模型。 对于 Quartz、 Core Animation、和 UIKit,您需要使用本文下面部分描述的概 念。 视图描画周期 UIView对象的基本描画模型涉及到如何按需更新视图的内容。通过收集您发 出的更新请求、并在最适合的时机将它们发送给您的描画代码, UIView类使 内容更新过程变得更为简单和高效。 任何时候,当视图的一部分需要重画时, UIView对象内置的描画代码就会调 用其 drawRect:方法,并向它传入一个包含需要重画的视图区域的矩形。您 需要在定制视图子类中重载这个方法,并在这个方法中描画视图的内容。在首 次描画视图时, UIView传递给 drawRect:方法的矩形包含视图的全部可见区 域。但在随后的调用中,该矩形只代表实际需要被描画的部分。触发视图更新 的动作有如下几种: ■ 对遮挡您的视图的其它视图进行移动或删除操作。 ■ 将视图的 hidden属性声明 设置为 NO,使其从隐藏状态变为可见。 ■ 将视图滚出屏幕,然后再重新回到屏幕上。 ■ 显式调用视图的 setNeedsDisplay 或者 setNeedsDisplayInRect:方法。 在调用 drawRect:方法之后,视图会将自己标志为已更新,然后等待新的更 新动作触发下一个更新周期。如果您的视图显示的是静态内容,则只需要在视 图的可见性发生变化时进行响应就可以了,这种变化可能由滚动或其它视图是 否被显示引起的。然而,如果您需要周期性地更新视图内容,就必须确定什么 时候调用 setNeedsDisplay或 setNeedsDisplayInRect:方法来触发 更新。举例来说,如果您需要每秒数次地更新内容,则可能要使用一个定时 器。在响应用户交互或生成新的视图内容时,也可能需要更新视图。 坐标和坐标变换 如 “视图坐标系统 ”部分描述的那样,窗口或视图的坐标原点位于左上角,坐标 的值向下向右递增。当您编写描画代码时,需要通过这个坐标系统来指定描画 内容中点的位置。 如果您需要改变缺省的坐标系统,可以通过修改当前的转换矩阵来实现。 当前 转换矩阵( CTM) 是一个数学矩阵,用于将视图坐标系统上的点映射到设备的 屏幕上。在视图的 drawRect:方法首次被调用时,就需要建立 CTM,使坐标 系统的原点和视图的原点互相匹配,且将坐标轴的正向分别处理为向下和向 右。然而,您可以通过加入缩放、旋转、和转换因子来改变 CTM,从而改变缺 省坐标系统相对于潜在视图或窗口的尺寸、方向、和位置。 修改 CTM是在视图内容描画的标准技术,因为它需要的工作比其它方法少得 多。如果您希望在当前描画系统中坐标为 (20, 20)的位置上画出一个 10 x 10 的方形,可以首先创建一个路径,将它的起始点移动到坐标为 (20, 20)的位置 上,然后再画出组成方形的几条线。然而,如果您在之后希望将方形移动到坐 标为 (10, 10)的位置上,就必须用新的起始点重新创建路径。事实上,每次改 变原点,您都必须重新创建路径。创建路径是开销相对较大的操作,相比之 下,创建一个起始点为 (0, 0)的方形,然后通过修改 CTM来匹配目标描画原点 的开销就少一些。 在 Core Graphics框架 中,有两种修改 CTM的方法。您可以通过 CGContext参 考 定义的 CTM 操控函数来直接修改 CTM ,也可以创建一个 CGAffineTransform结构,将您希望的转换应用到该结构上,然后将它连 结到 CTM上。使用仿射变换可以将各种变换组合在一起,然后一次性地应用到 CTM上。您也可以通过修改和恢复仿射变换来调整点、尺寸、和矩形的值。有 关仿射变换的更多信息,请参见 Quartz 2D编程指南 和 CGAfneTransform参 考 。 图形上下文 在调用您提供的 drawRect:方法之前,视图对象会自动配置其描画环境,使 您的代码可以立即进行描画。作为这些配置的一部分, UIView对象会为当前 描画环境创建一个图形上下文(对应于 CGContextRef封装类型)。该图形 上下文包含描画系统执行后续描画命令所需要的信息,定义了各种基本的描画 属性,比如描画使用的颜色、裁剪区域、线的宽度及风格信息、字体信息、合 成选项、以及几个其它信息。 当您希望在视图之外的其它地方进行描画时,可以创建定制的图形上下文对 象。在 Quartz中,当您希望捕捉一系列描画命令并将它们用于创建图像或 PDF 文件时,就需要这样做。您可以用 CGBitmapContextCreate 或 CGPDFContextCreate函数来创建上下文。有了上下文对象之后,您可以 将它传递给创建内容时需要调用的描画函数。 您创建的定制图形上下文的坐标系统和 iPhone OS使用的本地坐标系统是不同 的。与后者的坐标原点位于左上角不同的是,前者的坐标原点位于左下角,其 坐标值向上向右递增。您在描画命令中指定的坐标必须对此加以考虑,否则, 结果图像或 PDF文件在渲染时就可能会发生错误。 重要提示: 由于在位图或 PDF上下文中进行描画时使用的是左下原点,所以在 将描画结果渲染到视图上的时候,必须对坐标系统进行补偿。换句话说,如果 您创建一个图像,并调用 CGContextDrawImage函数来进行描画,则该图 像在缺省情况下是上下颠倒的。为了纠正这个问题,您必须将 CTM的 y轴进行 翻转(即将该值乘以 -1),使其原点从左下角移动到视图的左上角。 如果使用 UIImage对象来包装您所创建的 CGImageRef类型,则不需要修改 CTM。 UIImage对象会自动对 CGImageRef 类型的坐标系统进行翻转补 偿。 有关图形上下文、如何修改图形状态信息、以及如何用图形上下文来创建定制 内容的更多信息,请参见 Quartz 2D编程指南 。如果需要与图形上下文结合使 用的函数列表,则请参见 CGContext参考 、 CGBitmapContext参考 、以及 CGPDFContext参考 。 点和像素的不同 Quartz描画系统使用基于向量的描画模型,这不同于基于栅格的描画模型。在 栅格描画模型中,描画命令操作的是每个独立的像素,而 Quartz的描画命令则 是通过固定比例的描画空间来指定,这个描画空间就是所谓的 用户坐标空间 。 然后,由 iPhone OS将该描画空间的坐标映射为设备的实际像素。这个模型的 优势在于,使用向量命令描画的图形在通过仿射变换放大或缩小之后仍然显示 良好。 为了维持基于向量的描画系统固有的精度, Quratz描画系统使用浮点数(而不 是定点数)作为坐标值。使用浮点类型的坐标值可以非常精确地指定描画内容 的位置。在大多数情况下,您不必担心这些值最终如何映射到设备的屏幕。 用户坐标空间是您发出的所有描画命令的工作环境。该空间的单位由点来表 示。 设备坐标空间 指的是设备内在的坐标空间,由像素来表示。缺省情况下, 用户坐标空间上的一个点等于设备坐标空间的一个像素,这意味着一个点等于 1/160英寸。然而,您不应该假定这个比例总是 1:1。 颜色和颜色空间 iPhone OS支持 Quartz中具有的所有颜色空间,但是,大多数应用程序应该只 需要 RGB颜色空间,因为 iPhone OS是为嵌入式硬件设计的,而且只在一个屏 幕上显示,在这种场合下, RGB颜色空间是最合适的。 UIColor对象提供了一些便利方法,用于通过 RGB、 HSB、和灰度值指定颜色 值。以这种方式创建颜色不需要指定颜色空间, UIColor对象会自动为您指 定。 您也可以使用 Core Graphics框架中的 CGContextSetRGBStrokeColor 和 CGContextSetRGBFillColor函数来创建和设置颜色。虽然 Core Graphics框架 支持用其它的颜色空间来创建颜色,还支持创建定制的颜色空 间,但是我们不推荐在描画代码中使用那些颜色。您的描画代码应该总是使用 RGB颜色。 支持的图像格式 表 4-1列出了 iPhone OS直接支持的图像格式。在这些格式中,我们优先推荐 PNG格式。 表 4-1  支持的图像格式 格式 文件扩展名 可移植网络图像格式 (PNG) .png 标记图像文件格式 (TIFF) .tiff, .tif 联合影像专家组格式 (JPEG) .jpeg, .jpg 图形交换格式 (GIF) .gif 视窗位图格式 (DIB) .bmp, .BMPf 视窗图标格式 .ico 视窗光标 .cur XWindow位图 .xbm 描画贴士 本文的下面部分将为您提供一些贴士,讨论如何在编写高质量描画代码的同时 确保应用程序外观对最终用户具有吸引力。 确定何时使用定制的描画代码 根据您创建的应用程序类型,不使用或使用很少的定制代码进行描画是可能 的。虽然沉浸式的应用程序通常广泛使用定制的描画代码,但是工具型和效率 型的应用程序则可以使用标准的视图和控件来显示内容。 定制描画代码的使用应该限制在当显示在屏幕上的内容需要动态改变的场合。 比如,用于跟踪用户描画命令的应用程序需要使用定制描画代码;还比如,游 戏程序也需要经常更新屏幕,以反映游戏环境的改变。在那些情况下,您需要 选择合适的描画技术,以及创建定制的视图类来正确处理事件和更新屏幕。 另一方面,如果应用程序中大量的用户界面是固定的,则可以事先将那些界面 渲染到一或多个图像文件中,然后在运行时通过 UIImageView对象显示出 来。您可以根据自己的需要,将图像视图和其它内容组合在一起。比如,您可 以用 UILabel对象来显示需要配置的文本,用按键或其它控件来进行交互。 提高描画的性能 在任何平台上,描画的开销都比较昂贵,对描画代码进行优化一直都是开发过 程的重要步骤。表 4-2列举了几个贴士,用于确保您的描画代码得到尽可能的 优化。除了这些贴士,您还应该用现有的性能工具对代码进行测试,消除描画 热点和多余的描画操作。 表 4-2  提高描画性能的贴士 Tip Action 使描画工作最 小化 在每个更新周期中,您应该只更新视图中真正发生变化的 部分。如果您使用 UIView的 drawRect:方法来进行描 画,则要通过传给该方法的更新矩形来限制描画的范围。 对于基于 OpenGL的描画,您必须自行跟踪更新区域。 尽可能将视图 标识为不透明 合成不透明的视图所需要的开销比合成部分透明的视图要 少得多。一个不透明的视图必须不包含任何透明的内容, 且视图的 opaque属性必须设置为 YES。 删除不透明的 PNG文件中的 alpha通道 如果一个 PNG图像的每个像素都是不透明的,则将其 alpha通道删除可以避免对包含该图像的图层进行融合操 作,从而很大程度上简化了该图像的合成,提高描画的性 能。 在滚动过程中 重用表格单元 和视图 应该避免在滚动过程种创建新的视图。创建新视图的开销 会减少用于更新屏幕的时间,因而导致滚动不平滑。 避免在滚动过 程中清除原先 的内容 缺省情况下,在调用 drawRect:方法对视图的某个区域 进行更新之前, UIKit会清除该区域对应的上下文缓冲区。 如果您对视图的滚动事件进行响应,则在滚动过程中反复 清除缓冲区的开销是很大的。为了禁止这种行为,可以将 clearsContextBeforeDrawing属性设置为 NO。 在描画过程中 尽可能不改变 图形状态 改变图形状态需要窗口服务器的参与。如果您要描画的内 容使用类似的图形状态,则尽可能将这些内容一起描画, 以减少需要改变的状态。 保持图像的质量 为用户界面提供高品质的图像应该是设计工作中的重点之一。图像是一种合理 而有效的显示复杂图形的方法,任何合适的地方都可以使用。在为应用程序创 建图像的时候,请记住下面的原则: ■ 使用 PNG格式的图像。 PNG格式可以提供高品质的图像内容,是 iPhone OS系统上推荐的图像格式。另外, iPhone OS对 PNG图像的描画路径是 经过优化的,通常比其它格式具有更高的效率。 ■ 创建大小合适的图像,避免在显示时调整尺寸。 如果您计划使用特定尺 寸的图像,则在创建图像资源时,务必使用相同的尺寸。不要创建一个 大的图像,然后再缩小,因为缩放需要额外的 CPU开销,而且需要进行 插值。如果您需要以不同的尺寸显示图像,则请包含多个版本的图像, 并选择与目标尺寸相对接近的图像来进行缩放。 用 Quartz和 UIKit进行描画 Quartz是 iPhone OS的窗口服务器和描画技术的一般叫法。 Core Graphics框 架 是 Quartz的核心,也是内容描画的基本接口。该框架提供的数据类型和函数 用于操作如下对象: ■ 图形上下文 ■ 路径 ■ 图像和位图 ■ 透明层 ■ 颜色、图案颜色、和颜色空间 ■ 渐变和阴影 ■ 字体 ■ PDF内容 UIKit在 Quartz基本特性的基础上提供了一组专门的类,用于与图形相关的操 作。 UIKit的图形类并不是为了向您提供一个全面的描画工具箱 —因为这样的工 具在 Core Graphics框架中已经有了,而是为了向其它 UIKit类提供描画支持。 UIKit包括下面的类和函数: ■ UIImage, 一个不可变类,用于图像显示。 ■ UIColor, 为设备颜色提供基本的支持。 ■ UIFont, 为需要字体的类提供字体信息。 ■ UIScreen, 提供屏幕的基本信息。 ■ 生成 UIImage对象的 JPEG或 PNG表示的函数。 ■ 描画矩形和对描画区域进行裁剪的函数。 ■ 改变和获取当前图形上下文的函数 有关 UIKit包含的类和方法的信息,请参见 UIKit框架参考 ,有关组成 Core Graphics框架的封装类型和函数,请参见 Core Graphics框架参考 。 配置图形上下文 在您的 drawRect:方法被调用时,视图对象的内置描画代码已经为您创建并 配置好了一个缺省图形上下文。您可以通过调用 UIGraphicsGetCurrentContext函数来取得当前上下文的指针,该函数 返回一个类型为 CGContextRef的引用,您可以将它传给 Core Graphics函 数,以修改当前的图形状态。表 4-3列出了负责设置各种图形状态的一些主要 函数,如果需要完整的函数列表,请参见 CGContext参考 。该表还列出了 UIKit中和这些函数对应的组件,如果有的话。 表 4-3  修改图形状态的 Core Graphics函数 图形状态 Core Graphics函数 UIKit对应 组件 当前转换矩阵 (CTM) CGContextRotateCTM CGContextScaleCTM CGContextTranslateCTM CGContextConcatCTM 无 裁剪区域 CGContextClipToRect 无 线 : 宽度,线间 链接,线端 点,破折号, 斜角限制 CGContextSetLineWidth CGContextSetLineJoin CGContextSetLineCap CGContextSetLineDash CGContextSetMiterLimit 无 曲线拟合的精 度 (平滑度 ) CGContextSetFlatness 无 抗锯齿设置 CGContextSetAllowsAntialiasin g 无 颜色:填充和 笔划设置 CGContextSetRGBFillColor CGContextSetRGBStrokeColor UIColor 类 Alpha值 (透明 度 ) CGContextSetAlpha 无 渲染意图 CGContextSetRenderingIntent 无 颜色空间:填 充和笔划设置 CGContextSetFillColorSpace CGContextSetStrokeColorSpace 无 文本:字体, 字体尺寸,字 符间隔,文本 描画模式 CGContextSetFont CGContextSetFontSize CGContextSetCharacterSpacing UIFont 类 混合模式 CGContextSetBlendMode 您可以为 UIImage 类和各种 描画函数 指定混合 模式 图形上下文中包含一个保存过的图形状态堆栈。在 Quartz创建图形上下文时, 该堆栈是空的。 CGContextSaveGState函数的作用是将当前图形状态推入 堆栈。之后,您对图形状态所做的修改会影响随后的描画操作,但不影响存储 在堆栈中的拷贝。在修改完成后,您可以通过 CGContextRestoreGState 函数把堆栈顶部的状态弹出,返回到之前的图形状态。这种推入和弹出的方式 是回到之前图形状态的快速方法,避免逐个撤消所有的状态修改;这也是将某 些状态(比如裁剪路径)恢复到原有设置的唯一方式。 有关图形上下文及如何用它来配置描画环境的一般信息,请参见 Quartz 2D编 程指南 的 图形上下文 部分。 创建和描画图像 iPhone OS同时支持通过 UIKit和 Core Graphics框架 装载和显示图像。到底选 择哪些类和函数描画图像取决于具体的应用场合。但是,我们推荐您尽可能使 用 UIKit来表示图像。表 4-4列举了一些使用场景及处理这些场景的推荐方法。 表 4-4  图像使用场景 场景 推荐用法 将图像作 为视图的 内容 使用 UIImageView类装载和显示图像。这种方法假定视图的 内容就是一个图像,但您仍然可以在图像视图上面放置其它视 图,用于描画其它控件或内容。 将图像作 为部分视 图的装饰 用 UIImage类装载和描画图像。 将某些位 图数据保 存到图像 对象中 使用 UIGraphicsBeginImageContext函数创建一个新 的、基于图像的图形上下文。在这之后,您就可以将图像内容 描画在上面,然后用 UIGraphicsGetImageFromCurrentImageContext函 数生成一个图像(如果需要的话,您甚至可以继续描画并生成 其它的图像)。在图像创建完成后,可以用 UIGraphicsEndImageContext函数来关闭图形上下文。 如果您更喜欢使用 Core Graphics,则可以用 CGBitmapContextCreate函数创建一个位图的图形上下 文,并在上面描画您的图像内容。画完之后,用 CGBitmapContextCreateImage函数把位图上下文中的 内容创建为一个 CGImageRef类型的图像。您可以直接描画 Core Graphics图像,或者用它来初始化一个 UIImage。 将图像保 存为 JPEG 或 PNG文 件 基于原始的图像数据创建一个 UIImage对象。通过 UIImageJPEGRepresentation或 UIImagePNGRepresentation函数取得一个 NSData对 象,并使用该对象的方法将数据保存为文件。 下面的例子将展示如何从应用程序的 程序包 中装载一个图像。在该图像装载完 成后,您可以将它用于初始化 UIImageView对象、将它保存到磁盘、或者在 视图的 drawRect:方法中进行显式描画。 NSString* imagePath = [[NSBundle mainBundle] pathForResource:@"myImage" ofType:@"png"]; UIImage* myImageObj = [[UIImage alloc] initWithContentsOfFile:imagePath]; 在视图的 drawRect:方法中,您可以使用 UIImage类提供的任何描画方法。 您可以指定希望在视图的什么位置描画图像,从而避免在描画之前进行位置的 转换。假定您将之前装载的图像存储在一个名为 anImage的成员变量中,下 面的代码会将该图像画在视图的 (10, 10) 坐标位置上: - (void)drawRect:(CGRect)rect { // Draw the image [anImage drawAtPoint:CGPointMake(10, 10)]; } 重要提示: 如果您使用 CGContextDrawImage函数来直接描画位图,则在 缺省情况下,图像数据会上下倒置,因为 Quartz图像假定坐标系统的原点在左 下角,且坐标轴的正向是向上和向右。虽然您可以在描画之前对其进行转换, 但是将 Quartz图像包装为一个 UIImage对象是更简单的方法,这样可以自动 补偿坐标空间的差别。有关如何用 Core Graphics创建和描画图像的更多信 息,请参见 Quartz 2D编程指南 。 创建和描画路径 路径用于描述由一序列线和 Bézier曲线构成的 2D几何形状。 UIKit中的 UIRectFrame和 UIRectFill函数(以及其它函数)的功能是在视图中描 画象矩形这样的简单路径。 Core Graphics中也有一些用于创建简单路径(比 如矩形和椭圆形)的便利函数。对于更为复杂的路径,必须用 Core Graphics 框架 提供的函数自行创建。 在创建路径时,需要首先通过 CGContextBeginPath函数配置一个接收路 径命令的图形上下文。调用该函数之后,就可以使用与路径相关的函数来设置 路径的起始点,描画直线和曲线,加入矩形和椭圆形等等。路径的几何形状指 定完成后,就可以直接进行描画,或者将其引用存储在 CGPathRef或 CGMutablePathRef数据类型中,以备后用。 在视图上描画路径时,可以描画轮廓,也可以进行填充,或者同时进行这两种 操作。路径轮廓可以用像 CGContextStrokePath这样的函数来画,即用当 前的笔划颜色画出以路径为中心位置的线。路径的填充则可以用 CGContextFillPath函数来实现,它的功能是用当前的填充颜色或样式填 充路径线段包围的区域。 有关如何描画路径的更多信息,包括如何为复杂路径元素指定点的信息,请参 见 Quartz 2D编程指南 的 路径 部分。有关路径创建函数的信息,则请参见 CGContext参考 和 CGPath参考 。 创建样式、渐变、和阴影 Core Graphics框架 还包含一些用于创建样式、渐变、和阴影类型的函数。基 于这些类型,您可以创建复杂的颜色,并用它们来填充自己创建的路径。样式 是从重复出现的图像或内容创建而来的,渐变和阴影则是不同颜色之间平滑过 渡的方式。 有关创建样式、渐变、和阴影的详细信息,在 Quartz 2D编程指南 中进行讨 论。 用 OpenGL ES进行描画 开放图形库( Open Graphics Library,即 OpenGL) 是一个跨平台的、基 于 C语言的接口,用于在桌面系统中创建 2D和 3D内容。游戏或需要以高帧率 进行描画的开发者通常需要使用这个接口。您可以用 OpenGL函数来指定图元 结构,比如点、线、多边形和纹理,以及增强这些结构外观的特殊效果。您调 用的函数会将图形命令发送给底层的硬件,然后由硬件进行渲染。由于大多数 渲染工作是由硬件来完成,所以 OpenGL的描画速度通常很快。 OpenGL的嵌入式系统版本是 OpenGL的精简版本,是专门为移动设备设计 的,可以充分利用现代图形硬件的优势。如果您希望为基于 iPhone OS的设备 —也就是 iPhone或 iPod Touch—创建 OpenGL内容,就要使用 OpenGL ES。 iPhone OS系统提供的 OpenGL ES框架 (OpenGLES.framework)同时支持 OpenGL ES v1.1和 OpenGL ES v2.0规范。 有关 iPhone OS系统上的 OpenGL ES的更多信息,请参见 iPhone OpenGL ES 编程指南 . 应用 Core Animation的效果 Core Animation是一个 Objective-C语言的 框架 ,其目的是为快速创建实时动 画提供基础设施。 Core Animation本身并不是一个描画技术,因为它并不提 供创建形状、图像、或其它内容的基本例程;相反,它是一种操作和显示由其 它技术创建的内容的技术。 在 iPhone OS上,大多数程序都会以某种形式受益于 Core Animation技术。 动画可以将当前正在发生的事情呈现给用户。比如,在用户使用 Settings程序 时,屏幕会根据用户是向预置的更深层次移动还是返回根结点而滑入或滑出视 图。这种反馈是很重要的,可以为用户提供上下文的信息。动画还可以增强应 用程序的视觉效果。 大多数情况下,您通过很少的工作就可以得到 Core Animation的好处。举例 来说,您可以对 UIView类的几个 属性声明 (其中包括视图的边框、中心、颜 色、和透明度等)进行配置,使得当它们的值发生变化时,可以触发动画效 果。您需要通过少量的工作让 UIKit知道您希望执行哪些动画,但动画的创建和 运行都是自动的。有关如何触发内置视图动画的更多信息,请参见 “视图动 画 ”部分。 如果您要超越基本的动画效果,就必须直接和 Core Animation的类及方法进 行更多的交互。本文的下面部分将进一步提供有关 Core Animation的信息, 向您展示如何用它提供的类和方法创建 iPhone OS上的典型动画。更多有关 Core Animation及其用法的信息,请参见 Core Animation编程指南 。 关于层 Core Animation的关键技术是层对象。层是一种轻量级的对象,在本质上类 似于 视图 ,但实际上是 模型对象 ,负责封装显示内容的几何属性、显示时机、 和视觉 属性变量 。内容本身可以通过如下三种方式来提供: ■ 您可以将一个 CGImageRef类型的数据赋值给层对象的 contents属性 变量 。 ■ 您可以为层分配一个委托,让它负责描画工作。 ■ 您可以从 CALayer派生出子类,并对其显示方法进行重载。 当您操作层对象的属性时,您真正操作的是模型级别的数据,该数据决定了与 之关联的内容应该如何被显示,而实际的渲染则由您的代码之外的模块来处 理,系统对这个过程进行了大量的优化,确保渲染工作能快速完成。您需要做 的只是设置层的内容和配置动画属性,然后让 Core Animation接管剩下的工 作。 更多有关层及如何使用层的信息,请参见 Core Animation编程指南 。 关于动画 对于具有动画效果的层, Core Animation使用独立的动画对象来控制动画的 时机和行为。 CAAnimation类及其子类实现了不同类型的动画行为,供您在 代码中使用。您可以创建简单的动画,将某个 属性变量 从一个值变为另一个 值;也可以创建复杂的关键帧动画,通过您自己提供的值和时间函数来跟踪动 画。 Core Animation还可以将多个动画组合为一个单独的单元,称为事务。 CATransaction对象负责将一组动画组合成一个单元来管理,您也可以用它 提供的方法来设置动画的持续时间。 如果您需要如何创建定制动画的实例,请参见 动画类型和时机的编程指南 。 文本和 Web iPhone OS文本系统的设计者考虑了移动设备用户的基本需求,将文本系统设 计为电子邮件和 SMS程序中常用的单行和多行文本输入控件。文本系统支持 Unicode,且包含几个不同的输入法,方便显示和读取不同语言的文本。 关于文本和 Web的支持 iPhone OS的文本系统提供了大量的功能,同时又非常简单易用。 UIKit框架 中 包含几个高级类,负责管理文本的显示和输入。该框架还含有一个更为高级的 类,用于显示 HTML和基于 JavaScript的内容。 本文的下面部分将描述 iPhone OS对文本和 web内容的基本支持。如果您需要 这里列举的各个类的更多信息,请参见 UIKit框架参考 。 文本视图 UIKit框架提供三个显示文本内容的基本类: ■ UILabel 显示静态文本字符串 ■ UITextField 显示单行可编辑文本 ■ UITextView 显示多行可编辑文本 虽然标签和文本编辑框通常用于显示相对少量的文本,但实际上这些类可以显 示任意数量的文本。然而,基于 iPhone OS的设备的屏幕比较小,为了使显示 在屏幕上的文本便于阅读,这些类不支持像 Mac OS X这样的桌面操作系统上 常见的高级格式功能。另一方面,考虑到可能的需要,这三个类仍然支持指定 字体信息,包括字体的尺寸和风格选项,只是指定的字体会应用到对象中显示 的所有文本。 图 5-1显示了这些文本类在屏幕上的显示实例。这些例子来自 UICatalog示例 程序,该程序演示了 UIKit框架中的很多视图和控件。左图显示的是几个不同风 格的文本输入框,右图则显示一个文本视图。灰色背景中显示的说明文字所在 的视图是一些 UILabel对象,它们被嵌入到负责显示各种视图的表格单元 中。左图的屏幕底部还有一个 UILabel对象,显示内容为 “Left View”。 图 5-1  UICatalog应用程序的文本类 在使用可编辑的文本视图时,您必须提供一个 委托 对象,负责管理编辑会话。 文本视图会向委托对象发送几个不同的 通告 ,让它知道编辑何时开始,何时结 束,并使它有机会重载某些编辑动作。举例来说,委托可以决定当前文本是否 包含有效的值,还可以在需要的时候防止编辑会话被终止。在编辑过程最终结 束的时候,您可以通过委托取得编辑结果,更新应用程序的数据模型。 由于各种文本视图的用法有轻微的不同,所以它们的委托方法也有所不同。为 UITextField类提供支持的委托需要实现 UITextFieldDelegate协议 定 义的方法。类似地,为 UITextView类提供支持的委托需要实现 UITextViewDelegate协议定义的方法。对于上述两种情形,系统并没有 要求您一定要实现协议中的任何方法,但是如果没有实现必要的方法,文本输 入框就没有什么用处了。有关这两个协议的更多信息,请参见 UITextFieldDelegate协议参考 和 UITextViewDelegate协议参考 。 Web视图 UIWebView类使您可以将一个微型 web浏览器集成到应用程序的用户界面 上。 UIWebView类充分使用了 iPhone OS上的 web技术,同样的这些技术也 用于实现 iPhone OS上的 Safari、实现对 HTML、 CSS、和 JavaScript内容的全 面支持。 UIWebView还支持很多用户在 Safari中已经熟悉了的手势,比如通 过双击和双指捏夹( pinch)的手势来放大和缩小页面,还有通过手指拖动来 滚动页面。 除了显示内容,您还可以用 web视图对象来显示 web表单,收集用户输入。和 UIKit的其它文本类相似,如果您在 web页面的表单中有可编辑的文本框,则轻 触该文本框就会弹出键盘,用户可以通过键盘输入文本。这是 web浏览整体体 验的一部分, web视图会自行管理键盘的显示和消除。 图 5-2显示了一个 UIWebView对象的例子,它来自 UICatalog示例程序,该程 序演示了 UIKit框架中的很多视图和控件。这个例子只是显示 HTML内容,如果 您希望用户可以象使用 web浏览器那样在网页之间进行漫游,需要加入一些控 件。比如,图中的 web视图只是占用 URL文本框下面的空间,而不包含文本框 的本身。 图 5-2  web视图 web视图通过其关联的 委托 对象提供有关网页何时被装载、及装载过程是否发 生错误的信息。 web委托是指实现一个或多个 UIWebViewDelegate协议 方 法的对象。您可以通过实现委托方法来响应装载错误或处理一些与装载有关的 其它任务。更多有关 UIWebViewDelegate协议方法的信息请参见 UIWebViewDelegate协议参考 。 键盘和输入法 每当用户触击一个可以接受文本输入的对象时,该对象就会请求系统显示一个 合适的键盘。根据用户程序的需要和偏好的语言,系统可以显示几种不同的键 盘。您的应用程序虽然不能控制用户的偏好语言(因此也不能控制键盘的输入 法),但可以控制键盘的使用属性,比如特殊键的配置及其行为。 您可以直接通过应用程序中的文本对象来配置键盘的属性。 UITextField和 UITextView类都遵循 UITextInputTraits协议 ,该协议定义了一些配置 键盘的属性。在程序或 Interface Builder的查看器窗口中设置这些属性就可以 使系统显示指定类型的键盘。 请注意: 虽然 UIWebView类并不直接支持 UITextInputTraits协议,但 您还是可以配置文本输入元素的一些键盘属性。特别值得一提的是,您可以在 输入元素的定义中包含 autocorrect和 autocapitalization属性,通 过这些属性来指定键盘的行为,如下面的例子所示: 您不能在输入元素中指定键盘的类型。 web视图显示的是缺省的键盘,但包含 一些额外的控制,可以进行表单元素之间漫游。 缺省的键盘配置是为一般的文本输入设计的。图 5-3显示了缺省的和其它的几 个键盘配置。缺省键盘显示的是一个字母键盘,用户可以将它切换为数字和标 点符号键盘。大多数其它键盘在都提供与缺省键盘类似的功能,同时又提供一 些适合于特定任务的其它按键。但是,电话和数字键盘的布局显著不同,它们 是特别为数字输入设计的。 图 5-3  几个不同的键盘类型 为了实现不同的语言偏好, iPhone OS还支持与不同语言相对应的输入法和键 盘布局, 图 5-4显示了部分输入法和布局。输入法和键盘布局是由用户语言偏 好设置决定的。 图 5-4  几个不同的键盘和输入法 管理键盘 虽然很多 UIKit对象在响应用户交互时会自动显示键盘,但您的程序仍然需要配 置和管理键盘。本文的下面部分将描述应用程序在键盘管理方面应该承担的责 任。 接收键盘通告 当键盘被显示或隐藏的时候, iPhone OS会向所有经过注册的观察者对象发出 如下 通告 : ■ UIKeyboardWillShowNotification ■ UIKeyboardDidShowNotification ■ UIKeyboardWillHideNotification ■ UIKeyboardDidHideNotification 当键盘首次出现或者消失,以及键盘的所有者或应用程序的方向发生变化的任 何时候,系统都会发出键盘通告。在上述的各种情况下,系统只发送与具体场 景相关的的消息集合。举例来说,如果键盘的所有者发生变化,系统只向当前 的拥有者发送 UIKeyboardWillHideNotification消息,但不发送 UIKeyboardDidHideNotification消息,因为这个变化不会导致键盘 最终被隐藏。 UIKeyboardWillHideNotification消息只是简单地通知 键盘当前的所有者即将失去键盘焦点。而改变键盘的方向则会使系统发出上述 的两种消息,因为每个方向的键盘是不同的,在显示新的键盘之前,必须先隐 藏原来的键盘。 每个键盘通告都包含键盘在屏幕上的位置和尺寸。您应该使用通告中的信息来 确定键盘的尺寸和位置,而不是假定键盘具有某个特定的尺寸或处于某个特定 的位置。键盘在使用不同输入法时并一定总是一样的,在不同版本的 iPhone OS上也可能会发生变化。另外,即使对于特定的某种语言和某个系统版本,键 盘的尺寸也会因为应用程序方向的不同而不同。作为例子,请看图 5-5显示了 URL键盘在肖像模式和景观模式下的相对尺寸。使用键盘通告中的信息可以确 保得到正确的尺寸和位置信息。 图 5-5  在肖像模式和景观模式下的相对键盘尺寸 请注意: info字典中的 UIKeyboardBoundsUserInfoKey键包含的矩形只 能用于取得尺寸信息,不要将该矩形的原点(它的值总是为 {0.0, 0.0})用于 矩形计算。由于键盘是以动画的形式出现在它的位置上的,其实际的边界尺寸 会随着时间的不同而不同,因此, info 字典中有 UIKeyboardCenterBeginUserInfoKey 和 UIKeyboardCenterEndUserInfoKey两个键,用于保存键盘的起始和终 止的位置,您可以根据这些位置计算出键盘的原点。 使用键盘通告的一个原因是为了重新定位被键盘遮掩的内容。有关如何进行重 新定位的信息,请参见 “移动键盘下面的内容 ”部分。 显示键盘 当用户触击一个视图时,系统就会自动将该视图作为第一响应者。而当这种场 景发生在包含可编辑文本的视图时,该视图就会启动一个文本编辑会话。如果 当前键盘不可见,该视图会在编辑会话刚开始时请求系统显示键盘。如果键盘 已经显示在屏幕上了,第一响应者的改变会导致来自键盘的文本输入被重定向 到用户刚刚触击的视图上。 键盘是在视图变为第一响应者时自动被显示的,因此,您通常不需要为了显示 它而做什么工作。但是,您可以通过调用视图对象的 becomeFirstResponder方法来为可编辑的文本视图显示键盘。调用这个 方法可以使目标视图成为第一响应者,并开始编辑过程,其效果和用户触击该 视图是一样的。 如果您的应用程序在一个屏幕上管理几个基于文本的视图,则需要跟踪当前哪 个视图是第一响应者,以便在需要的时候取消键盘的显示。 取消键盘 虽然键盘通常是自动显示的,但它并不自动取消。相反,您的应用程序需要在 恰当的时机取消键盘。通常情况下,您在响应用户动作的时候进行这样的操 作,比如当用户触击键盘上的 Return或 Done按键、或者触击应用程序界面上 的其它按键时。根据键盘配置的不同,您可能需要在用户界面上加入额外的控 件来取消键盘。 您可以调用作为当前第一响应者的文本视图的 resignFirstResponder方 法来取消键盘。当文本视图失去第一响应者的状态时,就会结束其当前的编辑 会话,将这个变化通知它的 委托 对象,并取消键盘。换句话说,如果您有一个 名为 myTextField的变量,指向一个 UITextField对象,假定该对象是当 前的第一响应者,则可以简单地通过下面的代码来取消键盘: [myTextField resignFirstResponder]; 从这个点之后的所有操作都由文本对象自动处理。 移动键盘下面的内容 当系统收到显示键盘的请求时,就从屏幕的底部滑出键盘,并将它放在应用程 序内容的上方。由于键盘位于您的内容的上面,所以有可能遮掩住用户希望编 辑的文本对象。如果这种情况发生,就必须对内容进行调整,使目标对象保持 可见。 需要做的调整通常包括暂时调整一或多个视图的尺寸和位置,从而使文本对象 可见。管理带有键盘的文本对象的最简单方法是将它们嵌入到一个 UIScrollView(或其子类,如 UITableView)对象。当键盘被显示出来 时,您需要做的只是调整滚动视图的尺寸,并将目标文本对象滚动到合适的位 置。为此,在 UIKeyboardDidShowNotification通告的处理代码中需 要进行如下操作: 1. 取得键盘的尺寸。 2. 将滚动视图的高度减去键盘的高度。 3. 将目标文本框滚动到视图中。 图 5-6演示了一个简单的应用程序如何处理上述的几个步骤。该程序将几个文 本输入框嵌入到 UIScrollView对象中,当键盘出现时,通告处理代码首先 调整滚动视图的尺寸,然后用 UIScrollView 类的 scrollRectToVisible:animated:方法将被触击的文本框滚动到视图 中。 图 5-6  调整内容的位置,使其适应键盘 请注意: 在配置滚动视图时,请务必为所有的内容视图配置恰当的自动尺寸调 整规则。在之前的图中,文本框实际上是一个 UIView对象的子视图,该 UIView对象又是 UIScrollView对象的子视图。如果该 UIView对象的 UIViewAutoresizingFlexibleWidth 和 UIViewAutoresizingFlexibleHeight选项被设置了,则改变滚动视 图的边框尺寸会同时改变它的边框,因而可能导致不可预料的结果。禁用这些 选项可以确保该视图保持尺寸不变,并正确滚动。 程序清单 5-1显示了如何注册接收键盘 通告 和如何实现相应的处理器方法。这 段代码是由负责滚动视图管理的 视图控制器 实现的,其中 scrollView变量 是一个指向滚动视图对象的插座变量。每个处理器方法都从通告的 info对象取 得键盘的尺寸,并根据这个尺寸调整滚动视图的高度。此外, keyboardWasShown:方法的任务是将当前活动的文本框矩形滚入视图,该 文本框对象存储在一个定制变量中(在本例子中名为 activeField),该变 量是视图控制器的一个成员变量,在 textFieldDidBeginEditing:委托 方法中进行赋值,委托方法本身的代码显示在 程序清单 5-2中(在这个例子 中,视图控制器同时也充当所有文本输入框的委托)。 程序清单 5-1  处理键盘通告 // Call this method somewhere in your view controller setup code. - (void)registerForKeyboardNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWasShown:) name:UIKeyboardDidShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWasHidden:) name:UIKeyboardDidHideNotification object:nil]; } // Called when the UIKeyboardDidShowNotification is sent. - (void)keyboardWasShown:(NSNotification*)aNotification { if (keyboardShown) return; NSDictionary* info = [aNotification userInfo]; // Get the size of the keyboard. NSValue* aValue = [info objectForKey:UIKeyboardBoundsUserInfoKey]; CGSize keyboardSize = [aValue CGRectValue].size; // Resize the scroll view (which is the root view of the window) CGRect viewFrame = [scrollView frame]; viewFrame.size.height -= keyboardSize.height; scrollView.frame = viewFrame; // Scroll the active text field into view. CGRect textFieldRect = [activeField frame]; [scrollView scrollRectToVisible:textFieldRect animated:YES]; keyboardShown = YES; } // Called when the UIKeyboardDidHideNotification is sent - (void)keyboardWasHidden:(NSNotification*)aNotification { NSDictionary* info = [aNotification userInfo]; // Get the size of the keyboard. NSValue* aValue = [info objectForKey:UIKeyboardBoundsUserInfoKey]; CGSize keyboardSize = [aValue CGRectValue].size; // Reset the height of the scroll view to its original value CGRect viewFrame = [scrollView frame]; viewFrame.size.height += keyboardSize.height; scrollView.frame = viewFrame; keyboardShown = NO; } 上面程序清单中的 keyboardShown变量是一个布尔值,用于跟踪键盘是否可 见。如果您的用户界面有多个文本输入框,则用户可能触击其中的任意一个进 行编辑。发生这种情况时,虽然键盘并不消失,但是每次开始编辑新的文本框 时,系统都会产生 UIKeyboardDidShowNotification通告。您可以通 过跟踪键盘是否确实被隐藏来避免多次减少滚动视图的尺寸。 程序清单 5-2显示了一些额外的代码,视图控制器用这些代码来设置和清理之 前例子中的 activeField变量。在初始化时,界面中的每个文本框都将视图 控制器设置为自己的委托。因此,当文本编辑框被激活的时候,这些方法就会 被调用。更多关于文本框及其委托通告的信息,请参见 UITextField类参考 。 程序清单 5-2  跟踪活动文本框的方法 - (void)textFieldDidBeginEditing:(UITextField *)textField { activeField = textField; } - (void)textFieldDidEndEditing:(UITextField *)textField { activeField = nil; } 描画文本 除了显示和编辑文本的 UIKit类之外, iPhone OS还包含几个直接在屏幕上描画 文本的方法。描画简单字符串的最简单有效的方法是使用 NSString类的 UIKit扩展,该扩展包含一些在屏幕上描画字符串的方法,并且可以描画时使用 多种属性。还有一些方法,可以在真正描画之前计算渲染字符串所需要的尺 寸,这些方法有助于更加精确布局应用程序的内容。 重要提示: 由于性能上的考虑,您应该尽可能避免直接描画文本。对于静态文 本,通过一或多个 UILabel对象进行描画比使用定制描画例程要高效得多。 类似地, UITextField类也支持不同的风格,这些风格使您更加易于将可编 辑的文本区域集成到您的内容中。 当您需要在界面上描画定制文本字符串时,请使用 NSString方法。 UIKit包 含一些对基本 NSString类的扩展,用于在视图中描画字符串。这些方法使您 可以精确调整文本的位置,以及将文本和视图内容进行融合;这个类的方法还 可以根据指定的字体和风格属性计算文本的包围矩形。更多信息请参见 NSString UIKit扩展参考 。 如果您需要对描画过程中用到的字体有更多的控制,还可以使用 Core Graphics框架中的函数来进行描画。 Core Graphics框架提供的方法可以对字 形和文本进行精确描画和定位。有关这些函数及其用法的更多信息,请参见 Quartz 2D编程指南 和 Core Graphics框架参考 。 在 Web视图中显示内容 如果您的用户界面包含 UIWebView对象,就可以显示本地或网络上的内容。 对于本地的内容,您可以动态创建,也可以使用文件,然后调用 loadData:MIMEType:textEncodingName:baseURL: 或 loadHTMLString:baseURL:方法;如果要从网络加载,则需要创建一个 NSURLRequest对象,然后传递给 web视图对象的 loadRequest:方法。 在发起一个基于网络的请求后,如果由于某种原因必须 释放 web视图,则必须 在释放之前取消待处理的请求。为此,您可以调用 web视图的 stopLoading 方法。通常情况下,您可以在 web 视图的视图控制器的 viewWillDisappear:方法中执行这些代码。如果需要确定一个请求是否 处于等待状态,可以通过 web视图的 loading属性来判断。 文件和网络 运行在 iPhone OS系统上的应用程序可以通过各种 Core OS和 Core Services框 架 来访问本地的文件系统和网络。读写本地文件系统的能力使您可以保存用户 数据和应用程序状态,以备后用;而访问网络的能力则使您可以和网络服务器 进行交流,进而实现远程操作的执行和数据的收发。 文件和数据管理 iPhone OS系统上的文件和用户的媒体数据及个人文件共享闪存上的空间。出 于安全的目的,您的应用程序被放在其自己的目录下,并且只能对该目录进行 读写。本章的下面部分将描述应用程序本地文件系统的结构及几个读写文件的 技术。 常用目录 出于安全的目的,应用程序只能将自己的数据和偏好设置写入到几个特定的位 置上。当应用程序被安装到设备上时,系统会为其创建一个家目录。表 6-1列 出了应用程序家目录下的一些重要子目录,您的程序可能需要对其进行访问。 表中还描述了每个目录的设计目的和访问限制,以及 iTunes是否对该目录下的 内容进行备份。有关备份和恢复过程的更多信息,请参见 “备份和恢复 ” 部分; 有关应用程序家目录本身的信息,则请参见 “应用程序沙箱 ”部分。 表 6-1  iPhone应用程序的目录 目录 描述 / AppName.app 这是 程序包 目录,包含应用程序的本身。由于应用程序 必须经过签名,所以您在运行时不能对这个目录中的内 容进行修改,否则可能会使应用程序无法启动。 在 iPhone OS 2.1及更高版本的系统, iTunes不对这个 目录的内容进行备份。但是, iTunes会对在 App Store 上购买的应用程序进行一次初始的同步。 / Documents/ 您应该将所有的应用程序数据文件写入到这个目录下。 这个目录用于存储用户数据或其它应该定期备份的信 息。有关如何取得这个目录路径的信息,请参见 “获取 应用程序目录的路径 ”部分。 iTunes会备份这个目录的内容。 / Library/ Preferences 这个目录包含应用程序的偏好设置文件。您不应该直接 创建偏好设置文件,而是应该使用 NSUserDefaults 类或 CFPreferences API来取得和设置应用程序的偏 好,详情请参见 “添加 Settings程序包 ”部分。 iTunes会备份这个目录的内容。 / Library/ Caches 这个目录用于存放应用程序专用的支持文件,保存应用 程序再次启动过程中需要的信息。您的应用程序通常需 要负责添加和删除这些文件,但在对设备进行完全恢复 的过程中, iTunes会删除这些文件,因此,您应该能够 在必要时重新创建。您可以使用 “获取应用程序目录的 路径 ” 部分描述的接口来获取该目录的路径,并对其进 行访问。 在 iPhone OS 2.2及更高版本, iTunes不对这个目录的 内容进行备份。 /tmp/ 这个目录用于存放临时文件,保存应用程序再次启动过 程中不需要的信息。当您的应用程序不再需要这些临时 文件时,应该将其从这个目录中删除(系统也可能在应 用程序不运行的时候清理留在这个目录下的文件)。有 关如何获得这个目录路径的信息,请参见 “获取应用程 序目录的路径 ”部分。 在 iPhone OS 2.1及更高版本, iTunes不对这个目录的 内容进行备份。 备份和恢复 您不需要在应用程序中为备份和恢复操作做任何准备。在 iPhone OS 2.2及更 高版本的系统中,当设备被连接到计算机并完成同步时, iTunes会对除了下面 这些目录之外的所有文件进行增量式的备份: ■ /AppName.app ■ /Library/Caches ■ /tmp 虽然 iTunes确实对应用程序的 程序包 本身进行备份,但并不是在每次同步时都 进行这样的操作。通过设备上的 App Store购买的应用程序在下一次设备和 iTunes同步时进行备份。而在之后的同步操作中,应用程序并不进行备份,除 非应用程序包本身发生了变化(比如由于应用程序被更新了)。 为了避免同步过程花费太长时间,您应该有选择地往应用程序家目录中存放文 件。 /Documents目录应该用于存放用户数据文件或 不容易在应用程序中重新创建的文件。存储临时数据的文件应该放在 Application Home/tmp目录,而且应该在不需要的时候将其删除。如果您的 应用程序需要创建用于下次启动的数据文件,则应该将那些文件放到 Application Home/Library/Caches目录下。 请注意: 如果您的应用程序需要创建数据量大或频繁变化的文件,则应该考虑 将它们存储在 Application Home/Library/Caches目录下,而不是 /Documents目录。备份大数据文件会使备份过程显 著变慢,备份频繁变化(因此必须频繁备份)的文件也同样如此。将这些文件 放到 Caches目录下可以避免每次同步都对其进行备份(在 iPhone OS 2.2及 更高版本)。 有关如何在应用程序中使用目录的更多信息,请参见 表 6-1。 在应用程序更新过程中被保存的文件 更新应用程序就是将用户下载的新版应用程序代替之前的版本。在这个过程 中, iTunes会将更新过的应用程序安装到新的应用程序目录下,并在删除老版 本之前,将用户数据文件转移到新的应用程序目录下。在更新的过程中, iTunes保证如下目录中的文件会得以保留: ■ /Documents ■ /Library/Preferences 虽然其它用户目录下的文件也可能被转移,但是您不应该假定更新之后该文件 还仍然存在。 Keychain数据 keychain是一个安全、经过加密保护的容器,用于保存密码和其它秘密信息。 应用程序的 keychain数据存储在应用程序沙箱之外。如果应用程序被卸载,则 该数据会自动被删除。当用户通过 iTunes备份应用程序数据时, keychain数 据也会被备份。然而, keychain数据只能被恢复到之前做备份的设备上。应用 程序的更新并不影响其 keychain数据。 有关 iPhone OS keychain的更多信息,请参见 Keychain服务编程指南 文档中 的 “Keychain服务的概念 ”部分。 获取应用程序目录的路径 系统在各个级别上都提供了用于获取应用程序沙箱目录路径的编程方法。然 而,取得这些路径的推荐方式还是使用 Cocoa 编程接口。 NSHomeDirectory函数(在 Foundation框架中)负责返回顶级家目录的路 径 —也就是包含应用程序、 Documents、 Library、和 tmp目录的路径。除 了这个函数,您还可以用 NSSearchPathForDirectoriesInDomains 和 NSTemporaryDirectory函数来取得 Documents、 Caches、和 tmp 目录的准确路径。 NSHomeDirectory和 NSTemporaryDirectory函数都通过 NSString 对象返回正确格式的路径。您可以通过 NSString类提供的与路径相关的方法 来修改路径信息或创建新的路径字符串。举例来说,在取得临时的目录路径之 后,您可以附加一个文件名,并用结果字符串在临时目录下创建给定名称的文 件。 请注意: 如果您使用带有 ANSI C编程接口的框架 —包括那些接受路径参数的接 口 —请记住 NSString对象和其在 Core Foundation框架中的等价类型之间 是 “免费桥接 ”的。这意味着您可以将一个 NSString对象(比如上述某个函数 的返回结果)强制类型转换为一个 CFStringRef类型,如下面的例子所示: CFStringRef homeDir = (CFStringRef)NSHomeDirectory(); 有关免费桥接的更多信息,请参见 Carbon-Cocoa集成指南文档。 Foundation框架中的 NSSearchPathForDirectoriesInDomains函数 用于取得几个应用程序相关目录的全路径。在 iPhone OS上使用这个函数时, 第一个参数指定正确的搜索路径常量,第二个参数则使用 NSUserDomainMask常量。表 6-2列出了大多数常用的常量及其返回的目 录。 表 6-2  常用的搜索路径常量 常量 目录 NSDocumentDirecto ry /Documents NSCachesDirectory /Library/Caches NSApplicationSupp ortDirectory /Library/ Application Support 由于 NSSearchPathForDirectoriesInDomains函数最初是为 Mac OS X设计的,而 Mac OS X上可能存在多个这样的目录,所以它的返回值是一个路 径数组,而不是单一的路径。在 iPhone OS上,结果数组中应该只包含一个给 定目录的路径。程序清单 6-1显示了这个函数的典型用法。 程序清单 6-1 取得指向应用程序 Documents目录的文件系统路径 NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; 在调用 NSSearchPathForDirectoriesInDomains函数时,您可以使 用 NSUserDomainMask之外的其它域掩码参数,或者使用 表 6-2之外的其它 目录常量,但是应用程序不能向其返回的目录写入数据。举例来说,如果您指 定 NSApplicationDirectory 作为目录参数,同时指定 NSSystemDomainMask作为域掩码参数,则可以返回(设备上的) / Applications路径,但是,您的应用程序不能往该位置写入任何文件。 另外一个需要记住的考虑是,不同平台的目录位置是不一样的。 NSSearchPathForDirectoriesInDomains、 NSHomeDirectory、 NSTemporaryDirectory、和其它类似函数的返回路径取决于应用程序运 行在设备还是仿真器上。作为例子, 程序清单 6-1上显示的函数调用在设备上 返回的路径( documentsDirectory)大致如下: /var/mobile/Applications/30B51836-D2DD-43AA- BCB4-9D4DADFED6A2/Documents 但是,它在仿真器上返回的路径则具有如下的形式: /Volumes/Stuff/Users/johnDoe/Library/Application Support/iPhone Simulator/User/Applications/118086A0- FAAF-4CD4-9A0F-CD5E8D287270/Documents 在读写用户偏好设置时,请使用 NSUserDefaults类或 CFPreferences API。这些接口使您免于构造 Library/Preferences/目录路径和直接读写 偏好文件。有关使用这些接口的更多信息,请参见 “添加 Settings程序包 ”部 分。 如果应用程序的程序包中包含声音、图像、或其它资源,则应该使用 NSBundle类或 CFBundleRef封装类型来装载那些资源。程序包知道应用程 序内部资源应该在什么位置上,此外,它还知道用户的语言偏好,能够自动选 择本地化的资源。有关程序包的更多信息,请参见 “应用程序的程序包 ”部分。 文件数据的读写 iPhone OS提供了如下几种读、写、和管理文件的方法: ■ Foundation框架 : 1 如果您可以将应用程序数据表示为一个 属性列表 ,则可以用 NSPropertyListSerialization API来将属性列表转换为一 个 NSData对象,然后通过 NSData类的方法将数据对象写入磁 盘。 2 如果应用程序的 模型对象 采纳了 NSCoding协议,则可以通过 NSKeyedArchiver 类、特别是它的 archivedDataWithRootObject:方法将模型对象图进行归 档。 3 Foundation框架中的 NSFileHandle类提供了随机访问文件内容 的方法。 4 Foundation框架中的 NSFileManager类提供了在文件系统中创 建和操作文件的方法。 ■ Core OS调用: 1 诸如 fopen、 fread、和 fwrite这些调用可以用于对文件进行 顺序或随机读写。 2 mmap和 munmap调用是将大文件载入内存并访问其内容的有效方 法。 请注意: 上面的 Core OS调用列表只是列举一些较为常用的例子。更完全的可 用函数列表请参见 iPhone OS手册 的第三部分中的函数列表。 本章的下面部分将描述如何使用一些高级技术来进行文件的读写。有关 Foundation框架中与文件相关类的更多信息,请参见 Foundation框架参考 。 属性列表数据的读写 属性列表 是一种数据表示形式,用于封装几种 Foundation(及 Core Foundation)的数据类型,包括 字典、数组 、 字符串、日期、二进制数据、 数值及布尔值 。属性列表通常用于存储结构化的配置数据。举例来说,每个 Cocoa和 iPhone应用程序中都有一个 Info.plist文件,它就是用于存储应 用程序本身配置信息的属性列表。您自己也可以用属性列表来存储其它信息, 比如应用程序退出时的状态等。 在代码中,属性列表的构造通常从构造一个字典或数组、并将它作为容器对象 开始,然后在容器中加入其它的属性列表对象,(可能)包含其它的字典和数 组。字典的键必须是字符串对象,键的值则是 NSDictionary、 NSArray、 NSString、 NSDate、 NSData、和 NSNumber类的实例。 对于可以将数据表示为属性列表对象的应用程序(比如 NSDictionary对 象),您可以用程序清单 6-2所示的方法来将属性列表写入磁盘。该方法将属 性列表序列化为 NSData 对象,然后调用 writeApplicationData:toFile:方法(其实现如 程序清单 6-4所示) 将数据写入磁盘。 程序清单 6-2  将属性列表对象转换为 NSData对象并写入存储 - (BOOL)writeApplicationPlist:(id)plist toFile:(NSString *)fileName { NSString *error; NSData *pData = [NSPropertyListSerialization dataFromPropertyList:plist format:NSPropertyListBinaryFormat_v1_0 errorDescription:&error]; if (!pData) { NSLog(@"%@", error); return NO; } return ([self writeApplicationData:pData toFile:(NSString *) fileName]); } 在 iPhone OS系统上保存属性列表文件时,采用二进制格式进行存储是很重要 的。在编码时,可以通过为 dataFromPropertyList:format:errorDescription: 方法的 format 参数指定 NSPropertyListBinaryFormat_v1_0值来实现。二 进制格式比其它基于文本的格式紧凑得多,这种紧凑不仅使属性列表在用户设 备上占用的空间最小,还可以减少读写属性列表的时间。 程序清单 6-3的代码展示了如何从磁盘装载属性列表,并重新生成属性列表中 的对象。 程序清单 6-3 从应用程序的 Documents目录读取属性列表对象 - (id)applicationPlistFromFile:(NSString *)fileName { NSData *retData; NSString *error; id retPlist; NSPropertyListFormat format; retData = [self applicationDataFromFile:fileName]; if (!retData) { NSLog(@"Data file not returned."); return nil; } retPlist = [NSPropertyListSerialization propertyListFromData:retData mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&error]; if (!retPlist){ NSLog(@"Plist not returned, error: %@", error); } return retPlist; } 有关属性列表和 NSPropertyListSerialization类的更多信息,请参见 属性列表编程指南 。 用归档器进行数据读写 归档器 的作用是将任意的对象集合转换为字节流。这听起来像是 NSPropertyListSerialization类采用的过程,但它们之间有一个重要 的区别。属性列表序列化只能转换一个有限集合的数据类型(大多数是数量类 型),而归档器可以转换任意的 Objective-C对象、数量类型、数组、结构、 字符串、及更多其它类型。 归档过程的关键在于目标对象的本身。归档器操作的对象必须遵循 NSCoding 协议 ,该协议定义了读写对象状态的接口。归档器在编码一组对象时,会向每 个对象发送一个 encodeWithCoder:消息,目标对象则在这个方法中将自身 的关键状态信息写入到对应的档案中。解档过程的信息流与此相反,在解档过 程中,每个对象都会接收到一个 initWithCoder:消息,用于从档案中读取 当前状态信息,并基于这些信息进行初始化。解档过程完成后,字节流就被重 新组成一组与之前写入档案时具有相同状态的新对象。 Foundation框架支持两种归档器 —顺序归档和基于键的归档。基于键的归档 器更加灵活,是应用程序开发中推荐使用的归档器。下面的例子显示如何用一 个基于键的归档器对一个对象图进行归档。 _myDataSource对象的 representation方法返回一个单独的对象(可能是一个数组或字典),指 向将要包含到档案中的所有对象,之后该数据对象就被写入由 myFilePath 变量指定路径的文件中。 NSData *data = [NSKeyedArchiver archivedDataWithRootObject:[_myDataSource representation]]; [data writeToFile:myFilePath atomically:YES]; 请注意: 您还可以向 NSKeyedArchiver 对象发送 archiveRootObject:toFile:消息,以便在一个步骤中完成档案的创建 和将档案写入存储。 您可以简单地通过相反的流程来装载磁盘上的档案内容。在装载磁盘数据之 后,可以通过 NSKeyedUnarchiver 类及其 unarchiveObjectWithData:类方法来取回 模型对象 图。例如,您可以 用下面的代码来解档之前例子中的数据: NSData* data = [NSData dataWithContentsOfFile:myFilePath]; id rootObject = [NSKeyedUnarchiver unarchiveObjectWithData:data]; 更多如何使用归档器和如何使对象支持 NSCoding协议的信息,请参见 Cocoa 的归档和序列化编程指南 。 将数据写到 Documents目录 有了封装应用程序数据的 NSData对象(或者是档案,或者是序列化了的属性 列表)之后,您就可以调用程序清单 6-4所示的方法来将数据写到应用程序的 Documents目录中。 程序清单 6-4  将数据写到应用程序的 Documents目录 - (BOOL)writeApplicationData:(NSData *)data toFile:(NSString *)fileName { NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; if (!documentsDirectory) { NSLog(@"Documents directory not found!"); return NO; } NSString *appFile = [documentsDirectory stringByAppendingPathComponent:fileName]; return ([data writeToFile:appFile atomically:YES]); } 从 Documents目录读取数据 为了从应用程序的 Documents目录读取文件,您首先需要根据文件名构建相 应的路径,然后以期望的方法将文件内容读入内存。对于相对较小的文件 —也 就是尺寸小于几个内存页面的文件 —您可以用程序清单 6-5中的代码来取得文 件内容。该代码首先为 Documents目录下的文件构建一个全路径,并为这个 路径创建一个数据对象,然后返回。 程序清单 6-5  从应用程序的 Documents目录读取数据 - (NSData *)applicationDataFromFile:(NSString *)fileName { NSArray *paths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; NSString *appFile = [documentsDirectory stringByAppendingPathComponent:fileName]; NSData *myData = [[[NSData alloc] initWithContentsOfFile:appFile] autorelease]; return myData; } 对于载入时需要多个内存页面的文件,应该避免一次性地装载整个文件。如果 您只是计划使用部分文件,这一点就尤其重要。对于大文件,您应该考虑用 mmap函数或 NSData的 initWithContentsOfMappedFile:方法来将文 件映射到内存。 到底是采用映射文件还是直接装载取决于您的考虑。如果只需要少量( 3-4) 内存页面,则将整个文件载入内存相对安全一些。但是,如果您的文件需要数 十或上百个页面,则将文件映射到内存可能更为有效一些。当然,无论采用什 么方法,您都应该测量应用程序的性能,确定装载文件和为其分配必要内存需 要多长时间。 文件访问的指导原则 在您创建文件或写入文件数据时,请记住下面这些指导原则: ■ 使写入磁盘的数据量尽可能少。文件操作速度相对较慢,且涉及到 Flash 盘的写操作,有一定的寿命限制。下面这些具体的小贴士可以帮助您最 少化与文件相关的操作: 1 只写入发生变化的文件部分,但要尽可能对变化进行累计,避免在 只有少数字节发生改变时对整个文件进行写操作。 2 在定义文件格式时,将频繁变化的内容放在一起,以便使每次需要 写入磁盘的总块数最少。 3 如果您的数据是需要随机访问的结构化内容,则可以将它们存储在 Core Data持久仓库或 SQLite数据库中。如果您处理的数据量可能 增长到数兆以上,这一点尤其重要。 ■ 避免将缓存文件写入磁盘。这个原则的唯一例外是:在应用程序退出 时,您需要写入某些状态信息,使程序在下次启动时可以回到之前的状 态。 保存状态信息 当用户按下 Home键时, iPhone OS会退出您的应用程序,返回到 Home屏 幕。类似地,如果您的应用程序打开一个由其它应用程序处理的 URI模式, iPhone OS也会退出您的应用程序,在相应的应用程序上打开该 URI。换句话 说,在 Mac OS X上引起应用程序挂起或转向后台的动作,在 iPhone OS上都会 使其退出。这些动作在移动设备上经常发生,因此,您的应用程序必须改变管 理可变数据和程序状态的方式。 大多数桌面应用程序由用户手工选择将文件存入磁盘的时机,与此不同的是, iPhone应用程序应该在工作流的关键点上自动保存已发生的变化。究竟何时保 存数据由您自己来决定,但是有两个潜在的时间点:或者在用户做出改变之后 马上进行保存;或者将同一页面上的变化累计成批,然后在退出该页面、显示 新页面、或者应用程序退出的时候进行保存。在任何情况下,您不应该让用户 漫游到新的页面而不保存之前页面的内容。 当您的应用程序被要求退出时,应该将当前状态保持到临时的缓存文件或偏好 数据库中。在用户下次启动应用程序时,可以根据这些信息将程序恢复到之前 的状态。您保持的状态信息应该尽可能少,但同时又足够使应用程序恢复到恰 当的点。您不必一定要显示用户上次退出时操作的页面,如果那样做并不合理 的话。比如,如果一个用户在编辑某个联系人的时候离开了 Phone程序,那么 在下次运行时, Phone程序显示的是联系人的顶级列表,而不是该联系人的编 辑屏幕。 大小写敏感性 iPhone OS设备的文件系统是大小写敏感的。在处理文件名的任何时候,您都 应该确保大小写准确匹配,否则可能不能打开或访问文件。 网络 iPhone OS的网络栈中包含几个基于( iPhone和 iPod touch设备上的)无线通 讯硬件的编程接口。主编程接口是 CFNetwork框架 ,该框架在 BSD套接字和 Core Foundation框架的封装类型之上,实现了网络实体间的通讯。您也可以 用 Foundation框架的 NSStream类和位于系统 Core OS层中的 BSD套接字来进 行通讯。 本文的下面部分将为需要集成网络功能的开发者提供一些专门针对 iPhone的贴 士。有关如何通过 CFNetwork框架实现网络通讯的信息,请参见 CFNetwork 编程指南 和 CFNetwork框架参考 ;有关如何使用 NSStream类的信息,则请 参见 Foundation框架参考 。 有效进行网络通讯的贴士 在实现收发网络数据的代码时,请记住这是设备上最耗电的操作之一。最少化 收发数据的时间有助于提高电池的使用寿命。为此,您在编写与网络相关的代 码时需要考虑如下贴士: ■ 对于您自己控制的协议,请将数据格式定义得尽可能紧凑。 ■ 避免使用聊天式的协议进行通讯。 ■ 在任何可能的时候,将数据包成群传输。 蜂窝网和 Wi-Fi无线网都被设计为在没有数据传输活动时关闭电源。然而,根 据无线网络的不同,这样做可能需要花几秒钟的时间。如果您的应用程序每隔 数秒就发送少量的数据,则即使无线装置实际上并没做什么,也会一直保持电 源打开,持续耗电。相比于经常性地传输少量数据,一次性传递所有数据或间 隔时间较长但每次传递数据量较大是更好的选择。 在进行网络通讯时,意识到数据包在任何时候都可能丢失是很重要的。在编写 网络通讯代码时,请务必在出现错误时进行处理,使程序尽可能强壮。实现响 应网络条件变化的处理程序是完全合理的,但如果这些处理程序始终没有被调 用,也不要觉得奇怪。举例来说,在网络服务消失时, Bonjour的网络回调函 数并不总是立即被调用。当接收到某个服务即将消失的通告时, Bonjour系统 服务确实立即调用浏览回调函数( browsing callbacks),然而,网络服务可 能没有通告就消失了,如果设备提供的网络服务意外地丢掉网络连接,或者通 告在传递中丢失,就可能出现这种情况。 使用 Wi-Fi 如果您的应用程序通过 Wi-Fi无线信号访问网络,则必须将这个事实通知系 统,即在应用程序的 Info.plist 文件中包含 UIRequiresPersistentWiFi键。包含这个键使系统知道在检测到活动 的 Wi-Fi 热区时应该弹出网络选择框,同时还使系统知道在您的应用程序运行 时不应试图关闭 Wi-Fi硬件。 为了防止 Wi-Fi硬件消耗太多的电能, iPhone OS内置一个定时器,如果在 30 分钟内没有应用程序通过 UIRequiresPersistentWiFi键请求使用 Wi- Fi,就会完全关闭该硬件。如果用户启动某个包含该键的应用程序,则在该程 序的生命周期中, iPhone OS会有效地禁用该定时器。但是一旦该程序退出, 系统就会重新启用该定时器。 请注意: 即使 UIRequiresPersistentWiFi键的值为 true,在设备空闲 (也就是处于屏幕锁定状态)时也是没有效果的。在那种情况下,应用程序被 认为是不活动的,虽然它可能在某些级别上还在工作,但没有 Wi-Fi连接。 有关 UIRequiresPersistentWiFi键及 Info.plist文件中其它键的更 多信息,请参见 “信息属性列表 ”部分。 飞行模式警告 当应用程序启动时,如果设备处于飞行模式,系统可能会显示一个对话框通知 用户。系统仅在下面的所有条件都满足时才会显示这个通知对话框: ■ 应用程序的信息 属性列表 (Info.plist) 文件包含 UIRequiresPersistentWiFi键,且该键的值被设置为 true。 ■ 应用程序启动的同时设备处于飞行模式。 ■ 在切换到飞行模式后设备上的 Wi-Fi还没有被手工激活。 多媒体支持 无论多媒体功能在您的应用程序中是处于中心地位,还是偶尔被使用, iPhone 用户都期望有很高的品质。视频应该充分利用设备携带的高分辨率屏幕和高帧 率,而引人注目的音频也会对应用程序的总体用户体验有不可估量的增强作 用。 您可以利用 iPhone OS的多媒体 框架 来为应用程序加入下面这些功能: ■ 高品质的音频录制和回放 ■ 生动的游戏声音 ■ 实时的声音聊天 ■ 用户 iPod音乐库内容的回放 ■ 在支持的设备上进行视频的回放和录制 本章将介绍 iPhone OS上为应用程序添加音视频功能的多媒体技术。 在 iPhone OS上使用声音 iPhone OS为应用程序提供一组丰富的声音处理工具。根据功能的不同,这些 工具被安排到如下的框架中: ■ 如果希望用简单的 Objective-C接口进行音频的播放和录制,可以使用 AV Foundation框架。 ■ 如果要播放和录制带有同步能力的音频、解析音频流、或者进行音频格 式转换,可以使用 Audio Toolbox框架。 ■ 如果要连接和使用音频处理插件,可以使用 Audio Unit框架。 ■ 如果希望在游戏和其它应用程序中回放位置音频,需要使用 OpenAL框 架。 iPhone OS对 OpenAL 1.1的支持是建立在 Core Audio基础上的。 ■ 如果希望播放 iPod库中的歌曲、音频书、或音频播客,需要使用 Media Player框架中的 iPod媒体库访问接口。 Core Audio框架(和其它音频框架对等)中提供所有 Core Audio服务需要使 用的数据类型。 本部分将就如何着手实现各种音频功能提供一些指导,如下表所示: ■ 播放用户 iPod库中的歌曲、音频播客、以及音频书,请参见 “用 iPod媒体 库访问接口播放媒体项 ”部分。 ■ 播放警告及用户界面声音效果,或者使具有震动功能的设备发生震动, 可以使用系统声音服务,具体请参见 “使用系统声音服务播放短声音及激 活震动 ”部分。 ■ 如果要用最少量的代码播放和录制音频,可以使用 AV Foundation框 架,具体参见 “通过 AVAudioPlayer类轻松播放声音 ”及 “用 AVAudioRecorder类进行音频录制 ”部分。 ■ 如果需要提供全功能的音频回放,包括立体声定位、音量控制、和同期 声( simultaneous sounds),可以使用 OpenAL,具体参见 “使用 OpenAL播放和定位声音 ”部分。 ■ 如果要提供最低延迟的音频,特别是需要同时进行音频输入输出(比如 VoIP应用程序)时,请使用 I/O音频单元,具体请参见 “iPhone OS中的 音频单元支持 ”部分。 ■ 如果您播放的声音需要精确的控制(包括同步),可以使用音频队列服 务,具体参见 “用音频队列服务播放和控制声音 ”部分,音频队列服务还支 持音频录制,具体请见 “用音频队列服务进行音频录制 ”部分的描述。 ■ 如果需要解析来自网络连接的音频流,请使用音频文件流服务,具体参 见 “解析音频流 ”部分。 请务必阅读本文接下来的部分,即 “基础:硬件编解码器、音频格式、和音频会 话 ”部分,以了解在基于 iPhone OS的设备上音频工作机制的关键信息;而且也 请您阅读 “iPhone音频的最佳实践 ”部分,该部分提供了一些指导原则,并列举 了一些能得到最好性能和最佳用户体验的音频和文件格式。 当您准备好进一步学习时,请访问 iPhone Dev Center。这个开发者中心包含 各种指南文档、实例代码、及更多其它信息。有关如何执行常见音频任务的贴 士,请参见 音频 &视频编程的 How-To's部分;如果需要 iPhone OS音频开发的 深入解释,则请参见 Core Audio概述 、 音频队列服务编程指南 、和 音频会话 编程指南 。 基础:硬件编解码器、音频格式、和音频会话 在开始 iPhone音频开发之前,了解 iPhone OS设备的一些硬软件架构知识是很 有帮助的。 iPhone音频硬件编解码 iPhone OS的应用程序可以使用广泛的音频数据格式。从 iPhone OS 3.0开 始,这些格式中的大多数都可以支持基于软件的编解码。您可以同时播放多路 各种格式的声音,虽然出于性能的考虑,您应该针对给定的场景选择最佳的格 式。通常情况下,硬件解码带来的性能影响比软件解码要小。 下面这些 iPhone OS音频格式可以利用硬件解码进行回放: ■ AAC ■ ALAC (Apple Lossless) ■ MP3 通过硬件,设备每次只能播放这些格式中的一种。举例来说,如果您正在播放 的是 MP3立体声,则第二个同时播放的 MP3声音就只能使用软件解码。类似 地,您不能通过硬件同时播放一个 AAC声音和一个 ALAC声音。如果 iPod应用 程序正在后台播放 AAC声音,则您的应用程序只能使用软件解码来播放 AAC、 ALAC、和 MP3音频。 为了以最佳性能播放多种声音,或者为了在 iPod程序播放音乐的同时能更有效 地播放声音,可以使用线性 PCM(无压缩)或者 IMA4(有压缩)格式的音 频。 如果需要了解如何检测设备硬软件编解码器是否可用,请查阅 音频格式服务参 考 中有关 kAudioFormatProperty_HardwareCodecCapabilities 常量的讨论。 音频回放和录制格式 下面是一些 iPhone OS支持的音频回放格式: ■ AAC ■ HE-AAC ■ AMR (Adaptive Multi-Rate,是一种语音格式 ) ■ ALAC (Apple Lossless) ■ iLBC (互联网 Low Bitrate Codec,另一种语音格式 ) ■ IMA4 (IMA/ADPCM) ■ 线性 PCM (无压缩 ) ■ µ-law和 a-law ■ MP3 (MPEG-1 音频第 3层 ) 下面是一些 iPhone OS支持的音频录制格式: ■ ALAC (Apple Lossless) ■ iLBC (互联网 Low Bitrate Codec,用于语音 ) ■ IMA/ADPCM (IMA4) ■ 线性 PCM ■ µ-law和 a-law 下面的列表总结了 iPhone OS如何支持单路或多路音频格式: ■ 线性 PCM和 IMA4 (IMA/ADPCM) 在 iPhone OS上,您可以同时播放多 路线性 PCM或 IMA4声音,而不会导致 CPU资源的问题。这一点同样适用 于 AMR和 iLBC语音品质格式,以及 µ-law和 a-law压缩格式。在使用压 缩格式时,请检查声音的品质,确保满足您的需要。 ■ AAC、 MP3、和 ALAC (Apple Lossless) AAC、 MP3、和 ALAC声音 的回放可以使用 iPhone OS设备上高效的硬件解码,但是这些编解码器 共用一个硬件路径,通过硬件,设备每次只能播放上述格式的一种。 AAC、 MP3、和 ALAC的回放共用同一硬件路径的事实会对 “合作播放 ”风格的 应用程序(比如虚拟钢琴)产生影响。如果用户在 iPod程序上播放上述三种格 式之一的音频,则您的应用程序 —如果要和该音频一起播放声音 —需要使用软 件解码。 音频会话 Core Audio的音频会话接口(具体描述请见 音频会话服务参考 )使应用程序 可以为自己定义一般的音频行为,并在更大的音频上下文中良好工作。您能够 影响的行为有: ■ 您的音频在 Ring/Silent切换过程中是否变为无声 ■ 在屏幕锁定状态时您的音频是否停止 ■ 当您的音频开始播放时, iPod音频是继续播放,还是变为无声 更大的音频上下文包括用户所做的改变,比如用户插入耳机,处理 Clock和 Calendar这样的警告事件,或者处理呼入的电话。通过音频会话,您可以对这 样的事件做出恰当的响应。 音频会话服务提供了三种编程特性,如表 7-1所述。 表 7-1 音频会话接口提供的特性 音频会话 特性 描述 范畴 范畴是标识一组应用程序音频行为的键。您可以通过范畴的设 置来指示自己希望得到的音频行为,比如希望在屏幕锁定状态 时继续播放音频。 中断和路 由变化 当您的音频发生中断或中断结束,以及当硬件音频路由发生变 化时,音频会话会发出 通告 ,使您可以优雅地响应发生在更大 音频环境中的变化 —比如由于电话呼入而导致的中断。 硬件特征 您可以通过查询音频会话来了解应用程序所在的设备的特征, 比如硬件采样率,硬件通道数量,以及是否有音频输入。 AVAudioSession类参考 和 AVAudioSessionDelegate协议参考 描述了一个管 理音频会话的精简接口。如果要使音频会话支持中断,则可以直接使用基于 C 语言的音频会话服务接口,该接口的描述请见 音频会话服务参考 。在应用程序 中,这两个接口的代码可以混用及互相匹配。 音频会话带有一些缺省的行为,可以作为开发的起点。但是,除了某些特殊的 情况之外,采用缺省行为的音频应用程序并不适合发行。您需要通过配置和使 用音频会话来表达自己使用音频的意图,响应 OS级别的音频变化。 举例来说,在使用缺省的音频会话时,如果出现 Auto-Lock超时或屏幕锁定, 应用程序的音频就会停止。如果您希望在屏幕被锁定时继续播放音频,则必须 将下面的代码包含到应用程序的 初始化 代码中: [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback error: nil]; [[AVAudioSession sharedInstance] setActive: YES error: nil]; AVAudioSessionCategoryPlayback范畴确保音频的回放可以在屏幕锁 定时继续。激活音频会话会使指定的范畴也被激活。范畴的详细信息请参见 音 频会话编程指南 中的 音频会话范畴 部分。 如何处理呼入电话或时钟警告引起的中断取决于您使用的音频技术,如表 7-2 所示。 表 7-2  处理音频中断 音频技术 中断如何工作 系统声音服务 当中断开始时,系统声音和警告声音会变为无声。如 果中断结束 —当用户取消警告或选择忽略呼入电话 时,会发生这种情况 —它们就又自动变为可用。使用 这种技术的应用程序无法影响声音中断的行为。 音频队列服务、 OpenAL、 I/O音频 单元 这些技术为中断的处理提供最大的灵活性。您需要编 写一个中断监听回调函数,具体描述请参见 音频会话 编程指南 中的 “响应音频中断 ”部分。 AVAudioPlayer 类 AVAudioPlayer类为中断的开始和结束提供了 委 托 方法。根据实际的需要,您可以在 audioPlayerBeginInterruption:方法中更 新用户界面,音频播放器对象会负责暂停回放。您也 可以利用 audioPlayerEndInterruption:方 法来重启音频的回放,并在必要时更新用户界面。音 频播放器会负责重新激活您的音频会话。 每个 iPhone OS应用程序 —除了很少的例外 —都应该采纳音频会话服务。如果 需要了解具体的用法,请阅读 音频会话编程指南 。 播放音频 本部分将介绍如何用 iPod媒体库访问接口、系统声音服务、音频队列服务、 AV Foundation框架、和 OpenAL来播放 iPhone OS上的声音。 通过 iPod媒体库访问接口播放媒体项 从 iPhone OS 3.0开始, iPod媒体库访问接口使应用程序可以播放用户的歌 曲、音频书,和音频播客。这个 API的设计使基本回放变得非常简单,同时又 支持高级的检索和回放控制。 如图 7-1所示,您的应用程序有两种方式可以取得媒体项,一种是通过媒体项 选择器,如图左所示,它是个易于使用、预先封装好的视图控制器,其行为和 内置 iPod程序的音乐选择接口类似。对于很多应用程序,这种方式就够用了。 如果媒体选择器没有提供您需要的某种访问控制,则可以使用媒体查询接口, 该接口支持以基于断言( predicate)的方式指定 iPod媒体库中的项目。 图 7-1  使用 iPod媒体库访问接口 如上图所示,位于右边的应用程序在取得媒体项之后,可以通过这个 API提供 的音乐播放器进行播放。 有关如何在应用程序中加入媒体项回放功能的完整解释,请参见 iPod媒体库访 问接口指南。 使用系统声音服务播放短声音及触发震动 当您需要播放用户界面声音效果(比如触击按键)或警告声音,或者使支持震 动的设备产生震动时,可以使用系统声音服务。这个简洁接口的描述请参见 系 统声音服务参考 。您可以在 iPhone Dev Center中找到 SysSound实例代码。 请注意: 通过系统声音服务播放的声音不受音频会话配置的控制。因此,您无 法使系统声音服务的音频行为和应用程序的其它音频行为保持一致。这也是需 要避免使用系统声音服务播放音频的最重要原因,除非您有意为之。 AudioServicesPlaySystemSound函数使您可以非常简单地播放短声音 文件。使用上的简单也带来一些限制。您的声音文件必须是: ■ 长度小于 30秒 ■ 采用 PCM或者 IMA4 (IMA/ADPCM) 格式 ■ 包装为 .caf、 .aif、或者 .wav文件 此外,当您使用 AudioServicesPlaySystemSound函数时: ■ 声音会以当前系统音量播放,且无法控制音量 ■ 声音立即被播放 ■ 不支持环绕和立体效果 AudioServicesPlayAlertSound是一个类似的函数,用于播放一个短声 音警告。如果用户在声音设置中将设备配置为震动,则这个函数在播放声音文 件之外还会产生震动。 请注意: 系统和用户界面的声音效果并不提供给您的应用程序。举例来说,将 kSystemSoundID_UserPreferredAlert 常量作为参数传递给 AudioServicesPlayAlertSound函数将不会播放任何声音。 在用AudioServicesPlaySystemSound 或 AudioServicesPlayAlertSound函数时,您需要首先创建一个声音 ID对 象,如程序清单 7-1所示。 程序清单 7-1  创建一个声音 ID对象 // Get the main bundle for the app CFBundleRef mainBundle = CFBundleGetMainBundle (); // Get the URL to the sound file to play. The file in this case // is "tap.aiff" soundFileURLRef = CFBundleCopyResourceURL ( mainBundle, CFSTR ("tap"), CFSTR ("aif"), NULL ); // Create a system sound object representing the sound file AudioServicesCreateSystemSoundID ( soundFileURLRef, &soundFileObject ); 然后再播放声音,如清单 7-2所示。 程序清单 7-2  播放一个系统声音 - (IBAction) playSystemSound { AudioServicesPlaySystemSound (self.soundFileObject); } 这片代码经常用于偶尔或者反复播放声音。如果您希望反复播放,就需要保持 声音 ID对象,直到应用程序退出。如果您确定声音只用一次 —比如程序启动的 声音 —则可以在播放完成后立即销毁声音 ID,释放其占用的内存。 如果 iPhone OS设备支持振动,则运行在该设备上的应用程序可以通过系统声 音服务触发振动,振动的选项通过 kSystemSoundID_Vibrate标识符来指 定。 AudioServicesPlaySystemSound函数可以用于触发振动,具体如 程序清单 7-3所示。 程序清单 7-3  触发振动 #import #import - (void) vibratePhone { AudioServicesPlaySystemSound (kSystemSoundID_Vibrate); } 如果您的应用程序运行在 iPod touch上,则上面的代码不执行任何操作。 通过 AVAudioPlayer类轻松播放声音 AVAudioPlayer类提供了一个简单的 Objective-C接口,用于播放声音。如 果您的应用程序不需要立体声或精确同步,且不播放来自网络数据流的音频, 则我们推荐您使用这个类来回放声音。 通过音频播放器可以实现如下任务: ■ 播放任意长度的声音 ■ 播放文件或内存缓冲区中的声音 ■ 循环播放声音 ■ 同时播放多路声音(虽然不能精确同步) ■ 控制每个正在播放声音的相对音量 ■ 跳到声音文件的特定点上,这可以为需要快进和反绕的应用程序提供支 持 ■ 取得音频强度数据,用于测量音量 AVAudioPlayer类可以播放 iPhone OS上有的所有音频格式,具体描述请参 见 “音频回放和录制格式 ”部分。或者。如果您需要该类接口的完整描述,请参 见 AVAudioPlayer类参考 。 为了使音频播放器播放音频,您需要为其分配一个声音文件,使其做好播放的 准备,并为其指定一个委托对象。程序清单 7-4中的代码通常放在应用程序控 制器类的初始化方法中。 程序清单 7-4  配置 AVAudioPlayer对象 // in the corresponding .h file: // @property (nonatomic, retain) AVAudioPlayer *player; @synthesize player; // the player object NSString *soundFilePath = [[NSBundle mainBundle] pathForResource: @"sound" ofType: @"wav"]; NSURL *fileURL = [[NSURL alloc] initFileURLWithPath: soundFilePath]; AVAudioPlayer *newPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL: fileURL error: nil]; [fileURL release]; self.player = newPlayer; [newPlayer release]; [player prepareToPlay]; [player setDelegate: self]; 您可以通过 委托 对象(可能是您的 控制器对象 )来处理中断,以及在声音播放 完成后更新用户界面。有关 AVAudioPlayer类的委托对象的具体描述请参见 AVAudioPlayerDelegate协议参考 。程序清单 7-5显示了一个委托方法的简单 实现,其中的代码在声音播放完成时更新了播放 /暂停切换按键的标题。 程序清单 7-5  实现 AVAudioPlayer类的委托方法 - (void) audioPlayerDidFinishPlaying: (AVAudioPlayer *) player successfully: (BOOL) flag { if (flag == YES) { [self.button setTitle: @"Play" forState: UIControlStateNormal]; } } 调用回放控制方法可以使 AVAudioPlayer对象执行播放、暂停、或者停止操 作。您可以通过 playing属性来检测当前是否正在播放。程序清单 7-6显示了 播放 /暂停切换方法的基本实现,其功能是控制回放和更新 UIButton对象的 标题。 程序清单 7-6  控制 AVAudioPlayer对象 - (IBAction) playOrPause: (id) sender { // if already playing, then pause if (self.player.playing) { [self.button setTitle: @"Play" forState: UIControlStateHighlighted]; [self.button setTitle: @"Play" forState: UIControlStateNormal]; [self.player pause]; // if stopped or paused, start playing } else { [self.button setTitle: @"Pause" forState: UIControlStateHighlighted]; [self.button setTitle: @"Pause" forState: UIControlStateNormal]; [self.player play]; } } AVAudioPlayer类使用 Objective-C的 属性声明 来管理声音信息 —比如取得 声音时间线上的回放点和访问回放选项(如音量和是否重复播放的设置)。举 例来说,您可以通过如下的代码设置一个音频播放器的回放音量: [self.player setVolume: 1.0]; // available range is 0.0 through 1.0 有关 AVAudioPlayer类的更多信息,请参见 AVAudioPlayer类参考 。 用音频队列服务播放和控制声音 音频队列服务( Audio Queue Services)加入了一些 AVAudioPlayer类不 具有的回放能力。通过音频队列服务进行回放可以: ■ 精确计划声音的播放,支持声音的同步。 ■ 精确控制音量 —基于一个个的缓冲区。 ■ 通过音频文件流服务( Audio File Stream Services)来播放从流中捕捉 的音频。 音频队列服务可以播放 iPhone OS支持的所有音频格式,具体描述请见 “音频回 放和录制格式 ”部分;还支持录制,详见 “录制音频 ”部分。 有关如何使用这个技术的详细信息,请参见 音频队列服务编程指南 和 音频队列 服务参考 。如果需要实例代码,请见 iPhone Dev Center网站的 SpeakHere实 例( Mac OS X系统上的实现则见 Core Audio SDK的 AudioQueueTools工 程,在 Mac OS X上安装 Xcode工具之后,在 /Developer/Examples/ CoreAudio/SimpleSDK/AudioQueueTools 路径下可以找到 AudioQueueTools工程)。 创建一个音频队列对象 创建一个音频队列对象需要下面三个步骤: 1. 创建管理音频队列所需的数据结构,比如您希望播放的音频格式。 2. 定义管理音频队列缓冲区的回调函数。在回调函数中,您可以使用音频 文件服务来读取希望播放的文件(在 iPhone OS 2.1及更高版本中,您还 可以用扩展音频文件服务来读取文件)。 3. 通过 AudioQueueNewOutput函数实例化回放音频队列。 程序清单 7-7是上述步骤的 ANSI C代码。 SpeakHere示例工程中也有同样的步 骤,只是它们位于 Objective-C程序的上下文中。 程序清单 7-7  创建一个音频队列对象 static const int kNumberBuffers = 3; // Create a data structure to manage information needed by the audio queue struct myAQStruct { AudioFileID mAudioFile; CAStreamBasicDescription mDataFormat; AudioQueueRef mQueue; AudioQueueBufferRef mBuffers[kNumberBuffers]; SInt64 mCurrentPacket; UInt32 mNumPacketsToRead; AudioStreamPacketDescription *mPacketDescs; bool mDone; }; // Define a playback audio queue callback function static void AQTestBufferCallback( void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inCompleteAQBuffer ) { myAQStruct *myInfo = (myAQStruct *)inUserData; if (myInfo->mDone) return; UInt32 numBytes; UInt32 nPackets = myInfo->mNumPacketsToRead; AudioFileReadPackets ( myInfo->mAudioFile, false, &numBytes, myInfo->mPacketDescs, myInfo->mCurrentPacket, &nPackets, inCompleteAQBuffer->mAudioData ); if (nPackets > 0) { inCompleteAQBuffer->mAudioDataByteSize = numBytes; AudioQueueEnqueueBuffer ( inAQ, inCompleteAQBuffer, (myInfo->mPacketDescs ? nPackets : 0), myInfo->mPacketDescs ); myInfo->mCurrentPacket += nPackets; } else { AudioQueueStop ( myInfo->mQueue, FALSE ); myInfo->mDone = true; } } // Instantiate an audio queue object AudioQueueNewOutput ( &myInfo.mDataFormat, AQTestBufferCallback, &myInfo, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &myInfo.mQueue ); 控制回放音量 音频队列对象为您提供两种控制回放音量的方法。 您可以通过调用 AudioQueueSetParameter 函数并传入 kAudioQueueParam_Volume参数来直接设置回放的音量,如程序清单 7-8所示,音量的变化会立即生效。 程序清单 7-8  直接设置回放的音量 Float32 volume = 1; // linear scale, range from 0.0 through 1.0 AudioQueueSetParameter ( myAQstruct.audioQueueObject, kAudioQueueParam_Volume, volume ); 您还可以通过 AudioQueueEnqueueBufferWithParameters函数来设 置音频队列缓冲区的回放音量。这个函数可以指定音频队列缓冲区进入队列时 携带的音频队列设置。通过这个函数做出的改变在音频队列缓冲区开始播放的 时候生效。 在上述的两种情况下,对音频队列的音量所做的修改都会一直保持下来,直到 再次被改变。 指示回放音量 您可以通过下面的方式得到音频队列对象的当前回放音量: 1. 启用音频队列对象的音量计,具体方法是将其 kAudioQueueProperty_EnableLevelMetering 属性设置为 true。 2. 查询音频队列对象的 kAudioQueueProperty_CurrentLevelMeter属性。 这个属性的值是一个 AudioQueueLevelMeterState结构的数组,每个声 道都有一个相对应的结构。程序清单 7-9显示了这个结构的内容: 程序清单 7-9  AudioQueueLevelMeterState结构 typedef struct AudioQueueLevelMeterState { Float32 mAveragePower; Float32 mPeakPower; }; AudioQueueLevelMeterState; 同时播放多路声音 为了同时播放多路声音,需要为每路声音创建一个回放音频队列对象,并对每 个音频队列调用 AudioQueueEnqueueBufferWithParameters函数, 将第一个音频缓冲区排入队列,使之开始播放。 在基于 iPhone OS的设备中同时播放声音时,音频格式是很关键的。如果要同 时播放,您需要使用线性 PCM (无压缩 ) 音频格式或特定的有压缩音频格式,具 体描述请参见 “音频回放和录制格式 ”部分。 使用 OpenAL播放和定位声音 开源的 OpenAL音频 API位于 iPhone OS系统的 OpenAL框架 中,它提供了一个 优化接口,用于定位正在回放的立体声场中的声音。使用 OpenAL进行声音的 播放、定位、和移动是很简单的 —其工作方式和其它平台一样。此外, OpenAL还可以进行混音。 OpenAL使用 Core Audio的 I/O单元进行回放,从 而使延迟最低。 由于所有的这些原因, OpenAL是 iPhone OS设备中游戏程序的最好选择。当 然, OpenAL也是一般的 iPhone OS应用程序进行音频播放的良好选择。 iPhone OS对 OpenAL 1.1的支持是构建在 Core Audio之上的。更多的信息请 参见 iPhone OS系统的 OpenAL FAQ。如果需要有关 OpenAL的文档,请参见 http://openal.org的 OpenAL网站;如果需要演示如何播放 OpenAL音频的示 例程序,请参见 oalTouch。 录制音频 在 iPhone OS系统上,可以通过 AVAudioRecorder类和音频队列服务来进 行音频录制,而 Core Audio则为其提供底层的支持。这些接口所做的工作包 括连接音频硬件、管理内存、以及在需要时使用编解码器。您可以录制 “音频的 回放和录制格式 ”部分列出的所有格式的音频。 本部分将介绍如何通过 AVAudioRecorder类和音频队列服务在 iPhone OS 系统上录制音频。 通过 AVAudioRecorder类进行录制 iPhone OS上最简单的录音方法是使用 AVAudioRecorder类,类的具体描 述请参见 AVAudioRecorder类参考 。该类提供了一个高度精简的 Objective-C 接口。通过这个接口,您可以轻松实现诸如暂停 /重启录音这样的功能,以及 处理音频中断。同时,您还可以对录制格式保持完全的控制。 进行录制时,您需要提供一个声音文件的 URL、建立音频会话、以及配置录音 对象。进行这些准备工作的一个良好时机就是应用程序启动的时候,如程序清 单 7-10所示。诸如 soundFileURL和 recording这样的变量都在类接口文 件中进行声明。 程序清单 7-10  建立音频会话和声音文件的 URL - (void) viewDidLoad { [super viewDidLoad]; NSString *tempDir = NSTemporaryDirectory (); NSString *soundFilePath = [tempDir stringByAppendingString: @"sound.caf"]; NSURL *newURL = [[NSURL alloc] initFileURLWithPath: soundFilePath]; self.soundFileURL = newURL; [newURL release]; AVAudioSession *audioSession = [AVAudioSession sharedInstance]; audioSession.delegate = self; [audioSession setActive: YES error: nil]; recording = NO; playing = NO; } 您需要在接口声明中加入 AVAudioSessionDelegate 、 AVAudioRecorderDelegate、 AVAudioPlayerDelegate(如果同时 支持声音回放的话)协议。 然后,就可以实现如程序清单 7-11所示的录制方法。 程序清单 7-11  一个基于 AVAudioRecorder类的录制 /停止方法 -(IBAction) recordOrStop: (id) sender { if (recording) { [soundRecorder stop]; recording = NO; self.soundRecorder = nil; [recordOrStopButton setTitle: @"Record" forState: UIControlStateNormal]; [recordOrStopButton setTitle: @"Record" forState: UIControlStateHighlighted]; [[AVAudioSession sharedInstance] setActive: NO error: nil]; } else { [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryRecord error: nil]; NSDictionary *recordSettings = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithFloat: 44100.0], AVSampleRateKey, [NSNumber numberWithInt: kAudioFormatAppleLossless], AVFormatIDKey, [NSNumber numberWithInt: 1], AVNumberOfChannelsKey, [NSNumber numberWithInt: AVAudioQualityMax], AVEncoderAudioQualityKey, nil]; AVAudioRecorder *newRecorder = [[AVAudioRecorder alloc] initWithURL: soundFileURL settings: recordSettings error: nil]; [recordSettings release]; self.soundRecorder = newRecorder; [newRecorder release]; soundRecorder.delegate = self; [soundRecorder prepareToRecord]; [soundRecorder record]; [recordOrStopButton setTitle: @"Stop" forState: UIControlStateNormal]; [recordOrStopButton setTitle: @"Stop" forState: UIControlStateHighlighted]; recording = YES; } } 有关 AVAudioRecorder类的更多信息,请参见 AVAudioRecorder类参考 。 用音频队列服务进行录制 用音频队列服务进行录制时,您的应用程序需要配置音频会话、实例化一个录 音音频队列对象,并为其提供一个回调函数。回调函数负责将音频数据存入内 存以备随时使用,或者写入文件进行长期存储。 声音的录制发生在 iPhone OS的系统定义级别( system-defined level)。系 统会从用户选择的音频源取得输入 —比如内置的麦克风、耳机麦克风(如果连 接到 iPhone上的话)、或者其它输入源。 和声音的回放一样,您可以通过查询音频队列对象的 kAudioQueueProperty_CurrentLevelMeter属性来取得当前的录制 音量,具体描述请见 “指示回放音量 ”部分。 有关如何通过音频队列服务录制音频的详细实例,请参见 音频队列服务编程指 南 的 录制音频 部分,实例代码则请见 iPhone Dev Center网站上的 SpeakHere。 解析音频流 为了播放音频流内容,比如来自网络连接的音频流,可以结合使用音频文件流 服务和音频队列服务。音频文件流服务负责从常见的、采用网络位流格式的音 频文件容器中解析出音频数据和元数据。您也可以用它来解析磁盘文件中的数 据包和元数据。 iPhone OS可以解析的音频文件和位流格式和 Mac OS X相同,具体如下: ■ MPEG-1 Audio Layer 3,用于 .mp3文件 ■ MPEG-2 ADTS,用于 .aac音频数据格式 ■ AIFC ■ AIFF ■ CAF ■ MPEG-4,用于 .m4a、 .mp4、和 .3gp文件 ■ NeXT ■ WAVE 在取得音频数据包之后,您就可以以任何 iPhone OS系统支持的格式进行播 放,这些格式在 “音频回放和录制格式 ”部分中列出。 为了获得最好的性能,处理网络音频流的应用程序应该仅使用来自 Wi-Fi连接 的数据。您可以通过 iPhone OS提供的 System Configuration框架 及其 SCNetworkReachability.h头文件定义的接口来确定什么网络是可到达 和可用的。如果需要实例代码,请参见 iPhone Dev Center网站的 Reachability工程。 为了连接网络音频流,可以使用 iPhone OS系统中的 Core Foundation框架中 的接口,比如 CFHTTPMesaage接口,具体描述请见 CFHTTPMessage参考 。 通过音频文件流服务解析网络数据包,将它恢复为音频数据包,然后放入缓冲 区,发送给负责回放的音频队列对象。 音频文件流服务依赖于音频文件服务定义的接口,比如 AudioFramePacketTranslation 结构和 AudioFilePacketTableInfo结构,具体描述请见 音频文件服务参考 。 有关如何使用流的更多信息,请参见 音频文件流服务参考 。实例代码则请参见 位于 /Examples/CoreAudio/Services/ 目录下的 AudioFileStream例子工程,其中 是开发工具所在的目录。 iPhone OS系统上的音频单元支持 iPhone OS提供一组音频插件,称为音频单元,可以用于所有的应用程序。您 可以通过 Audio Unit框架 提供的接口来打开、连接、和使用音频单元;还可以 定义定制的音频单元,在自己的应用程序内部使用。由于应用程序必须静态连 接定制的音频单元,所以 iPhone OS系统上的其它应用程序不能使用您开发的 音频单元。 表 7-3列出了 iPhone OS提供的音频单元。 表 7-3  系统提供的音频单元 音频单元 描述 转换器单 元 转换器单元,类型为 kAudioUnitSubType_AUConverter,用于音频数据的 格式转换。 iPod均衡 器单元 iPod EQ单元,类型为 kAudioUnitSubType_AUiPodEQ,提供一个简单的、基 于预设的均衡器,可以在应用程序中使用。 3D混音 器单元 3D混音器单元,类型为 kAudioUnitSubType_AU3DMixerEmbedded,用于混 合多个音频流,指定立体声输出移动,操作采样率,等等。 多通道混 音器单元 多通道混音器单元,类型为 kAudioUnitSubType_MultiChannelMixer,用于将 多个音频流混合成为单一的音频流。 一般输出 单元 一般输出单元,类型为 kAudioUnitSubType_GenericOutput,支持和线性 PCM格式互相转换,可以用于开始或结束一个音频单元图。 I/O单元 I/O单元,类型为 kAudioUnitSubType_RemoteIO,用于 连接音频输入和输入硬件,支持实时 I/O。如何使用音频单元 的实例代码请见 aurioTouch工程。 语音处理 I/O单元 语音处理 I/O单元,类型为 kAudioUnitSubType_VoiceProcessingIO,具有 I/O 单元的特征,同时为了支持双向交流,加入了回响抑制功能。 有关系统音频单元的更多信息,请参见系统音频单元访问指南。 iPhone音频的最佳实践 操作音频的贴士 在操作 iPhone OS系统上的音频内容时,您需要记住表 7-4列出的基本贴士。 表 7-4  音频贴士 贴士 动作 正确地使用 压缩音频 对于 AAC、 MP3、和 ALAC (Apple Lossless) 音频,解码过 程是由硬件来完成的,虽然比较有效,但同时只能解码一个 音频流。如果您需要同时播放多路声音,请使用 IMA4 (压缩 ) 或者线性 PCM (无压缩 ) 格式来存储那些文件。 将音频转换 为您需要的 数据格式和 文件格式 Mac OS X的 afconvert工具可以进行很多数据格式和文件 类型的转换。请参见 “iPhone OS偏好的音频格式 ” 部分和 afconvert工具的手册页面。 评价音频的 内存使用问 题 当您使用音频队列服务播放音频时,需要编写一个回调函 数,负责将较短的音频数据片断发送到音频队列的缓冲区。 在某些情况下,将整个音频文件载入内存是最佳的选择,这 样可以使播放时的磁盘访问尽最少;而在另外一些情况下, 最好的方法则是每次只载入足够填满缓冲区的数据。请测试 和评价哪种策略对您的应用程序最好。 限制音频的 采样率和位 深度,减少 音频文件的 尺寸 采样率和每个样本的位深度对无压缩音频的尺寸有直接的影 响。如果您需要播放很多这样的声音,则应该考虑降低这些 指标,以减少音频数据的内存开销。举例来说,相对于使用 采样率为 44.1 kHz的音频作为声音效果, 您可以使用采样 率为 32 kHz(或可能更低)的音频,仍然可以得到很合理的 品质。 选择恰当的 技术 使用 Core Audio的系统声音服务来播放警告和用户界面声音 效果。当您希望使用便利的高级接口来定位立体声场中的声 音,或者要求很低的回放延迟时,则应该使用 OpenAL。如 果需要从文件或网络数据流中解析出音频数据,可以使用音 频文件服务接口。如果只是简单回放一路或多路声音,则应 该使用 AVAudioPlayer类。对于具有其它音频功能的应用 程序,包括音频流的回放和音频录制,可以使用音频队列服 务。 低延迟编码 如果需要尽可能低的回放延迟,可以使用 OpenAL,或者直 接使用 I/O单元。 iPhone OS偏好的音频格式 对于无压缩(最高品质)音频,请使用封装在 CAF文件中的、 16位、低位在前 ( little endian)的线性 PCM音频数据。您可以用 Mac OS X的 afconvert命 令行工具来将音频文件转换为上述格式: /usr/bin/afconvert -f caff -d LEI16 {INPUT} {OUTPUT} afconvert工具可以进行广泛的音频数据格式和文件类型转换。您可以通过 afconvert的手册页面,以及在 shell提示符下键入 afconvert -h命令获 取更多信息。 对于压缩音频,当每次只需播放一个声音,或者当不需要和 iPod同时播放音频 时,适合使用 AAC格式的 CAF或 m4a文件。 当您需要在同时播放多路声音时减少内存开销时,请使用 IMA4 (IMA/ADPCM) 压缩格式,这样可以减少文件尺寸,同时在解压缩过程中对 CPU的影响又最 小。和线性 PCM数据一样,请将 IMA4数据封装在 CAF文件中。 在 iPhone OS使用视频 录制视频 从 iPhone OS 3.0开始,您可以在具有录制支持的设备上录制视频,包括当时 的音频。显示视频录制界面的方法是创建和推出一个 UIImagePickerController对象,和显示静态图片照相机界面完全一 样。 在录制视频时,您必须首先检查是否存在照相机源类型 (UIImagePickerControllerSourceTypeCamera) ,以及照相机是否 支持电影媒体类型 (kUTTypeMovie) 。根据您为 mediaTypes属性分配的 媒体类型的不同,选择器对象可以直接显示静态图像照相机,或者视频摄像 机,还可以显示一个选择界面,让用户选择。 使用 UIImagePickerControllerDelegate协议,注册为图像选择器的 委托。在视频录制完成时,您的委托对象的 imagePickerController:didFinishPickingMediaWithInfo: 方法会备调用。 对于支持录制的设备,您也可以从用户照片库中选择之前录制的视频。 有关如何使用图像选择器的更多信息,请参见 UIImagePickerController类参 考 。 播放视频文件 在 iPhone OS系统上,应用程序可以通过 Media Player框架 ( MediaPlayer.framework)来播放视频文件。视频的回放只支持全屏 模式,需要播放场景切换动画的游戏开发者或需要播放媒体文件的其它开发者 可以使用。当应用程序开始播放视频时,媒体播放器界面就会接管,将屏幕渐 变为黑色,然后渐渐显示视频内容。视频播放界面上可以显示或者不显示调整 回放的用户控件。您可以通过部分或全部激活这些控件(如图 7-2所示),使 用户可以改变音量、改变回放点、开始或停止视频的播放。如果禁用所有的控 件,视频会一直播放,直到结束。 图 7-2  带有播放控制的媒体播放器界面 在开始播放前,您必须知道希望播放的 URL。对于应用程序提供的文件,这个 URL通 常是指向应用程序包中某个文件的指针;但是,它也可以是指向远程服务器文件的指 针。您可以用这个 URL来 实例化 一个新的 MPMoviePlayerController类的实例。 这个类负责视频文件的回放和管理用户交互,比如响应用户对播放控制(如果显示的 话)的触击动作。简单调用控制器的 play方法,就可以开始播放了。 程序清单 7-12显示一个实例方法,功能是播放位于指定 URL的视频。 play方法是异 步的调用,在电影播放时会将控制权返回给调用者。电影控制器负责将电影载入一个 全屏的视图,并通过动画效果将电影放到应用程序现有内容的上方。在视频回放完成 后,电影控制器会向委托对象发出一个 通告 ,该委托对象负责在不再需要时释放电影 控制器。 程序清单 7-12  播放全屏电影 -(void)playMovieAtURL:(NSURL*)theURL { MPMoviePlayerController* theMovie = [[MPMoviePlayerController alloc] initWithContentURL:theURL]; theMovie.scalingMode = MPMovieScalingModeAspectFill; theMovie.movieControlMode = MPMovieControlModeHidden; // Register for the playback finished notification. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(myMovieFinishedCallback:) name:MPMoviePlayerPlaybackDidFinishNotification object:theMovie]; // Movie playback is asynchronous, so this method returns immediately. [theMovie play]; } // When the movie is done, release the controller. -(void)myMovieFinishedCallback:(NSNotification*)aNotification { MPMoviePlayerController* theMovie = [aNotification object]; [[NSNotificationCenter defaultCenter] removeObserver:self name:MPMoviePlayerPlaybackDidFinishNotification object:theMovie]; // Release the movie instance created in playMovieAtURL: [theMovie release]; } 有关 Media Player框架的各个类的更多信息,请参见 Media Player框架参考 。有关 它支持的视频格式列表,请参见 iPhone OS技术概览 。 设备支持 iPhone OS支持很多使移动计算的用户体验更具吸引力的特性。通过 iPhone OS,应用程序可以访问诸如加速计和照相机这样的硬件特性,也可以访问像用 户照片库这样的软件特性。本文的下面部分将描述这些特性,并向您展示如何 将它们集成到您的应用程序中。 确定硬件支持是否存在 为 iPhone OS设计的应用程序必须能够运行在具有不同硬件特性的多种设备 上。虽然像加速计和 Wi-Fi连网这样的特性在所有设备上都是支持的,但是一 些设备不包含照相机或 GPS硬件。如果您的应用程序要求设备具有这样的特 性,应该在用户购买之前通知他们。对于那些不是必需、但如果存在就希望支 持的特性,则必须在试图使用之前检测它们是否存在。 重要提示: 如果应用程序运行的前提是某个特性一定要存在,则应该在应用程 序的 Info.plist文件中对 UIRequiredDeviceCapabilities键进行 相应的设置,以避免将需要某种特性的应用程序安装在不具有该特性的设备 上。但是,如果您的应用程序在给定特性存在或不存在时都可以运行,则不应 该包含这个键。更多有关如果配置该键的信息,请参见 “信息属性列表 ”部分。 表 8-1列出了确定某种硬件是否存在的方法。如果您的应用程序在缺少某个特 性时可以工作,而在该特性存在时又可以加以利用,则应该使用这些技术。 表 8-1  识别可用的硬件特性 特性 选项 确定网络是 否存在 ... 使用 Software Configuration框架的可达性 ( reachability)接口检测当前的网络连接。有关如何使用 Software Configuration框架的例子请参见 可达性 部分。 确定静态照 相机是否存 在 ... 使用 UIImagePickerController类的 isSourceTypeAvailable:方法来确定照相机是否存 在。更多信息请参见 “使用照相机进行照相 ”部分。 确定音频输 入(麦克 风)是否存 在 … 在 iPhone OS 3.0及之后的系统上,可以用 AVAudioSession类来确定音频输入是否存在。该类考 虑了 iPhone OS设备上的很多不同的音频输入设备,包括 内置的麦克风、耳机插座、和连接的配件。更多信息请参 见 AVAudioSession类参考 部分。 确定 GPS硬件 是否存在 … 在配置 CLLocationManager对象、使应用程序可以获 取位置变化时,指定高精度级别。 Core Location框架 并不 指定硬件是否存在的直接信息,而是使用精度值来提供您 所需要的数据。如果一系列位置事件报告的精度都不够 高,您可以通知用户。更多信息请参见 “获取用户的当前位 置 ”部分。 确定特定的 配件是否存 在 … 使用 External Accessory框架的类来寻找合适的附近对 象,并进行连接。更多信息请参见 “和配件进行通讯 ”部分。 和配件进行通讯 在 iPhone OS 3.0及之后的系统上, External Accessory框架 ( ExternalAccessory.framework)提供了一种管道机制,使应用程 序可以和 iPhone或 iPod touch设备的配件进行通讯。通过这种管道,应用程序 开发者可以将配件级别的功能集成到自己的程序中。 请注意: 下面部分将向您展示 iPhone应用程序如何连接配件。如果您有兴趣成 为 iPhone或 iPod touch配件的开发者,可以在 http://developer.apple.com 网站上找到相应的信息。 为了使用 External Accessory框架的接口,您必须将 ExternalAccessory.framework加入到 Xcode工程,并连接到相应的目 标中。此外,还需要在相应的源代码文件的顶部包含一个 #import 语句,才能访问该框 架的类和头文件。有关如何为工程添加框架的更多信息,请参见 Xcode工程管 理指南 中的 工程中的文件 部分;有关 External Accessory框架中类的一般信 息,请参见 External Accessory框架参考 。 配件的基础 在和配件进行通讯之前,需要与配件的制造商紧密合作,理解配件提供的服 务。制造商必须在配件的硬件中加入显式的支持,才能和 iPhone OS进行通 讯。作为这种支持的一部分,配件必须支持至少一种命令 协议 ,也就是支持一 种定制的通讯模式,使配件和应用程序之间可以进行数据传输。苹果并不维护 一个协议的注册表,支持何种协议及是否使用其他制造商支持的定制或标准协 议是由制造商自行决定的。 作为和配件制造商通讯的一部分,您必须找出给定的配件支持什么协议。为了 避免名字空间发生冲突,协议的名称由反向的 DNS字符串来指定,形式是 com.apple.myProtocol。这使得每个配件制造商都可以根据自己的需要 定义协议,以支持不同的配件产品线。 应用程序通过打开一个使用指定协议的会话来和配件进行通讯。打开会话的方 法是创建一个 EASession类的实例,该类中包含 NSInputStream和 NSOutputStream对象,可以和配件进行通讯。通过这些流对象,应用程序 可以向配件发送未经加工的数据包,以及接收来自配件的类似数据包。因此, 您必须按照期望的协议来理解每个数据包的格式。 声明应用程序支持的协议 能够和配件通讯的应用程序应该在其 Info.plist文件中声明支持的协议, 使系统知道在相应的配件接入时,该应用程序可以被启动。如果当前没有应用 程序可以支持接入的配件,系统可以选择启动 App Store并指向支持该设备的 应用程序。 为了声明支持的协议,您必须在应用程序的 Info.plist文件中包含 UISupportedExternalAccessoryProtocols键。该键包含一个字符 串 数组 ,用于标识应用程序支持的通讯协议。您的应用程序可以在这个列表中 以任意顺序包含任意数量的协议。系统并不使用这个列表来确定应用程序应该 选择哪个协议,而只是用它来确定应用程序是否能够和相应的配件进行通讯。 您的代码需要在开始和配件进行对话时选择适当的通讯协议。 在运行时连接配件 在配件接入系统并做好通讯准备之前,通过 External Accessory框架无法看到 配件。当配件变为可见时,您的应用程序就可以获取相应的配件对象,然后用 其支持的一或多个协议打开会话。 共享的 EAAccessoryManager对象为应用程序寻找与之通讯的配件提供主 入口点。该类包含一个已经接入的配件对象的数组,您可以对其进行 枚举 ,看 看是否存在应用程序支持的配件。 EAAccessory对象中的绝大多数信息(比 如名称、制造商、和型号信息)都只是用于显示。如果您要确定应用程序是否 可以连接一个配件,必须看配件的协议,确认应用程序是否支持其中的某个协 议。 请注意: 多个配件对象支持同一协议是可能的。如果发生这种情况,您的代码 必须负责选择使用哪个配件对象。 对于给定的配件对象,每次只能有一个指定协议的会话。 EAAccessory对象 的 protocolStrings属性包含一个 字典 ,字典的键是配件支持的协议。如 果您试图用一个已经在使用的协议创建会话, External Accessory框架就会产 生错误。 程序清单 8-1展示了如何检查接入配件的列表并从中取得应用程序支持的第一 个配件。它为指定的协议创建一个会话,并对会话的输入和输出流进行配置。 在这个方法返回会话对象时,已经完成和配件的连接,并可以开始发送和接收 数据了。 程序清单 8-1  创建和配件的通讯会话 - (EASession *)openSessionForProtocol:(NSString *)protocolString { NSArray *accessories = [[EAAccessoryManager sharedAccessoryManager] connectedAccessories]; EAAccessory *accessory = nil; EASession *session = nil; for (EAAccessory *obj in accessories) { if ([[obj protocolStrings] containsObject:protocolString]) { accessory = obj; break; } } if (accessory) { session = [[EASession alloc] initWithAccessory:accessory forProtocol:protocolString]; if (session) { [[session inputStream] setDelegate:self]; [[session inputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [[session inputStream] open]; [[session outputStream] setDelegate:self]; [[session outputStream] scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [[session outputStream] open]; [session autorelease]; } } return session; } 在配置好输入输出流之后,最好一步就是处理和流相关的数据了。程序清单 8-2展示了在 委托 方法中处理流事件的基本代码结构。清单中的方法可以响应 来自配件输入输出流的事件。当配件向应用程序发送数据时,事件发生表示有 数据可供读取;类似地,当配件准备好接收应用程序数据时,也通过事件来表 示(当然,您并不一定要等到这个事件发生才向流写出数据,应用程序也可以 调用流的 hasBytesAvailable方法来确认配件是否还能够接收数据)。有 关流及如何处理流事件的更多信息,请参见 Cocoa流编程指南 。 程序清单 8-2  处理流事件 // Handle communications from the streams. - (void)stream:(NSStream*)theStream handleEvent:(NSStreamEvent) streamEvent { switch (streamEvent) { case NSStreamHasBytesAvailable: // Process the incoming stream data. break; case NSStreamEventHasSpaceAvailable: // Send the next queued command. break; default: break; } } 监控与配件有关的事件 当配件接入或断开时, External Accessory框架都可以发送 通告 。但是这些通 告并不自动发送,如果您的应用程序感兴趣,必须调用 EAAccessoryManager 类的 registerForLocalNotifications方法来显式请求。当配件接入、认证、并 准备好和应用程序进行交互时,框架可以发出一个 EAAccessoryDidConnectNotification通告;而当配件断开时,框架 则可以发送一个 EAAccessoryDidDisconnectNotification通告。您 可以通过缺省的 NSNotificationCenter来注册接收这些通告。两种通告 都包含受影响的配件的信息。 除了通过缺省的通告中心接收通告之外,当前正在和配件进行交互的应用程序 可以为相应的 EAAccessory对象分配一个 委托 ,使它在发生变化的时候得到 通知。委托对象必须遵循 EAAccessoryDelegate协议 ,该协议目前包含名 为 accessoryDidDisconnect:的可选方法,您可以通过这个方法来接收 配件断开通告,而不需要事先配置通告观察者。 有关如何注册接收通告的更多信息,请参见 Cocoa通告编程主题 。 访问加速计事件 加速计以时间为轴,测量速度沿着给定线性路径发生的变化。每个 iPhone和 iPod touch都包含三个加速计,分别负责设备的三个轴向。这种加速计的组合 使得我们可以检测设备在任意方向上的运动。您可以用这些数据来跟踪设备突 然发生的运动,以及当前相对于重力的方向。 请注意: 在 iPhone OS 3.0及之后的系统,如果您希望检测特定类型的运动, 比如摇摆设备,应该考虑通过运动事件来进行,而不是使用加速计的接口。运 动事件为检测特定类型的加速计运动提供一致的接口,更多的细节请参见 “运动 事件 ”部分。 每个应用程序都可以通过 UIAccelerometer的 单件 对象来接收加速计数 据。您可以通过 UIAccelerometer的 sharedAccelerometer类方法 来 取得该类的实例。之后,您就可以设置加速计数据更新的间隔时间及负责取得 数据的自定义 委托 。数据更新的间隔时间的最小值是 10毫秒,对应于 100Hz 的刷新频率。对于大多数应用程序来说,可以使用更大的时间间隔。您一旦设 置了委托对象,加速计就会开始发送数据。而委托对象也会在您请求的时间间 隔之后收到数据。 程序清单 8-3展示了配置加速计的基本步骤。在这个例子中,更新频率设置为 50Hz,对应于 20毫秒的时间间隔。 myDelegateObject是您定义的定制对 象,必须支持 UIAccelerometerDelegate协议,该协议定义了接收加速 计数据的方法。 程序清单 8-3  配置加速计 #define kAccelerometerFrequency 50 //Hz -(void)configureAccelerometer { UIAccelerometer* theAccelerometer = [UIAccelerometer sharedAccelerometer]; theAccelerometer.updateInterval = 1 / kAccelerometerFrequency; theAccelerometer.delegate = self; // Delegate events begin immediately. } 全局共享的加速计会以固定频率调用委托对象的 accelerometer:didAccelerate:方法,通过它传送事件数据,如清单 8-4所示。在这个方法中,您可以根据自己的需要处理加速计数据。一般地 说,我们推荐您使用一些过滤器来分离您感兴趣的数据成分。 程序清单 8-4  接收加速计事件 - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate: (UIAcceleration *)acceleration { UIAccelerationValue x, y, z; x = acceleration.x; y = acceleration.y; z = acceleration.z; // Do something with the values. } 将全局共享的 UIAccelerometer对象的委托设置为 nil,就可以停止加速 计事件的递送。将委托对象设置为 nil的操作会向系统发出通知,使其在需要 的时候关闭加速计硬件,从而节省电池的寿命。 在委托方法中收到的加速计数据代表的是来自加速计硬件的实时数据。即使设 备完全处于休息状态,加速计硬件报告的数据也可能产生轻微的波动。使用这 些数据时,务必通过取平均值或对收到的数据进行调整的方法,来平抑这种波 动。作为例子, Bubble Level示例程序提供了一些控制,可以根据已知的表面 调整当前的角度,后续读取的数据则是相对于调整后的角度进行调整。如果您 的代码需要类似级别的精度,也应该在程序界面中包含一些调整的选项。 选择恰当的更新频率 在配置加速计事件的更新频率时,最好既能满足应用程序的需求,又能使事件 发送次数最少。需要系统以每秒 100次的频率发送加速计事件的应用程序是很 少的。使用较低的频率可以避免应用程序过于繁忙,从而提高电池的寿命。表 8-2列出了一些典型的更新频率,以及在该频率下产生的加速计数据适合哪些 应用场合。 表 8-2  常用的加速计事件更新频率 事件频率 (Hz) 用途 10–20 适合用于确定代表设备当前方向的向量。 30–60 适合用于游戏和使用加速计进行实时输入的应用程序。 70–100 适合用于需要检测设备高频运动的应用程序,比如检测用户快 速触击或摆动设备。 从加速计数据中分离重力成分 如果您希望通过加速计数据来检测设备的当前方向,就需要将数据中源于重力 的部分从源于设备运动的部分中分离开来。为此,您可以使用低通滤波器来减 少加速计数据中剧烈变化部分的权重,这样过滤之后的数据更能反映由重力产 生的较为稳定的因素。 程序清单 8-5展示了一个低通滤波器的简化版本。清单中的代码使用一个低通 滤波因子生成一个由当前的滤波前数据的 10%和前一个滤波后数据的 90%组 成的值。前一个加速计数值存储在类的 accelX、 accelY、和 accelZ 成员 变量中。由于加速计数据以固定的频率进入您的应用程序,所以这些数值会很 快稳定下来,但过滤后的数据对突然而短暂的运动响应缓慢。 程序清单 8-5  从加速计数据中分离出重力的效果 #define kFilteringFactor 0.1 - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate: (UIAcceleration *)acceleration { // Use a basic low-pass filter to keep only the gravity component of each axis. accelX = (acceleration.x * kFilteringFactor) + (accelX * (1.0 - kFilteringFactor)); accelY = (acceleration.y * kFilteringFactor) + (accelY * (1.0 - kFilteringFactor)); accelZ = (acceleration.z * kFilteringFactor) + (accelZ * (1.0 - kFilteringFactor)); // Use the acceleration data. } 从加速计数据中分离实时运动成分 如果您希望通过加速计数据检测设备的实时运动,则需要将突然发生的运动变 化从稳定的重力效果中分离出来。您可以通过高通滤波器来实现这个目的。 程序清单 8-6展示了一个简化版的高通滤波器算法。从前一个事件得到的加速 计数值存储在类的 accelX、 accelY、和 accelZ成员变量中。清单中的代 码首先计算低通滤波器的值,然后从当前加速计数据中减去该值,得到仅包含 实时运动成分的数据。 程序清单 8-6  从加速计数据中分离出实时运动成分 #define kFilteringFactor 0.1 - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate: (UIAcceleration *)acceleration { // Subtract the low-pass value from the current value to get a simplified high-pass filter accelX = acceleration.x - ( (acceleration.x * kFilteringFactor) + (accelX * (1.0 - kFilteringFactor)) ); accelY = acceleration.y - ( (acceleration.y * kFilteringFactor) + (accelY * (1.0 - kFilteringFactor)) ); accelZ = acceleration.z - ( (acceleration.z * kFilteringFactor) + (accelZ * (1.0 - kFilteringFactor)) ); // Use the acceleration data. } 取得当前设备的方向 如果您需要知道的是设备的大体方向,而不是精确的方向向量,则应该通过 UIDevice类的相关方法来取得。使用 UIDevice接口比较简单,不需要自行 计算方向向量。 在取得当前方向之前,您必须调用 beginGeneratingDeviceOrientationNotifications 方法,使 UIDevice类开始产生设备方向 通告 。对该方法的调用会打开加速计硬件(否 则为了省电,加速计硬件处于关闭状态)。 在打开方向通告的很短时间后,您就可以从 UIDevice对象 orientation属 性声明 得到当前的方向。您也可以通过注册接收 UIDeviceOrientationDidChangeNotification通告来得到方向信 息,当设备的大体方向发生改变时,系统就会发出该通告。设备的方向由 UIDeviceOrientation常量来描述,它可以指示设备处于景观模式还是肖 像模式,以及设备的正面是朝上还是朝下。这些常量指示的是设备的物理方 向,不一定和应用程序的用户界面相对应。 当您不再需要设备的方向信息时,应该调用 UIDevice 的 endGeneratingDeviceOrientationNotifications方法来关闭方 向通告,使系统有机会关闭加速计硬件,如果其它地方也不使用的话。 使用位置和方向服务 Core Location框架 为定位用户当前位置和方向( Heading)提供支持,它负 责从相应的设备硬件收集信息,并以异步的方式报告给您的应用程序。数据是 否可用取决于设备的类型以及所需的硬件当前是否打开,如果设备处于飞行模 式,则某些硬件可能不可用。 在使用 Core Location框架的接口之前,必须将 CoreLocation.framework加入到您的 Xcode工程中,并在相关的目标中 进行连接。要访问该框架的类和头文件,还需要在相应的源代码文件的顶部包 含 #import 语句。更多有关如何 在工程中加入框架的信息,请参见 Xcode工程管理指南 文档中的 工程中的文件 部分。 有关 Core Location框架的类的一般性信息请参见 Core Location框架参考 。 取得用户的当前位置 Core Location框架使您可以定位设备的当前位置,并将这个信息应用到程序 中。该框架利用设备内置的硬件,在已有信号的基础上通过三角测量得到固定 位置,然后将它报告给您的代码。在接收到新的或更为精确的信号时,该框架 还对位置信息进行更新。 如果您确实需要使用 Core Location框架,则务必控制在最小程度,且正确地 配置位置服务。收集位置数据需要给主板上的接收装置上电,并向基站、 Wi- Fi热点、或者 GPS卫星查询,这个过程可能要花几秒钟的时间。此外,请求更 高精度的位置数据可能需要让接收装置更长时间地处于打开状态,而长时间地 打开这个硬件会耗尽设备的电池。如果位置信息不是频繁变化,通常可以先取 得初始位置,然后每隔一段时间请求一次更新就可以了。如果您确实需要定期 更新位置信息,也可以为位置服务设置一个最小的距离阈值,从而最小化代码 必须处理的位置更新。 取得用户当前位置首先要 创建 CLLocationManager类的实例,并用期望的 精度和阈值参数进行配置。开始接收通告则需要为该对象分配一个 委托 ,然后 调用 startUpdatingLocation方法来确定用户当前位置。当新的位置数 据到来时,位置管理器会通知它的委托对象。如果位置更新通告已经发送完 成,您也可以直接从 CLLocationManager对象获取最新的位置数据,而不 需要等待新的事件。 程序清单 8-7 展示了定制的 startUpdates 方法和 locationManager:didUpdateToLocation:fromLocation: 委托 方法的的一个实现。 startUpdates方法创建一个新的位置管理器对象(如 果尚未存在的话),并用它启动位置更新事件的递送(在这个实例中, locationManager变量是 MyLocationGetter类中声明的成员变量,该 类遵循 CLLocationManagerDelegate协议 。事件处理方法通过事件的时 间戳来确定其延迟的程度,对于太过时的事件,该方法会直接忽略,并等待更 为实时的事件。在得到足够实时的数据后,即关闭位置服务。 程序清单 8-7  发起和处理位置更新事件 #import @implementation MyLocationGetter - (void)startUpdates { // Create the location manager if this object does not // already have one. if (nil == locationManager) locationManager = [[CLLocationManager alloc] init]; locationManager.delegate = self; locationManager.desiredAccuracy = kCLLocationAccuracyKilometer; // Set a movement threshold for new events locationManager.distanceFilter = 500; [locationManager startUpdatingLocation]; } // Delegate method from the CLLocationManagerDelegate protocol. - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation { // If it's a relatively recent event, turn off updates to save power NSDate* eventDate = newLocation.timestamp; NSTimeInterval howRecent = [eventDate timeIntervalSinceNow]; if (abs(howRecent) < 5.0) { [manager stopUpdatingLocation]; printf("latitude %+.6f, longitude %+.6f\n", newLocation.coordinate.latitude, newLocation.coordinate.longitude); } // else skip the event and process the next one. } @end 对时间戳进行检查是推荐的做法,因为位置服务通常会立即返回最后缓存的位 置事件。得到一个大致的固定位置可能要花几秒钟的时间,更新之前的数据只 是反映最后一次得到的数据。您也可以通过精度来确定是否希望接收位置事 件。位置服务在收到精度更高的数据时,可能返回额外的事件,事件中的精度 值也会反映相应的精度变化。 请注意: Core Location框架在位置请求的一开始(而不是请求返回的时候) 记录时间戳。由于 Core Location使用几个不同的技术来取得固定位置,位置 请求返回的顺序有时可能和时间戳指示的顺序不同。这样,新事件的时间戳有 时会比之前的事件还要老一点,这是正常的。 Core Location框架致力于提高 每个新事件的位置精度,而不考虑时间戳的值。 获取与方向有关的事件 Core Location框架支持两种获取方向信息的方法。包含 GPS硬件的设备可以 提供当前移动方向的大致信息,该信息和经纬度数据通过同一个位置事件进行 传递。包含磁力计的设备可以通过方向对象提供更为精确的方向信息,方向对 象是 CLHeading类的实例。 通过 GPS硬件取得大致方向的过程和 “取得用户的当前位置 ”部分的描述是一样 的,框架会向您的应用程序 委托 传递一个 CLLocation对象,对象中的 course和 speed属性声明 包含相关的信息。这个接口适用于需要跟踪用户移 动的大多数应用程序,比如实现汽车导航系统的导航程序。对于基于指南针或 者可能需要了解用户静止时朝向的应用程序,可以请求位置管理器提供方向对 象。 您的程序必须运行在包含磁力计的设备上才能接收方向对象。磁力计可以测量 地球散发的磁场,进而确定设备的准确方向。虽然磁力计可能受到局部磁场 (比如扬声器的永磁铁、马达、以及其它类型电子设备发出的磁场)的影响, 但是 Core Location框架具有足够的智能,可以过滤很多局部磁场的影响,确 保方向对象包含有用的数据。 请注意: 如果路线或方向信息对于您的应用程序的必须的,则应该在程序的 Info.plist文件中正确地包含 UIRequiredDeviceCapabilities 键。这个键用于指定应用程序正常工作需要具备的设备特性,您可以用它来指 定设备必须具有 GPS和磁力计硬件。更多有关这个键值设置的信息请参见 “信息 属性列表 ”部分。 为了接收方向事件,您需要创建一个 CLLocationManager对象,为其分配 一个委托对象,并调用其 startUpdatingHeading方法,如程序清单 8-8 所示。然而,在请求方向事件之前,应该检查一下位置管理器的 headingAvailable属性,确保相应的硬件是存在的。如果该硬件不存在, 应用程序应该回退到通过位置事件获取路线信息的代码路径。 程序清单 8-8  发起方向事件的传送 CLLocationManager* locManager = [[CLLocationManager alloc] init]; if (locManager.headingAvailable) { locManager.delegate = myDelegateObject; // Assign your custom delegate object locManager.headingFilter = 5; [locManager startUpdatingHeading]; } else // Use location events instead 您赋值给 delegate属性的对象必须遵循 CLLocationManagerDelegate协 议 。当一个新的方向事件到来时,位置管理器会调用 locationManager:didUpdateHeading:方法,将事件传递给您的应用 程序。一旦收到新的事件,应用程序应该检查 headingAccuracy属性,确 保刚收到的数据是有效的,具体做法如清单 8-9。 程序清单 8-9  处理方向事件 - (void)locationManager:(CLLocationManager*)manager didUpdateHeading: (CLHeading*)newHeading { // If the accuracy is valid, go ahead and process the event. if (newHeading.headingAccuracy > 0) { CLLocationDirection theHeading = newHeading.magneticHeading; // Do something with the event data. } } CLHeading对象的 magneticHeading属性包含主方向数据,且该数据一 直存在。这个属性给出了相对于磁北极的方向数据,磁北极和北极不在同一个 位置上。如果您希望得到相对于北极(也称为地理北极)的方向数据,则必须 在 startUpdatingHeading之前调用 startUpdatingLocation方法来 启动位置更新,然通过 CLHeading对象的 trueHeading属性取得相对于地 理北极的方向。 显示地图和注解 iPhone OS 3.0引入了 Map Kit框架 。通过这个框架可以在应用程序的窗口中 嵌入一个全功能的地图界面。 Maps程序中的很多常见功能都包含在这个框架 提供的地图支持中,您可以通过它来显示标准的街道地图、卫星图像,或两者 的组合;还可以通过代码来缩放和移动地图。该框架还自动支持触摸事件,用 户可以用手指缩放或移动地图。您还可以在地图中加入自己定制的注释信息, 以及用框架提供的反向地理编码功能寻找和地图坐标关联的地址。 在使用 Map Kit框架的功能之前,必须将 MapKit.framework加入到 Xcode 工程中,并且在相关的目标中加以连接;在访问框架的类和头文件之前,需要 在相应的源代码文件的顶部加入 #import 语句。有 关如何将框架加入工程的更多信息,请参见 Xcode工程管理指南 中的 工程中的 文件 部分;有关 Map Kit框架类的一般性信息,则请参见 MapKit框架参考 。 重要提示: Map Kit框架使用 Google的服务来提供地图数据。框架及其相关接 口的使用必须遵守 Google Maps/Google Earth API的服务条款,具体条款信 息位于 http://code.google.com/apis/maps/iphone/terms.html。 在用户界面中加入地图视图 为应用程序加入地图之前,需要在应用程序的视图层次中嵌入一个 MKMapView类的实例,该类为地图信息的显示和用户交互提供支持。您可以 通过代码来为该类 创建实例 ,并通过 initWithFrame:方法来对其进行 初始 化 ,或者用 Interface Builder将它加入到 nib文件 中。 地图视图也是个视图,因此您可以通过它的 frame属性声明 随意调整它的位置 和尺寸。虽然地图视图本身没有提供任何控件,但是您可以在它的上面放置工 具条或其它视图,使用户可以和地图内容进行交互。您在地图视图中加入的所 有子视图的位置是不变的,不会随着地图内容的滚动而滚动。如果您希望在地 图上加入定制的内容,并使它们跟着地图滚动,则必须创建注解,具体描述请 参见 “显示注解 ”部分。 MKMapView类有很多属性,可以在显示之前进行配置,其中最重要的是 region属性,负责定义最初显示的地图部分及如何缩放和移动地图内容。 缩放和移动地图内容 MKMapView类的 region属性控制着当前显示的地图部分。当您希望缩放和 移动地图时,需要做的只是正确改变这个属性的值。这个属性包含一个 MKCoordinateRegion类型的结构,其定义如下: typedef struct { CLLocationCoordinate2D center; MKCoordinateSpan span; } MKCoordinateRegion; 改变 center域可以将地图移动到新的位置;而改变 span域的值则可以实现缩 放。这些域的值需要用地图坐标来指定,地图坐标用度、分、和秒来度量。对 于 span域,您需要通过经纬度距离来指定它的值。虽然纬度距离相对固定,每 度大约 111公里,但是经度距离却是随着纬度的变化而变化的。在赤道上,经 度距离大约每度 111公里;而在地球的极点上,这个值则接近于零。当然,您 总是可以通过 MKCoordinateRegionMakeWithDistance函数来创建基 于公里值(而不是度数)的区域。 如果您希望在更新地图时不显示过程动画,可以直接修改 region或 centerCoordinate属性的值;如果需要动画过程,则必须使用 setRegion:animated: 或 setCenterCoordinate:animated: 方 法。 setCenterCoordinate:animated:方法可以移动地图,且避免在 无意中触发缩放,而 setRegion:animated:方法则可以同时缩放和移动地 图。举例来说,如果您要使地图向左移动,移动距离为当前宽度的一半,则可 以通过下面的代码找到地图左边界的坐标,然后将它用于中心点的设置,如下 所示: CLLocationCoordinate2D mapCenter = myMapView.centerCoordinate; mapCenter = [myMapView convertPoint: CGPointMake(1, (myMapView.frame.size.height/2.0)) toCoordinateFromView:myMapView]; [myMapView setCenterCoordinate:mapCenter animated:YES]; 缩放地图则应该修改 span属性的值,而不是中心坐标。减少 span属性值可以 使视图缩小;相反,增加该属性值可以使视图放大。换句话说,如果当前的 span值是一度,将它指定为两度会使地图跨度放大两倍: MKCoordinateRegion theRegion = myMapView.region; // Zoom out theRegion.span.longitudeDelta *= 2.0; theRegion.span.latitudeDelta *= 2.0; [myMapView setRegion:theRegion animated:YES]; 显示用户的当前位置 Map Kit框架内置支持将用户的当前位置显示在地图上,具体做法是将地图视 图对象的 showsUserLocation属性值设置为 YES就可以了。进行这个设置 会使地图视图通过 Core Location框架找到用户位置,并在地图上加入类型为 MKUserLocation的注解。 在地图上加入 MKUserLocation注解对象的事件会通过 委托 对象进行报告, 这和定制注解的报告方式是一样的。如果您希望在用户位置上关联一个定制的 注解视图,应该在委托对象的 mapView:viewForAnnotation:方法中返 回该视图。如果您希望使用缺省的注解视图,则应该在该方法中返回 nil。 坐标和像素之间的转换 您通常通过经纬度值来指定地图上的点,但有些时候也需要在经纬度值和地图 视图对象中的像素之间进行转换。举例来说,如果您允许用户在地图表面拖动 注解,定制注解视图的事件处理器代码就需要将边框坐标转换为地图坐标,以 便更新关联的注解对象。 MKMapView类中几个例程,用于在地图坐标和地图 视图对象的本地坐标系统之间进行转换,这些例包括: 1 convertCoordinate:toPointToView: 2 convertPoint:toCoordinateFromView: 3 convertRegion:toRectToView: 4 convertRect:toRegionFromView: 有关如何处理定制注解事件的更多信息,请参见 “处理注解视图中的事件 ”部 分。 显示注解 注解是您定义并放置在地图上面的信息片段。 Map Kit框架 将注解实现为两个 部分,即注解对象和用于显示注解的视图。大多数情况下,您需要负责提供这 些定制对象,但框架也提供一些标准的注解和视图供您使用。 在地图视图上显示注解需要两个步骤: 1. 创建注解对象 并将它加入到地图视图中。 2. 在自己的 委托对象 中实现 mapView:viewForAnnotation:方法,并 在该方法中创建相应的注解视图。 注解对象是指遵循 MKAnnotation协议 的任何对象。通常情况下,注解对象 是相对小的数据对象,存储注解的坐标及相关信息,比如注解的名称。注解是 通过协议来定义的,因此应用程序中的任何对象都可以成为注解对象。然而, 在实践上,注解对象应该是轻量级的,因为在显式删除注解对象之前,地图视 图会一直保存它们的引用。注意,同样的结论并不一定适用于注解视图。 在将注解显示在屏幕上时,地图视图负责确保注解对象具有相关联的注解视 图,具体的方法是在注解坐标即将变为可见时调用其委托对象的 mapView:viewForAnnotation:方法。但是,由于注解视图的量级通常 总是比其对应的注解对象更重,所以地图对象尽可能不在内存中同时保存很多 注解视图。为此,它实现了注解视图的回收机制。这个机制和表视图在滚动时 回收表单元使用的机制相类似,即当一个注解视图移出屏幕时,地图视图就解 除其与注解对象之间关联,将它放入重用队列。而在创建新的注解视图之前, 委托的 mapView:viewForAnnotation:方法应该总是调用地图对象的 dequeueReusableAnnotationViewWithIdentifier:方法来检查重 用队列中是否还有可用的视图对象。如果该方法返回一个正当的视图对象,您 就可以对其进行再次初始化,并将它返回;否则,您再创建和返回一个新的视 图对象。 添加和移除注解对象 您不应直接在地图上添加注解视图,而是应该添加注解对象,注解对象通常不 是视图。注解对象可以是应用程序中遵循 MKAnnotation协议的任何对象。 注解对象中最重要的部分是它的 coordinate 属性声明 ,它是 MKAnnotation协议必需实现的属性,用于为地图上的注解提供锚点。 往地图视图加入注解所需要的全部工作就是调用地图视图对象的 addAnnotation:或 addAnnotations:方法。何时往地图视图加入注解 以及何时为加入的注解提供用户界面由您自己来决定。您可以提供一个工具 条,由用户通过工具条上的命令来创建注解,或者也可以自行编码创建注解, 注解信息可能来自本地或远程的数据库信息。 如果您的应用程序需要删除某个老的注解,则在删除之前,应该调用 removeAnnotation:或 removeAnnotations:方法将它从地图中移 除。地图视图会显示它知道的所有注解,如果您不希望某些注解被显示在地图 上,就需要显式地将它们删除。例如,如果您的应用程序允许用户对餐厅或本 地风景点进行过滤,就需要删除与过滤条件不相匹配的所有注解。 定义注解视图 Map Kit框架提供了两个注解视图类: MKAnnotationView和 MKPinAnnotationView。 MKAnnotationView类是一个具体的视图, 定义了所有注解视图的基本行为。 MKPinAnnotationView类则是 MKAnnotationView的子类,用于在关联的注解坐标点上显示一个标准的系 统大头针图像。 您可以将 MKAnnotationView类用于显示简单的注解,也可以从该类派生出 子类,提供更多的交互行为。在直接使用该类时,您需要提供一个定制的图 像,用于在地图上表示您希望显示的内容,并将它赋值给注解视图的 image属 性。如果您显示的内容不需要动态改变,而且不需要支持用户交互,则这种用 法是非常合适的。但是,如果您需要支持动态内容和用户交互,则必须定义定 制子类。 在一个定制的子类中,有两种方式可以描画动态内容:可以继续使用 image属 性来显示注解图像,这样或许需要设置一个定时器,负责定时改变当前的图 像;也可以重载视图的 drawRect:方法来显示描画您的内容,这种方法也需 要设置一个定时器,以定时调用视图的 setNeedsDisplay方法。 如果您通过 drawRect:方法来描画内容,则必须记住:要在注解视图初始化 后不久为其指定尺寸。注解视图的缺省 初始化 方法并不包含边框矩形参数,而 是在初始化后通过您分配给 image属性的图像来设置边框尺寸。如果您没有设 置图像,就必须显式设置边框尺寸,您渲染的内容才会被显示。 有关如何在注解视图中支持用户交互的信息,请参见 “处理注解视图的事件 ”部 分;有关如何设置定时器的信息,则请参见 Cocoa定时器编程主题 。 创建注解视图 您应该总是在 委托对象 的 mapView:viewForAnnotation:创建注解视 图。在创建新视图之前,您应该总是调用 dequeueReusableAnnotationViewWithIdentifier:方法来检查是 否有可重用的视图,如果该方法返回非 nil值,就应该将地图视图提供的注解 分配给重用视图的 annotation属性,并执行其它必要的配置,使视图处于 期望的状态,然后将它返回;如果该方法返回 nil,则应该创建并返回一个新 的注解视图对象。 程序清单 8-10是 mapView:viewForAnnotation:方法的一个例子实现, 展示了如何为定制注解对象提供大头针注解视图。如果队列中已经存在一个大 头针注解视图,该方法就将它和相应的注解对象相关联;如果重用队列中没有 视图,该方法则创建一个新的视图,对其基本属性进行配置,并为插图符号配 置一个附加视图。 程序清单 8-10  创建注解视图 - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id )annotation { // If it's the user location, just return nil. if ([annotation isKindOfClass:[MKUserLocation class]]) return nil; // Handle any custom annotations. if ([annotation isKindOfClass:[CustomPinAnnotation class]]) { // Try to dequeue an existing pin view first. MKPinAnnotationView* pinView = (MKPinAnnotationView*)[mapView dequeueReusableAnnotationViewWithIdentifier:@"CustomPinAnnotation"]; if (!pinView) { // If an existing pin view was not available, create one pinView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"CustomPinAnnotation"] autorelease]; pinView.pinColor = MKPinAnnotationColorRed; pinView.animatesDrop = YES; pinView.canShowCallout = YES; // Add a detail disclosure button to the callout. UIButton* rightButton = [UIButton buttonWithType: UIButtonTypeDetailDisclosure]; [rightButton addTarget:self action:@selector (myShowDetailsMethod:) forControlEvents:UIControlEventTouchUpInside]; pinView.rightCalloutAccessoryView = rightButton; } else pinView.annotation = annotation; return pinView; } return nil; } 处理注解视图中的事件 虽然注解视图位于地图内容上面的特殊层中,但它们也是功能完全的视图,能 够接收触摸事件。您可以通过这些事件来实现用户和注解之间的交互。比如, 您可以通过视图中的触摸事件来实现注解在地图表面的拖拽行为。 请注意: 由于地图被显示在一个滚动界面上,所以,在用户触击定制视图和事 件最终被派发之间往往有一个小的延迟。滚动视图可以利用这个延迟来确定触 摸事件是否为某种滚动手势的一部分。 随后的一系列示例代码将向您展示如何实现一个支持用户拖动的注解视图。例 子中的注解视图直接在注解坐标点上显示一个公牛眼图像,并包含一个定制的 附加视图,用以显示目的地的详细信息。图 8-1显示注解视图的一个实例以及 其包含的气泡符号。 图 8-1  公牛眼注解视图 程序清单 8-11显示了 BullseyeAnnotationView类的定义。类中包含一 些正确跟踪视图移动需要的其它成员变量,以及一个指向地图视图本身的指 针,指针的值是在 mapView:viewForAnnotation:方法中设置的,该方 法是创建或再次初始化注解视图的地方。在事件跟踪完成后,代码需要调整注 解对象的地图坐标,这时需要用到地图视图对象。 程序清单 8-11  BullseyeAnnotationView类 @interface BullseyeAnnotationView : MKAnnotationView { BOOL isMoving; CGPoint startLocation; CGPoint originalCenter; MKMapView* map; } @property (assign,nonatomic) MKMapView* map; - (id)initWithAnnotation:(id )annotation; @end @implementation BullseyeAnnotationView @synthesize map; - (id)initWithAnnotation:(id )annotation { self = [super initWithAnnotation:annotation reuseIdentifier:@"BullseyeAnnotation"]; if (self) { UIImage* theImage = [UIImage imageNamed:@"bullseye32.png"]; if (!theImage) return nil; self.image = theImage; self.canShowCallout = YES; self.multipleTouchEnabled = NO; map = nil; UIButton* rightButton = [UIButton buttonWithType: UIButtonTypeDetailDisclosure]; [rightButton addTarget:self action:@selector (myShowAnnotationAddress:) forControlEvents:UIControlEventTouchUpInside]; self.rightCalloutAccessoryView = rightButton; } return self; } @end 当触击事件首次到达公牛眼视图时,该类的 touchesBegan:withEvent: 方法会记录事件的信息,作为初始信息,如清单 8-12 所示。 touchesMoved:withEvent:方法会利用这些信息来调整视图位置。所有 的位置信息都存储在父视图的坐标空间中。 程序清单 8-12  跟踪视图的位置 @implementation BullseyeAnnotationView (TouchBeginMethods) - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // The view is configured for single touches only. UITouch* aTouch = [touches anyObject]; startLocation = [aTouch locationInView:[self superview]]; originalCenter = self.center; [super touchesBegan:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { UITouch* aTouch = [touches anyObject]; CGPoint newLocation = [aTouch locationInView:[self superview]]; CGPoint newCenter; // If the user's finger moved more than 5 pixels, begin the drag. if ( (abs(newLocation.x - startLocation.x) > 5.0) || (abs(newLocation.y - startLocation.y) > 5.0) ) isMoving = YES; // If dragging has begun, adjust the position of the view. if (isMoving) { newCenter.x = originalCenter.x + (newLocation.x - startLocation.x); newCenter.y = originalCenter.y + (newLocation.y - startLocation.y); self.center = newCenter; } else // Let the parent class handle it. [super touchesMoved:touches withEvent:event]; } @end 当用户停止拖动注解视图时,您需要调整原有注解的坐标,确保视图位于新的 位置。清单 8-13 显示了 BullseyeAnnotationView 类的 touchesEnded:withEvent:方法,该方法通过地图成员变量将基于像素 的点转化为地图坐标值。由于注解的 coordinate属性通常是只读的,所以 例子中的注解对象实现了一个名为 changeCoordinate的定制方法,负责更 新它在本地存储的值,而这个值可以通过 coordinate属性取得。如果触摸 事件由于某种原因被取消, touchesCancelled:withEvent:方法会使注 解视图回到原来的位置。 程序清单 8-13  处理最后的触摸事件 @implementation BullseyeAnnotationView (TouchEndMethods) - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (isMoving) { // Update the map coordinate to reflect the new position. CGPoint newCenter = self.center; BullseyeAnnotation* theAnnotation = self.annotation; CLLocationCoordinate2D newCoordinate = [map convertPoint:newCenter toCoordinateFromView:self.superview]; [theAnnotation changeCoordinate:newCoordinate]; // Clean up the state information. startLocation = CGPointZero; originalCenter = CGPointZero; isMoving = NO; } else [super touchesEnded:touches withEvent:event]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { if (isMoving) { // Move the view back to its starting point. self.center = originalCenter; // Clean up the state information. startLocation = CGPointZero; originalCenter = CGPointZero; isMoving = NO; } else [super touchesCancelled:touches withEvent:event]; } @end 通过反向地理编码器获取地标信息 Map Kit框架主要处理地图坐标值。地图坐标值由经度和纬度组成的,比较易 于在代码中使用,但却不是用户最容易理解的描述方式。为使用户更加易于理 解,您可以通过 MKReverseGeocoder类来取得与地图坐标相关联的地标信 息,比如街道地址、城市、州、和国家。 MKReverseGeocoder类负责向潜在的地图服务查询指定地图坐标的信息。 由于需要访问网络,反向地理编码器对象总是以异步的方式执行查询,并将结 果返回给相关联的 委托对象 。委托对象必须遵循 MKReverseGeocoderDelegate协议。 启动反向地理编码器的具体做法是首先创建一个 MKReverseGeocoder类的 实例,并将恰当的对象赋值给该实例的 delegate属性,然后调用 start方 法。如果查询成功完成,您的委托就会收到带有一个 MKPlacemark对象的查 询结果。 MKPlacemark对象本身也是注解对象 —也就是说,它们采纳了 MKAnnotation协议 —因此如果您愿意的话,可以将它们添加到地图视图的 注解列表中。 用照相机照相 通过 UIKit的 UIImagePickerController类可以访问设备的照相机。该类 可以显示标准的系统界面,使用户可以通过现有的照相机拍照,以及对拍得的 图像进行裁剪和尺寸调整;该类还可以用于从用户照片库中选取照片。 照相机界面是一个模式视图,由 UIImagePickerController类来管理。 具体使用时,您不应从代码中直接访问该视图,而是应该调用当前活动的 视图 控制器 的 presentModalViewController:animated:方法,并向其传 入一个 UIImagePickerController对象作为新的视图控制器。一旦被安 装,选取控制器就会自动将照相机界面滑入屏幕,并一直保持活动,直到用户 确认或取消图像选取的操作。如果用户做出选择,选取控制器会将这个事件通 知其委托对象。 UIImagePickerController类管理的界面可能并不适用于所有的设备。 在显示照相机界面之前,您应该调用 UIImagePickerController类的 isSourceTypeAvailable:类方法 ,确认该界面是否可用。您应该总是尊 重该方法的返回值,如果它返回 NO,意味着当前设备没有照相机,或者照相机 由于某种原因不可用;如果返回 YES,则可以通过下面的步骤显示照相机界 面: 1. 创建 一个新的 UIImagePickerController对象。 2. 为该对象分配一个 委托对象 。 大多数情况下,您可以让当前的视图控制 器充当选取控制器的委托,但也可以根据自己的喜好使用完全不同的对 象。委托对象必须遵循 UIImagePickerControllerDelegate和 UINavigationControllerDelegate协议 。 请注意: 如果您的委托不遵循 UINavigationControllerDelegate协议,在编译时就会看到警 告信息。然而,由于该协议的方法是可选的,所以不会对代码带来什么 影响。如果要消除该警告信息,需要将 UINavigationControllerDelegate协议加入委托类支持的协议 列表中。 3. 将选取控制器的类型设置为 UIImagePickerControllerSourceTypeCamera。 4. 为 allowsImageEditing属性声明 设置恰当的值,以便激活或者禁用 图片编辑控制。这是个可选步骤。 5. 调用当前视图控制器的 presentModalViewController:animated:方法,显示选取控 制器。 程序清单 8-14 的代码实现了上述步骤。在调用 presentModalViewController:animated方法之后,选取控制器随 即接管控制权,将照相机界面显示出来,并负责响应所有的用户交互,直到退 出该界面。而从用户照片库中选取现有照片需要做的只是将选取控制器的 sourceType 属性的值改为 UIImagePickerControllerSourceTypePhotoLibrary就可以了。 程序清单 8-14  显示照相界面 -(BOOL)startCameraPickerFromViewController:(UIViewController*)controller usingDelegate:(id)delegateObject { if ( (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) || (delegateObject == nil) || (controller == nil)) return NO; UIImagePickerController* picker = [[UIImagePickerController alloc] init]; picker.sourceType = UIImagePickerControllerSourceTypeCamera; picker.delegate = delegateObject; picker.allowsImageEditing = YES; // Picker is displayed asynchronously. [controller presentModalViewController:picker animated:YES]; return YES; } 当用户触击相应的按键关闭照相机界面时, UIImagePickerController 会将用户的动作通知委托对象,但并不直接实施关闭操作。选取器界面的关闭 由委托对象负责(您的应用程序还必须负责在不需要选取器对象时将它释放, 这个工作也可以在委托方法中进行)。由于这个原因,委托对象实际上应该是 将选取器显示出来的视图控制器对象。一旦收到委托消息,视图控制器会调用 其 dismissModalViewControllerAnimated:方法来关闭照相机界 面。 程序清单 8-15展示了关闭照相机界面的委托方法,该界面是由 程序清单 8-14 的代码显示出来的。这些方法是由一个名为 MyViewController的定制类实 现的,它是 UIViewController的一个子类。在这个例子中,执行这些代码 和显示选取器的应该是同一个对象。 useImage:方法是一个空壳,应该被您 的定制代码代替,您可以在这个方法中使用用户选取的图像。 程序清单 8-15  图像选取器的委托方法 @implementation MyViewController (ImagePickerDelegateMethods) - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(NSDictionary *)editingInfo { [self useImage:image]; // Remove the picker interface and release the picker object. [[picker parentViewController] dismissModalViewControllerAnimated:YES]; [picker release]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [[picker parentViewController] dismissModalViewControllerAnimated:YES]; [picker release]; } // Implement this method in your code to do something with the image. - (void)useImage:(UIImage*)theImage { } @end 如果图像编辑功能被激活,且用户成功选取了一张图片,则 imagePickerController:didFinishPickingImage:editingIn fo:方法的 image参数会包含编辑后的图像,您应该将这个图像作为用户选取 的图像。当然,如果用户希望存储原始图像,可以从 editingInfo参数的字 典中得到(同时还可以得到编辑用的裁剪矩形)。 从照片库中选取照片 UIKit通过 UIImagePickerController类为访问用户照片库提供支持。这 个 控制器 可以显示照片选取器界面,用户可以通过该界面漫游用户照片库,选 取某个图像,并将它返回给应用程序。您也可以打开用户编辑功能,使用户可 以移动和裁剪返回的图像。这个类也可以用于显示一个照相机界面。 UIImagePickerController类既可以显示照相机界面,也可以显示用户 照片库,两种显示方式的使用步骤几乎一样。唯一的区别是是否将选取器对象 的 sourceType 属性值设置为 UIImagePickerControllerSourceTypePhotoLibrary。显示照相 机选取器的具体步骤请参见 “用照相机照相 ”部分的讨论。 请注意: 当您使用照相机选取器时,应该总是调用 UIImagePickerController 类的 isSourceTypeAvailable: 类方 法,并尊重其返回值,而不应假定给定的设备总是具有照片库功能。即使设备 支持照片库,该方法仍然可能在照片库不可用时返回 NO。 使用邮件编辑界面 在 iPhone OS 3.0及之后的系统中,您可以通过 MFMailComposeViewController类在应用程序内部显示一个标准的邮件 发送界面。在显示该界面之前,您可以用该类的方法来配置邮件的接受者、主 题、和希望包含的附件。当邮件在界面显示出来(通过标准的视图控制器技 术)之后和提交给 Mail程序进行发送之前,用户可以对邮件的内容进行编辑。 用户也可以将整个邮件取消。 请注意: 在所有版本的 iPhone OS中,您可以通过创建和打开一个 mailto类 型的 URL来制作邮件,这种类型的 URL会自动传递给 Mail程序进行处理。有关 如何打开这种类型的 URL的更多信息,请参见 “和其它应用程序间的通讯 ”部 分。 在使用邮件编辑界面之前,您必须首先把 MessageUI.framework加入到工 程中,并在相应的目标中进行连接。为了访问该框架中的类和头文件,还必须 在相应的源代码文件的顶部包含 #import 语句。有关如何在工程中加入框架的信息,请参见 Xcode工程管理指南 文档中 的 工程中的文件 部分。 应用程序在使用 MFMailComposeViewController类时,必须首先 创建一 个实例 , 并使用该实例的方法设置初始的电子邮件数据;还必须为 视图控制器 的 mailComposeDelegate属性声明 分配一个对象,负责在用户接收或取消 邮件发送时退出界面。您指定的 委托对象 必须遵循 MFMailComposeViewControllerDelegate协议 在指定电子邮件地址时,应该使用纯字符串对象。如果您希望使用通讯录用户 列表中的邮件地址,可以通过 Address Book框架来实现。更多有关如何通过 该框架获取电子邮件及其它数据的信息,请参见 iPhone OS的 Address Book 编程指南 。 程序清单 8-16 展示了如何在应用程序中创建 MFMailComposeViewController对象,并用模式视图显示邮件编辑接口 的代码。您可以将清单中的 displayComposerSheet方法包含到定制的视 图控制器中,并在需要时通过它来显示邮件编辑界面。在这个例子中,父视图 控制器将自身作为委托,并实现了 mailComposeController:didFinishWithResult:error:方法。 该委托方法只是退出邮件编辑界面,没有进行更多的操作。在您自己的应用程 序中,可以在委托方法中考察 result参数的值,确定用户是否发送或取消了邮 件。 程序清单 8-16  显示邮件编辑界面 @implementation WriteMyMailViewController (MailMethods) -(void)displayComposerSheet { MFMailComposeViewController *picker = [[MFMailComposeViewController alloc] init]; picker.mailComposeDelegate = self; [picker setSubject:@"Hello from California!"]; // Set up the recipients. NSArray *toRecipients = [NSArray arrayWithObjects:@"first@example.com", nil]; NSArray *ccRecipients = [NSArray arrayWithObjects:@"second@example.com", @"third@example.com", nil]; NSArray *bccRecipients = [NSArray arrayWithObjects:@"four@example.com", nil]; [picker setToRecipients:toRecipients]; [picker setCcRecipients:ccRecipients]; [picker setBccRecipients:bccRecipients]; // Attach an image to the email. NSString *path = [[NSBundle mainBundle] pathForResource:@"ipodnano" ofType:@"png"]; NSData *myData = [NSData dataWithContentsOfFile:path]; [picker addAttachmentData:myData mimeType:@"image/png" fileName:@"ipodnano"]; // Fill out the email body text. NSString *emailBody = @"It is raining in sunny California!"; [picker setMessageBody:emailBody isHTML:NO]; // Present the mail composition interface. [self presentModalViewController:picker animated:YES]; [picker release]; // Can safely release the controller now. } // The mail compose view controller delegate method - (void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error { [self dismissModalViewControllerAnimated:YES]; } @end 有关如何通过标准视图控制器技术显示界面的更多信息,请参见 iPhone OS视 图控制器编程指南 ;有关 Message UI框架中包含的类信息,则请参见 Message UI框架参考 。 应用程序偏好设置 在传统的桌面应用程序中,偏好设置是一些专门面向应用程序的设置,用于配 置应用程序的行为和外观。 iPhone OS也支持应用程序偏好设置,但并不将它 作为应用程序整体的一部分。在 iPhone OS上,应用程序级别的偏好设置并不 由各个程序本身的定制界面来显示,而是由系统提供的 Settings程序统一显 示。 为了将定制的应用程序偏好设置集成到 Settings程序中,您必须在应用程序包 的顶级目录中包含一个特殊格式的 Settings程序包 ,由它负责将应用程序的偏 好设置信息提供给 Settings程序,而 Settings程序则负责对其进行显示,并将 用户提供的值写入偏好设置数据库。在运行时,您的应用程序可以通过标准的 API取得这些偏好设置的值。本章的下面部分将描述 Settings程序包的格式, 以及用于取得偏好设置值的 API。 偏好设置的指导原则 将偏好设置加入到 Settings程序的做法最适合于效率工具类型的应用程序,以 及偏好设置值配置完成后很少再改变的程序。 Mail程序就是一个例子,它通过 这种形式的偏好设置来存储用户账户信息及消息检查设置。由于 Settings程序 可以按层次进行显示,所以当您有大量的偏好设置时,通过 Settings程序来进 行操作也是比较合适的,在自己的应用程序中提供同样的偏好设置集合可能需 要太多屏幕,而且可能造成用户的混淆。 当您的应用程序只需要少数的选项,或者用户需要经常改变这些选项时,应该 认真考虑是否用 Settings程序来管理。举例来说,工具程序更适合在主视图的 背面提供定制的配置选项,即在视图上通过一个特殊的控件翻转视图,显示应 用程序的选项,再通过另一个控件将视图翻转回来。对于简单的应用程序,这 种方式使用户可以立即访问应用程序选项,比使用 Settings程序方便得多。 对于游戏和其它全屏程序的预置,可以使用 Settings程序或自行实现定制的屏 幕。定制屏幕通常更适合游戏程序,因为偏好设置可以处理为游戏设置的一部 分。当然,您也可以使用 Settings程序,如果您认为那样对游戏的使用流程更 好的话。 请注意: 永远不要使偏好设置同时存在于 Setting程序和自定义的应用程序屏幕 上。举例来说,如果工具类应用程序在主视图的背面有偏好设置,则在 Settings程序中就不应该再有可配置的设置。如果您的应用程序需要进行偏好 设置,则请仅选择和使用一种方案。 偏好设置的接口 Settings程序实现了一组有层次的页面,用于访问应用程序的偏好设置。 Settings程序的主视图显示了可以进行偏好设置的系统程序及第三方应用程 序,用户选择一个第三方程序后会进入该程序的偏好设置页面。 每个应用程序都至少有一个偏好设置页面,我们称为主页面。如果您的应用程 序只有少数几个偏好设置,则一个主页面可能就够了。然而,如果偏好设置太 多,在主页面上放不下,也可以加入更多页面。这些额外的页面就成为主页面 的子页面,用户通过轻触特定类型的偏好设置来访问这些页面。 您显示的每一个偏好设置都必须具有特定的类型。偏好设置的类型定义了 Settings程序如何对其进行显示。大多数偏好设置类型都和某种类型的、用于 进行设置的控件相关联,而另外一些类型则提供一种偏好设置的组织方式。表 9-1列出了 Settings程序支持的各种元素类型,以及如何用这些类型来实现自 己的偏好设置页面。 表 9-1  偏好设置元素的类型 元素类 型 描述 文本框 文本框类型显示一个可选的标题和一个可编辑的文本输入框,适 用于需要用户输入自定义字符串的偏好设置。 这个类型的键是 PSTextFieldSpecifier。 标题 标题类型显示一个只读的字符串,适用于显示只读字符串的偏好 设置(如果偏好设置包含隐含或非直接的值,这个类型可以将可 能的值映射为字符串)。 这个类型的键是 PSTitleValueSpecifier。 拨动开 关 拨动开关类型显示一个 ON/OFF拨动按键,适用于配置值为二选 一的偏好设置。这个类型通常用于表示包含布尔值的偏好设置, 但也可以用于表示包含非布尔值的偏好设置。 这个类型的键是 PSToggleSwitchSpecifier。 滑块 滑块类型显示一个滑块控件,适用于值为一个范围的偏好设置。 这个类型的值是一个实数,值的最小和最大值由您来指定。 这个类型的键是 PSSliderSpecifier。 值列表 值列表类型使用户可以从一个值的列表中选择其一,适用于支持 多个互斥值的偏好设置,这些值的类型可以是任意的。 这个类型的键是 PSMultiValueSpecifier。 组 组类型使您可以将几组不同的偏好设置组织到一个页面上。组类 型并不表示一个可配置的偏好设置,而只是包含一个标题字符 串,显示在一或多个可配置的偏好设置之前。 这个类型的键是 PSGroupSpecifier。 子页面 子页面类型使用户可以访问新的偏好设置页面,适用于实现多层 次的偏好设置。有关如何配置和使用这个类型的更多信息,请参 见 “多层次的偏好设置 ” 。这个类型的键是 PSChildPaneSpecifier。 各种偏好设置类型的详细格式信息请参见 Settings程序的结构参考 。如果要了 解如何创建和编辑 Setting程序的页面文件,则请参见 “添加和修改 Settings程 序包 ”部分。 Settings程序包 在 iPhone OS中,开发者通过一种特殊的 Settings程序包 来指定应用程序的偏 好设置,这种程序包命名为 Settings.bundle,驻留在应用程序程序包的 顶级目录上。该程序包中包含一或多个 Settings页面文件,用于定义应用程序 偏好设置的详细信息;还可以包含显示偏好设置需要的其它支持文件,比如图 像或本地化文件。表 9-2列出了一个典型 Settings程序包的内容。 表 9-2  Settings.bundle目录下的内容 项目名称 描述 Root.plist 这个 Settings页面文件包含根页面的偏好设置,它的内容 在 “Settings页面文件的格式 ” 部分有更详细的描述。 其它 .plist 文件 如果您需要通过多个子面板来构建一组有层次结构的偏好 设置,则每个子面板的内容都分别存储在不同的 Settings 页面文件中。您需要负责命名这些文件,并将它们关联到 正确的子面板上。 一或多 个 .lproj 目 录 这些目录用于存储 Settings页面文件的本地化字符串资 源。每个目录都包含一个字符串文件,文件的标题在 Settings页面中指定。这些字符串文件为偏好设置提供可 以直接显示给用户的本地化内容。 其它图像 如果您使用滑块控件,则可以将滑块的图像存储在程序包 的顶级目录下。 除了 Settings程序包之外,应用程序的程序包中还可以包含应用程序设置的定 制图标。如果应用程序包的顶级目录含有名为 Icon-Settings.png的文 件,则该文件包含的图标会被 Settings程序用于标识应用程序的偏好设置。如 果不存在这样的文件, Settings程序会转而采用应用程序的图标文件(缺省为 Icon.png),并进行必要的缩放处理。您的 Icon-Settings.png文件必 须是 29 x 29像素的图像。 在启动时, Settings程序会检查每一个定制的应用程序是否包含 Settings程序 包,并对其进行装载,然后将相应的应用程序名称和图标显示在 Settings程序 的主页面上。当用户轻触您的应用程序对应的行时, Settings程序会装载 Settings程序包的 Root.plist页面文件,并根据该文件的定义显示应用程序 的主设置页面。 除了装载程序包的 Root.plist页面文件之外, Settings程序还会在必要时装 载与该文件相关联的语言资源。每个 Settings页面文件都可以有一个关联 的 .strings文件,用于包含可见字符串的本地化值。在准备显示偏好设置信 息时, Settings程序会根据用户偏好的语言来寻找相应的字符串资源,并在显 示之前替换偏好设置页面中对应的内容。 Settings页面文件的格式 Settings程序包中的每个 Settings页面文件都以 iPhone设置 属性列表 的文件格 式(它是一种结构化的文件格式)进行存储。编辑 Settings页面文件的最简单 方法,就是使用 Xcode内置的编辑器组件,具体做法请参见 “为 Settings页面的 编辑做准备 ”部分;您也可以用属性列表编辑器程序来进行编辑,它是 Xcode的 工具之一。 请注意: 在连编时, Xcode会将工程中基于 XML的属性文件自动转换为二进制 格式,转换过程是连编时自动完成的,目的是节省磁盘空间。 每个 Settings页面文件的根元素都包含表 9-3列出的键。事实上,只有一个键 是必须的,但我们推荐包含所有的两个键。 表 9-3 Settings页面文件中的根键 键 类型 值 Preferenc eSpecifie rs (必须包含 ) 数组 这个键的值是一个 字典数组 ,数组中的每个字典 都包含一个偏好设置元素的信息。有关元素类型 列表请参见 表 9-1,与元素类型相关联的键的描 述,则请参见 Settings程序的结构参考 。 StringsTa ble 字符 串 和这个页面文件相关联的字符串文件的名称。程 序包中专用于语言的工程目录应该包含这个字符 串文件的一个拷贝(带有相应的本地化字符 串)。如果您没有包含这个键,则表示页面文件 中的字符串没有被本地化。有关如何使用这些字 符串的信息,请参见 “本地化资源 ”部分。 多层次的偏好设置 如果您希望以一定的层次结构组织偏好设置,则您定义的每个页面都必须有它 自己的 .plist文件,每个 .plist文件包含一组仅在该页面显示的偏好设 置。应用程序偏好设置的主页面总是存储在 Root.plist文件中,其它页面 则可以根据自己的喜好进行命名。 为了建立父子页面之间的连接,您需要在父页面中包含一个子面板元素。子面 板元素负责占据一行,在用户触击时显示一个新的设置 Settings页面。子面板 元素的 File键标识一个 .plist文件的名称,该文件负责定义子页面的内 容; Title键则标识子页面的标题,该标题也作为子面板元素行的文本。 Settings程序会自动提供子页面的漫游控制,使用户可以回到父页面。 图 9-1展示了一组多层次的页面是如何工作的。图的左边显示了 .plist文 件,右边则显示各个页面之间的关系。 图 9-1  用子面板组织偏好设置 有关子面板元素及其关联键的更多信息,请参见 Settings程序的结构参考 。 本地化资源 由于偏好设置中包含用户可见的字符串,所以您应该在 Settings程序包中为那 些字符串提供本地化版本。对于程序包支持的每种本地化语言,偏好设置页面 都可以有一个 .strings文件与之对应。当 Settings程序碰到一个支持本地化 的键时,就会在相应本地化版本的 .strings文件中寻找匹配的键,如果找到 了,就显示与之关联的值。 在寻找诸如 .strings文件这样的本地化资源时, Settings程序遵循和 Mac OS X程序一样的规则,即首先寻找与用户偏好语言相匹配的本地化资源,如果 该版本的资源不存在,再选择缺省语言的版本。 有关字符串文件的格式、语言工程目录、以及如何从程序包中取得特定语言资 源的相关信息,请参见 国际化编程主题 。 添加和修改 Settings程序包 Xcode提供了一个为当前工程添加 Settings程序包 的模板。缺省的 Settings程 序包中包含一个 Root.plist文件,以及一个用于存放本地化资源的缺省语 言目录。您可以在这个基础上进行扩展,加入 Settings程序包需要的其它属性 列表文件和资源。 添加 Settings程序包 通过如下步骤可以为您的 Xcode工程添加一个 Settings程序包: 1. 选择 File > New File. 2. 选择 iPhone OS > Settings > Settings Bundle template. 3. 将文件命名为 Settings.bundle. 除了在工程中添加一个新的 Settings程序包之外, Xcode还自动将该程序包加 入到应用程序目标的 Copy Bundle Resources连编阶段中。这样,您需要做的 就只是修改 Settings程序包中的 属性列表 文件和添加其它资源了。 新添加的 Settings.bundle程序包具有如下结构: Settings.bundle/ Root.plist en.lproj/ Root.strings 为 Settings页面的编辑做准备 用 Settings程序包模板创建 Settings程序包之后,您可以将结构文件 ( schema file)的内容进行格式化,使它们更容易编辑。下面的步骤向您展 示如何格式化 Settings程序包的 Root.plist文件,这些步骤同样适用于您创 建的其它结构文件。 1. 显示 Settings程序包中 Root.plist文件的内容。 a. 在 Groups & Files列表中,展开 Settings.bundle,查看程序 包的内容。 b. 选择 Root.plist文件,其内容就会显示在 Detail视图中 . 2. 在 Detail视图中,选择 Root.plist文件的 Root键。 3. 选择 View > Property List Type > iPhone Settings plist. 这个命令会 将 Detail视图中的属性列表内容进行格式化。 Xcode不是直接显示属性列 表的键和值,而是将它们显示为可读的字符串(如图 9-2所示),使我 们更加易于理解和编辑文件的内容。 图 9-2  格式化过的 Root.plist文件内容 配置一个 Settings页面:一个教程 这个部分包含一个教程,目的是向您展示如果配置一个 Settings页面,使它显 示您需要的内容。教程的目标是创建一个像图 9-2这样的页面,如果您之前还 没有为自己的工程创建 Settings程序包,则在执行下面这些步骤之前,应该按 照 “为 Settings页面的编辑做好准备 ”部分的描述进行准备。 图 9-3  一个根 Settings页面 1. 将 Settings Page Title 键的值改为您的应用程序名称。 双击 YOUR_PROJECT_NAME文本并将它改为 MyApp。 2. 展开 Preference Items键,显示模板包含的缺省项目。 3. 将 Item 1的标题改为 Sound: ■ 展开 Preference Items的 Item 1。 ■ 将 Title键的值由 Group改为 Sound。 ■ 保持 Type键的值不变,仍然为 Group。 4. 为新命名的 Sound组创建第一个拨动开关。 ■ 选中 Preference Items的 Item 3项,并选择 Edit > Cut命 令。 ■ 选中 Item 1,并选择 Edit > Paste命令(这会将拨动开关项移到 文本框项的前面)。 ■ 展开拨动开关项,显示其配置键。 ■ 将 Title 键的值改为 Play Sounds。 ■ 将 Identifier键的值改为 play_sounds_preference。现 在,这个项目的配置应该如下图所示: 5. 为 Sound 组创建第二个拨动开关。 ■ 选中 Item 2(即 Play Sounds拨动开关)。 ■ 选择 Edit > Copy命令。 ■ 选择 Edit >Paste命令,将拨动开关的拷贝放到第一个的下面。 ■ 展开新的拨动开关项,显示其配置键。 ■ 将其 Title键的值改为 3D Sound。 ■ 将其 Identifier键的值改为 3D_sound_preference。 6. 现在,您已经完成了第一组设置,可以开始创建 User Info组了。 7. 将 Item 4改为 Group类型的元素,并命名为 User Info。 ■ 在 Preferences Items中点击 Item 4,显示一个项目类型列 表的下拉菜单。 ■ 从下拉菜单中,选择 Group 元素类型。 ■ 展开 Item 4的内容。 ■ 将 Title键的值设置为 User Info。 8. 创建 Name域。 ■ 选择 Preferences Item中的 Item 5。 ■ 使用下拉菜单,将其类型改为 Text Field。 ■ 将 Title键的值改为 User Info。 ■ 将 Identifier键的值改为 user_name。 ■ 合上展开按键,隐藏这个项目的内容。 9. 创建 Experience Level设置。 ■ 选择 Item 5并点击加号( +)键(或者按下回车键),创建一个 新的项目。 ■ 点击这个新创建的项目,将其类型设置为 Multi Value。 ■ 展开项目的内容,将其标题设置为 Experience Level,标识 设置为 experience_preference,缺省值设置为 0。 ■ 选中 Default Value键,点击加号键加入一个 Titles数组。 ■ 通过展开键打开 Titles数组,点击表格右侧的项目按键。点击这 个键可以为 Titles添加一个新的子项目。 ■ 选中新添加的子项目,点击两次加号键,创建总共三个子项目。 ■ 将子项目的值设置为 Beginner、 Expert、和 Master。 ■ 再次选择 Titles键,点击其展开键,将子项目隐藏起来。 ■ 点击加号键,创建 Values数组。 ■ 在 Values数组中加入三个子项目,将它们的值分别设置为 0、 1、 和 2。 ■ 点击 Item 6的展开按键,隐藏其内容。 10. 添加设置页面的最好一组。 ■ 创建一个新项目,将其类型设置为 Group,标题设置为 Gravity。 ■ 再次创建一个新项目,将其类型设置为 Slider,标识设置为 gravity_preference,缺省值设置为 1,最大值设置为 2。 创建额外的 Settings页面文件 Settings程序包模板包含一个 Root.plist文件,用于定义应用程序的顶级 Settings页面。您如果要定义额外的 Settings页面,必须在 Settings程序包中 加入额外的属性列表文件,您可以在 Finder或 Xcode中进行添加。 在 Xcode中为 Settings程序包添加属性列表的步骤如下: 1. 在 Groups & Files面板中,打开 Settings程序包,选中 Root.plist文 件。 2. 选择 File > New命令。 3. 选择 Other > Property List命令。 4. 选中新生成的文件,并选择 View > Property List Type > iPhone Settings plist命令,将它配置为一个设置文件。 往 Settings程序包加入新的 Settings页面之后,就可以按照 “配置一个 Settings 页面:一个教程 ”部分描述的那样,在页面中显示设置。您必须通过一个子面板 元素对其进行引用,详情请参见 “多层次的偏好设置 ”部分的描述。 访问您的偏好设置 iPhone应用程序可以通过 Foundation或者 Core Foundation框架 来读写偏好 设置的值。在 Foundation框架中,您可以通过 NSUserDefaults类来读写 偏好设置的值;而在 Core Foundation框架中,您则可以使用几个与偏好设置 相关的函数。 程序清单 9-1展示一个如何在应用程序中读取偏好设置的简单实例,例子中通 过 NSUserDefaults类取得一个在 “配置一个 Settings页面:一个教程 ”部分 中创建的偏好设置值,并将它赋值给应用程序的一个实例变量。 程序清单 9-1  访问应用程序偏好设置的值 - (void)applicationDidFinishLaunching:(UIApplication *)application { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [self setShouldPlaySounds:[defaults boolForKey:play_sounds_preference]]; // Finish app initialization... } 有关 NSUserDefaults类中用于读写偏好设置值的方法的更多信息,请参见 NSUserDefaults类参考 ;有关读写偏好设置的 Core Foundation函数,请参 见 偏好设置工具参考 。 在仿真器中调试应用程序的偏好设置 在运行您的应用程序时, iPhone Simulator会将所有偏好设置的值保存在 ~/ Library/Application Support/iPhone Simulator/User/ Applications//Library/Preferences目录下,这里的 是一个由程序生成的目录名, iPhone OS用它来标识您的应用程 序。 每次重新安装应用程序时, iPhone OS都会执行一次干净的安装,将之前所有 的偏好设置删除。换句话说,在 Xcode中连编或运行应用程序会导致老版本的 所有内容被新版本所代替。如果您要测试应用程序在两次运行之间偏好设置发 生的变化,则必须直接从仿真器界面上运行,而不应该通过 Xcode运行。 文档修订历史 下表描述了 iPhone应用程序编程指南文档的修改记录。 日期 备注 2009-06- 17 加入与指南针接口有关的信息。   将有关 OpenGL支持的部分转移到 iPhone OpenGL ES编程指 南 文档中。   更新系统支持的 Info.plist键的列表。 2009-05- 14 更新整个文档,使之适合 iPhone OS 3.0。   在 “事件处理 ”一章的 “拷贝和粘贴 ”部分中加入示例代码。   在 “文件与连网 ”一章中加入有关 keychain数据的部分。   加入有关如何显示地图和电子邮件界面的信息。   做了一些小的修改。 2009-01- 06 改正了几个书写错误,澄清了为 Settings应用程序创建子页 面的过程。 2008-11- 12 增加一些与浮点数运算有关的指导原则。   更新与 iTunes备份内容相关的信息。 2008-10- 15 重新组织书中的内容。   将高级别的 iPhone OS信息转移到 iPhone OS技术概览 文档 中。   将有关标准系统 URL模式的信息转移到 苹果 URL模式参考 文 档中。   将有关开发工具和如何配置设备的信息转移到 iPhone OS开 发指南 文档中。   加入 “核心应用程序 ”一章,介绍应用程序的架构,讨论很多 创建 iPhone应用程序的指导原则。   加入 “文本和 Web”一章,讨论如何使用文本和 web类及如何 操作屏幕上的键盘。   将 “文件和连网 ”独立为一章,包含现有的内容。   将标题改为 iPhone应用程序编程指南。 2008-07- 08 新文档,描述 iPhone OS和 iPhone应用程序的开发流程。

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 15 人已下载

下载文档

相关文档