iPhone与iPad高级编程指南


移动与嵌入式开发技术 iPhone & iPad 高级编程 (美) Gene Backlin 著 岳虹 凌冲 译 北 京 Gene Backlin Professional iPhone and iPad Application Development EISBN:978-0-470-87819-4 Copyright © 2011 by Wiley Publishing, Inc. Indianapolis, Indiana All Rights Reserved. This translation published under license. 本书中文简体字版由 Wiley Publishing, Inc. 授权清华大学出版社出版。未经出版者书面许可,不得以任何方式 复制或抄袭本书内容。 北京市版权局著作权合同登记号 图字:01-2011-0944 本书封面贴有 Wiley 公司防伪标签,无标签者不得销售。 版权所有,侵权必究。侵权举报电话:010-62782989 13701121933 图书在版编目(CIP)数据 iPhone & iPad 高级编程/(美) 贝克林(Backlin, G.) 著;岳虹,凌冲 译. —北京:清华大学出版社,2012.1 书名原文:Professional iPhone and iPad Application Development (移动与嵌入式开发技术) ISBN 978-7-302-27445-2 Ⅰ. i… Ⅱ. ①贝… ②岳… ③凌… Ⅲ. ①移动电话机—应用程序—程序设计 ②便携式计算机—应用程序— 程序设计 Ⅳ. ①TN929.53 ②TP368.32 中国版本图书馆 CIP 数据核字(2011)第 247001 号 责任编辑:王 军 韩宏志 装帧设计:牛艳敏 责任校对:邱晓玉 责任印制: 出版发行:清华大学出版社 地 址:北京清华大学学研大厦 A 座 http://www.tup.com.cn 邮 编:100084 社 总 机:010-62770175 邮 购:010-62786544 投稿与读者服务:010-62776969, c-service@tup.tsinghua.edu.cn 质 量 反 馈:010-62772015, zhiliang@tup.tsinghua.edu.cn 印 刷 者: 装 订 者: 经 销:全国新华书店 开 本:185×260 印 张:34.25 字 数:943 千字 版 次:2012 年 1 月第 1 版 印 次:2012 年 1 月第 1 次印刷 印 数:1~4000 定 价:59.80 元 —————————————————————————————————————————————— 产品编号: 译 者 序 苹果公司分别于 2007 年 1 月 9 日和 2010 年 1 月 27 日推出了 iPhone 和 iPad 产品。iPhone 是将 移动通信、宽屏触控、网页、地图和电子邮件功能完美融合在一起的智能手机;iPad 则是同样具有 网页、地图和电子邮件功能,并能提供音频和视频服务的平板电脑。iPhone 和 iPad 引入了基于大型 多点触控显示屏和新软件的全新用户界面,用户只需轻舒手指,就能酣畅地享用各种功能和服务。 目前为止,iPhone 更新到 iPhone 4,而 iPad 则更新到 iPad 2。33 年风雨路让苹果公司经历了大起大 落,从 1996 年开始,重新入主苹果公司的 Jobs 带领苹果重回个人 PC 舞台中心,再次迎来了黄金发 展期。 用户眼中看到的是苹果产品完美的细节和非同凡响的想象力;程序员眼中看到的则是苹果公司 从硬件到系统软件、从 Objective-C 语言到 Cocoa 框架的长期核心技术沉淀和积累。目前,苹果公司 的各种产品在国内已经拥有相当高的市场占有率,这对程序员而言无疑是难得的良机。目前国内关 于此类应用程序开发的中文参考书籍十分匮乏,因此,我们决定翻译本书,希望本书能够进一步促 进 iPhone 和 iPad 程序员的开发水平,也希望帮助对此感兴趣的初学者步入 iPhone 和 iPad 应用程序 开发殿堂。 本书作者 Gene Backlin 在计算机工业领域有超过 30 年的技术咨询经验,供职于 DePaul 大学计 算与数字媒体学院。Gene 拥有多年的实践经验和教学经验,书中在讨论每个主题时都紧贴实际,详 细列出开发步骤,并对涉及的源代码进行精讲。本书还讲述了如何查阅苹果开发者计划网站中的资 料、文档和代码,以便您了解更多技术信息和学习方法,“授您以渔”,力求使您收到圆满的学习 效果。 如果您跟随作者的讲述,动手完成书中的每个示例,那么您将拥有 iPhone 和 iPad 应用程序开 发的编程能力和宝贵经验,更重要的是,这些示例中的技术细节很可能在您今后的程序设计工作中 遇到,有些代码甚至可以原封不动地应用到开发当中。可以说,本书绝不是一本乏味的参考书,而 是一本能够使读者在收获知识同时感受愉悦的读物。 关于本书内容的全面介绍,请参考前言部分,在此不再赘述。对于本书的翻译,我们力求做到 语言平实无华,技术方面准确无误,期望能给读者带来轻松的阅读体验。在四个月的翻译过程中, 我们反复对本书的内容进行推敲、修改和查证,真诚希望为您奉献一本优秀作品,使本书能成为您 的良师益友。 作 者 简 介 Gene Backlin 是 MariZack 咨询公司的所有者兼首席顾问,MariZack 公司成立于 1991 年,将“帮 助客户取得成功”奉为唯一宗旨。Gene Backlin 为客户提供咨询服务的时间已逾 30 年,服务过的客 户包括 IBM、McDonnell Douglas、Waste Management、美国环境保护署(U.S. Environmental Protection Agency)、国家银行(Nations Bank)、美国银行(Bank of America)和第一银行(Bank One)等。Gene Backlin 也是 DePaul 大学计算与数字媒体学院的教师。 Gene Backin 少年时代就对电子学产生了浓厚的兴趣,这也帮助他成年后进入了计算机行业。 Gene Backlin 至今还保留着他在 1978 年制作的 H-8 数字计算机和 H-9 视频终端。他自学了 Extended Benton Harbor Basic 编程语言。在 IBM-PC 上市后,Gene Backlin 开发出符合 Heathkit H-151 标准的 计算机,这些计算机至今仍在运行。如果您问他什么是信息革命,他的回答一定是“这场革命令人 心醉神迷,带给人巨大的欢乐”。他在开发中使用过从纸带加载程序的计算机,乃至革命性的 NeXT 计算机(他至今仍有两台),再到 iPhone 和 iPad。Gene 为自己亲眼目睹计算机行业的飞速发展而感到 荣幸,更为能在该行业中大显身手而自豪。 Gene Backlin 曾在 1995 年撰写 Developing NeXTSTEP Applications 一书,本书是该书的姊妹篇。 技术编辑简介 Daniel W. Meeks 目前在贝尔实验室为 Unix 操作系统 9.0(及其以后的版本)和 2.8 BSD 操作系统 开发 grep、awk 和 sed 工具。Daniel W. Meeks 曾在内珀维尔的贝尔实验室参与开发 C++ Cfront 1.0 编译器。此后,Daniel W. Meeks 一直带领其技术小组为全球多家金融机构开发 C、Objective-C、C++、 Java 和 C#项目并提供支持。他现在利用周末时间建设网络基础设施,并将在 Apple、Windows 和 Android 操作系统上开发移动应用程序作为自己的爱好。 致 谢 首先感谢妻子 Roseann、儿子 Zachary 和女儿 Marissa,在撰写本书的过程中,你们给予我极大 的关爱。还有我的儿子 Ethan,你使我青春永驻,永远年轻。 还要感谢我的母亲 Mary Louise 和女儿 Hannah Angel,你们教会了我很多东西,你们的精神伴 随我生命中的每一天。 感谢我的父亲,当我需要您的时候您总出现在我的身边,您还给了我一个伟大的姓氏—— Gene! 感谢 Helen 和 Jerry,你们对我来说无比重要。 感谢 Dan,很高兴时隔多年后与你重逢并共享美好时光。 本书是多位人士辛勤劳动的结晶;Carol、Ami 和 Luann 为本书的问世付出了极大的心血,特此 对三位表达衷心的谢意。 最后再次感谢 Jean-Marc Krikorian,与您在 1995 年所做的一切相比,我所做的真的不算什么。 我不会忘记你们——我的读者,你们抽出了很多时间与我分享本书。本书为你们而写的,希望 能对你们有所帮助。如有任何问题或意见,请告诉我,再次感谢你们! 前 言 在 1975 年,我购买了一部 TI 公司生产的 SR-56 袖珍计算器,之所以购买它,是因为我看中了 它的可编程功能。SR-56 袖珍计算器有 10 个存储器和 100 个编程步骤。在摆弄了大概一个星期后, 我开始领会到程序的力量。我至今仍然保存着这个计算器,虽然它已经不能运行了,但它会唤起我 对那段美好编程时光的回忆。 在过去几十年中,我在工作中使用过很多种技术和程序语言,久而久之,开发过程趋于平淡和 乏味。起初令我心潮澎湃的那部 SR-56 也逐渐光环不在;直到 1989 年,NeXT 计算机的问世重新点 燃我的激情。NeXT 计算机为开发人员提供了一套丰富的工具集,使用这套工具集可在很短的时间 内得心应手地开发出复杂应用程序。NeXT 计算机使软件开发工作变得富有意义。 在 Mac 平台引入了 NeXT 开发环境后,两者开始了完美的融合,孕育出今天的 iPhone、iPod touch 和 iPad。从我开始在那部 SR-56 上编写第一个程序至今已经 35 年了,我始终对编程充满兴趣,就 像一个孩子进了糖果店一样,迷恋其中,流连忘返。本书将帮您深入了解如何在上述设备中使用相 应的开发工具得心应手地完成开发工作。 读者对象 本书主要面向熟悉 Xcode 开发环境和 Objective-C 程序语言的开发人员。 如果您熟悉应用程序开发但不了解 Xcode 开发环境和 Objective-C 程序语言,请首先访问苹果公 司 iPhone 开发中心(Apple iPhone Dev Center)进行学习,网址为 https://developer.apple.com/iphone/。 本书每一章都讨论一个关于 iPhone/iPad 设备的专题或特性,将带您一步步地创建具有这些特性 的应用程序(完整的工作示例)。开发过程中的源代码以模块形式组织,这样您就可以提取这些源代 码,并在自己的应用程序中实现相同的功能。 本书内容 本书的主题涵盖使用当前的 SDK 4.0 开发 iPhone 和 iPod touch 应用程序,以及使用 SDK 3.2 开 发 iPad 应用程序。Xcode 和 Interface Builder 是本书创建所有应用程序的主要开发工具。最后讨论了 使用 Instruments 应用程序提高性能的问题。 编排方式 每一章都会浓墨重彩地描述 SDK 中的应用程序框架,然后通过循序渐进的过程指引您设计应 用程序,以便帮助您理解这些框架,并学会在何时和何处将框架整合到自己的应用程序中。本书中 iPhone & iPad 高级编程 X 的内容需要通过动手实践来学习,在完成了所有“开发步骤”小节中的动手练习后,您将会拥有使 用 iOS 4 为 iPhone 和 iPad 创建和添加功能的经验。本书主题涵盖表格视图、图像视图、选择器、数 据存储、音频和视频等。最后,本书最后讨论如何确保应用程序高效运行,以便为那些从苹果公司 iTunes App Store 购买您的应用程序的用户提供令他们满意的用户体验。 使用本书前的准备工作 为了开发在 iPhone 或 iPod Touch 上运行的应用程序,需要下载 iPhone SDK 4.0。SDK 4.0 中包 括了 SDK 3.2,以便您开发在 iPad 上运行的应用程序。可以从 http://developer.apple.com/iphone/网页 中获取 SDK。 虽然 SDK 是免费的,但您仍然需要注册为苹果公司的会员才能进行下载。如果只是想开发在 SDK 中的 iPhone/iPad 模拟器上运行的应用程序,那么注册为开发会员是免费的。如果想开发能在 真实设备中安装的应用程序,或想在苹果公司的 iTunes Store 销售应用程序,就必须注册为付费会员。 目前,价格最低的注册年费 99 美元。 安装 iPhone SDK 4.0 需要一台运行 Mac OS X 10.6.2(Snow Leopard)或更高版本的 Macintosh 计算机。 源代码 读者在学习本书中的示例时,既可以手动输入所有的代码,也可以使用本书附带的源代码文件。 本书使用的所有源代码都可以从本书合作站点http://www.wrox.com/或www.tupwk. com.cn/downpage 上下载。只要登录站点 http://www.wrox.com/,使用 Search 工具或使用书名列表就可以找到本书。接 着单击本书细目页面上的 Download Code 链接,就可以获得所有源代码。 勘误表 尽管我们已经尽了最大的努力来保证文章或代码中不出现错误,但是错误总是难免的,如果您 在本书中找到了错误,例如拼写错误或代码错误,请告诉我们,我们将非常感激。通过勘误表,可 以让其他读者避免走入误区,当然,这还有助于提供更高质量的信息。 要在网站上找到本书英文版的勘误表,可以登录 http://www.wrox.com,通过 Search 工具或书名 列表查找本书,然后在本书的细目页面上,单击 Book Errata 链接。在这个页面上可以查看到 Wrox 编辑已提交和粘贴的所有勘误项。完整的图书列表还包括每本书的勘误表,网址是 www.wrox.com/misc-pages/booklist.shtml。 提示:由于许多图书的书名都很类似,所以按 ISBN 进行搜索是最简单的,本书 英文版的 ISBN 是 978-0-470-87819-4。 前 言 XI 如果你在勘误表上没有找到错误,那么可以到 www.wrox.com/contact/techsupport.shtml 上,完成 上面的表格,并把找到的错误发送给我们。我们将会核查这些信息,如果无误的话,会把它放置到 本书的勘误表中,并在本书的后续版本中更正这些问题。 p2p.wrox.com 要与作者和同行讨论,请加入 p2p.wrox.com 上的 P2P 论坛。这个论坛是一个基于 Web 的系统, 便于您张贴与 Wrox 图书相关的消息和相关技术,与其他读者和技术用户交流心得。该论坛提供了订 阅功能,当论坛上有新的消息时,它可以给您传送感兴趣的论题。Wrox 作者、编辑和其他业界专家 和读者都会到这个论坛上来探讨问题。 在 http://p2p.wrox.com 上,有许多不同的论坛,它们不仅有助于阅读本书,还有助于开发自己 的应用程序。要加入论坛,可以遵循下面的步骤: (1) 进入 p2p.wrox.com,单击 Register 链接。 (2) 阅读使用协议,并单击 Agree 按钮。 (3) 填写加入该论坛所需要的信息和自己希望提供的其他信息,并单击 Submit 按钮。 (4) 你会收到一封电子邮件,其中的信息描述了如何验证账户和完成加入过程。 加入论坛后,就可以张贴新消息,回复其他用户张贴的消息。可以随时在 Web 上阅读消息。如 果要让该网站给自己发送特定论坛中的消息,可以单击论坛列表中该论坛名旁边的 Subscribe to this Forum 图标。 关于使用 Wrox P2P 的更多信息,可阅读 P2P FAQ,了解论坛软件的工作情况以及 P2P 和 Wrox 图书的许多常见问题。要阅读 FAQ,可以在任意 P2P 页面上单击 FAQ 链接。 提示:不加入 P2P 也可以阅读论坛上的消息,但要张贴自己的消息,就必须加入 该论坛。 导 航 本章内容: ● 如何在 iPad 拆分视图中进行导航 ● 使用工具栏旋转图像 ● 使用选项卡栏实现一个简单的银行账户交易跟踪器 导航是一个通过层次结构找到所需信息的过程。在 iPhone 和 iPad 中,可以使用以下组件来 导航数据;其中每个组件的导航观感各不相同: ● 导航栏(Navigation bar)——按照层次结构安排数据,可以使用下查功能进行导航,同时提 供返回上层或顶层的路径。 ● 工具栏(Toolbar)——提供一些用于操作当前视图上下文的选项。 ● 选项卡栏(Tab bar)——为同一组数据提供不同的视图。 本章介绍了为上述每个导航组件创建应用程序的步骤,向您演示每种导航风格的一个简单用 例。运行应用程序的设备决定了所实现的导航风格。由于受限于可视区域,iPhone 比 iPad 具有更 多的下查数据。如果准备开发同时兼容这两种设备的应用程序,就必须注意这一点。从视觉表现 方面讲,iPad 不应该只是复制一个应用程序。 1 第 章 在设计应用程序的导航方式时,请查看苹果公司的iPad和iPhone用户界面指南。 请参阅附录 D,查看苹果公司提供的相关文档资源及其他开发指南。 iPhone & iPad 高级编程 2 1.1 导航栈 导航过程基于栈。视图以后进先出(LIFO)的方式存入由视图控制器(View Controller)管理的栈 中。初始视图是根视图控制器,可被其他视图覆盖。但与其上的其他视图不同,根视图控制器不 能删除。导航过程对用户交互作出响应,将视图控制器压入和弹出导航栈。每个当前视图负责将 下一个视图压入导航栈中。 为导航项提供具体内容的是导航栏。用户单击导航栏上的按钮将产生委托消息,这些消息发 送到视图控制器,从而执行视图的压入或弹出操作。 1.2 导航栏 应用程序中的导航栏主要包括用于导航视图的控件对象。导航栏为用户提供了按照应用程序 的层次结构将视图压栈或弹栈所需的全部控件。由关联的视图控制器通过委托方法来处理委托 消息。 1.2.1 UINavigationBarDelegate 协议 要将一个导航项压入或弹出导航栈,视图控制器需要实现 UINavigationBarDelegate 协议的多 个方法。 需要实现的方法如下: ● 压入导航项 (1) navigationBar:shouldPushItem: (2) navigationBar:didPushItem: ● 弹出导航项 (1) navigationBar:shouldPopItem: (2) navigationBar:didPopItem: 1.2.2 配置导航栏 导航栏位于视图顶部,会显示当前视图的标题。此外,导航栏还可能包含一些按钮控件,以 便用户在当前视图上下文中完成一些操作。为了实现此功能,可以使用以下几种方法: ● 位于导航栏左边的 backBarButtonItem 和 leftBarButtonItem。 ● 位于导航栏中间的 titleView。 ● 位于导航栏右边的 rightBarButonItem。 导航栏本身具有一些可以被修改的属性: ● barStyle ● translucent ● tintColor 第 1 章 导 航 3 1.2.3 压入和弹出导航项 从一个视图导航到另一个视图,要么是继续下查(压栈),要么是返回上层(弹栈),这些操作都 由应用程序中的视图控制器来处理。可将导航过程简单地视为由导航控制器在导航栈中管理一系 列视图控制器的行为。 视图控制器负责在导航栈中压入(在层次结构中下压)和弹出(在层次结构中返回)其他视图控 制器。导航过程如下: (1) 创建 UINavigationController。 (2) 导航控制器将视图控制器压入导航栈。 (3) 视图控制器显示下一个视图。 (4) 视图控制器取消上一个视图。 1.3 一个简单的导航栏 在此应用程序中,导航功能是显示 1~20 之间的数字。这 20 个数字按照奇偶数分组。需要注 意,使用 iPad 的拆分视图(split view)时,在纵向方向上,导航栏呈现为弹出菜单(popover),而在 横向方向上,导航栏则显示在拆分视图的左侧窗格中,如图 1-1 所示。 图 1-1 点击 Odd 选项将看到另一个导航栏,其中列出了 1~20 之间的奇数,如图 1-2 所示。 点击该列表中的任意一个数字,将在详细信息视图中看到所选择的内容,而弹出菜单则会消 失(如图 1-3 所示)。 iPhone & iPad 高级编程 4 图 1-2 图 1-3 1.3.1 开发步骤:一个简单的导航栏 创建这个作为服务器的应用程序需要执行以下两个步骤: (1) 启动 Xcode 软件,创建一个名为 NavigationBar-iPad 的基于拆分视图的应用程序。附录 A 列出了开始创建拆分视图应用程序的步骤。 (2) 在 Xcode 的 Groups&Files 区域中单击 Classes 分组。选择 File | New File,然后选择 UIViewController 子类,接着选中 UITableViewController 子类选项并将其命名为 RootDetailView- Controller。 此应用程序不使用 Interface Builder,而是通过编写代码来创建所有视图。下面开始设计程序 逻辑。 简单导航栏应用程序的源程序清单 直接使用 NavigationBar_iPadAppDelegate.h 文件和 NavigationBar_iPadAppDelegate.m 文件, 不对其进行修改。 修改 RootViewController.h 模板 RootViewController 类需要添加两个 NSArrays 来保存偶数和奇数。这两个数组保存在 NSDictionary 中,分别将 even 和 odd 作为关键字(key)(如程序清单 1-1 所示)。 程序清单 1-1 完整的 RootViewController.h 文件(Chapter1/NavigationBar-iPad/Classes/Root- ViewController.h) #import @class DetailViewController; 第 1 章 导 航 5 @interface RootViewController : UITableViewController { DetailViewController *detailViewController; NSDictionary *groupsDict; NSArray *evenArray; NSArray *oddArray; } @property (nonatomic, retain) IBOutlet DetailViewController *detailViewController; @property (nonatomic, retain) NSDictionary *groupsDict; @property (nonatomic, retain) NSArray *evenArray; @property (nonatomic, retain) NSArray *oddArray; - (void)initData; @end 修改 RootViewController.m 模板 前面更新了 RootViewController.m 模板的头文件,定义了添加到模板中,下面开始修改 Root- ViewController.m 模板。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 1-2 所示)。 程序清单 1-2 添加@synthesize #import "RootViewController.h" #import "DetailViewController.h" #import "RootDetailViewController.h" @implementation RootViewController @synthesize detailViewController; @synthesize groupsDict; @synthesize evenArray; @synthesize oddArray; 为了对视图进行初始化,需要定义弹出菜单的大小、偶数数组和奇数数组,并对偶数数组和 奇数数组进行初始化(如程序清单 1-3 所示)。 程序清单 1-3 视图初始化 #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setClearsSelectionOnViewWillAppear:NO]; [self setContentSizeForViewInPopover:CGSizeMake(200.0, 100.0)]; [self setTitle:@"Choices"]; [self initData]; } - (void)initData { NSMutableArray *even = [NSMutableArray array]; NSMutableArray *odd = [NSMutableArray array]; NSMutableDictionary *dict = [NSMutableDictionary dictionary]; NSString *msg = nil; for (int i=0;i<20; i++) { msg = [NSString stringWithFormat:@"Number = %d", i]; iPhone & iPad 高级编程 6 if ( i % 2 == 0 ) { [even addObject:msg]; } else { [odd addObject:msg]; } } [dict setObject:even forKey:@"Even"]; [dict setObject:odd forKey:@"Odd"]; [self setGroupsDict:dict]; } // Ensure that the view controller supports rotation and that the split view // can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } 在初始化表格视图时,需要考虑两个因素。如果只有一个单独的项列表,那么只需一个列表 区块和多个表示列表项的行。如果需要将列表中的某些项与其他项分开,则需要考虑创建多少个 列表区块,以便达到分开显示的目的。就本例而言,应用程序中只需一个列表来存放相关数据, 因此列表区块数量为 1。此时,行数由存放于字典 groupDict 中的两个数组来确定(如程序清单 1-4 所示)。 程序清单 1-4 定义 TableView 显示的内容 #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return [groupsDict count]; } tableView:cellForRowAtIndexPath 方法使用[[cell textLabel] setText:key]方法为表格视图中的每 个单元格赋值并显示相应的值。每个单元格的值即为 groupsDict 中每项的关键字值(key value)。在 本应用程序中,偶数数组的关键字是 Even,奇数数组的关键字则为 Odd(如程序清单 1-5 所示)。 程序清单 1-5 TableView 单元格显示的内容 #pragma mark - #pragma mark Table view delegate - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self groupsDict] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @"CellIdentifier"; // Dequeue or create a cell of the appropriate type. 第 1 章 导 航 7 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]autorelease]; cell.accessoryType = UITableViewCellAccessoryNone; } // Configure the cell. [[cell textLabel] setText:key]; return cell; } 在本应用程序中,如果标有 Even 的单元格被点击,则会将 RootDetailViewController 压入导 航栈,最终视图中会显示偶数列表。如果点击了标有 Odd 的单元格,则奇数列表将显示出来(如 程序清单 1-6 所示)。 程序清单 1-6 选择 TableView 单元格 #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self groupsDict] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; NSArray *values = [[self groupsDict] objectForKey:key]; /* When a row is selected, set the detail view controller's detail item to the item associated with the selected row. */ RootDetailViewController *rootDetailViewController = [[RootDetailViewController alloc] initWithKey:key values:values viewController:[self detailViewController]]; [[self navigationController] pushViewController:rootDetailViewController animated:YES]; [rootDetailViewController release]; } 完整的 RootViewController.m 文件如程序清单 1-7 所示。 程序清单 1-7 完整的 RootViewController.m 文件(Chapter1/NavigationBar-iPad/Classes/Root- ViewController.m) #import "RootViewController.h" #import "DetailViewController.h" #import "RootDetailViewController.h" @implementation RootViewController @synthesize detailViewController; @synthesize groupsDict; @synthesize evenArray; @synthesize oddArray; iPhone & iPad 高级编程 8 #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setClearsSelectionOnViewWillAppear:NO]; [self setContentSizeForViewInPopover:CGSizeMake(200.0, 100.0)]; [self setTitle:@"Choices"]; [self initData]; } - (void)initData { NSMutableArray *even = [NSMutableArray array]; NSMutableArray *odd = [NSMutableArray array]; NSMutableDictionary *dict = [NSMutableDictionary dictionary]; NSString *msg = nil; for (int i=0;i<20; i++) { msg = [NSString stringWithFormat:@"Number = %d", i]; if ( i % 2 == 0 ) { [even addObject:msg]; } else { [odd addObject:msg]; } } [dict setObject:even forKey:@"Even"]; [dict setObject:odd forKey:@"Odd"]; [self setGroupsDict:dict]; } // Ensure that the view controller supports rotation and that // the split view can therefore // show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)aTableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return [groupsDict count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self groupsDict] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @"CellIdentifier"; // Dequeue or create a cell of the appropriate type. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 第 1 章 导 航 9 if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryNone; } // Configure the cell. [[cell textLabel] setText:key]; return cell; } #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self groupsDict] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; NSArray *values = [[self groupsDict] objectForKey:key]; /* When a row is selected, set the detail view controller' s detail item to the item associated with the selected row. */ RootDetailViewController *rootDetailViewController = [[RootDetailViewController alloc] initWithKey:key values:values viewController:[self detailViewController]]; [[self navigationController] pushViewController:rootDetailViewController animated:YES]; [rootDetailViewController release]; } #pragma mark - #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Relinquish ownership of anything that can be recreated // in viewDidLoad or on demand. // For example: self.myOutlet = nil; [self setGroupsDict:nil]; [self setEvenArray:nil]; [self setOddArray:nil]; [self setDetailViewController:nil]; } - (void)dealloc { [groupsDict release]; [evenArray release]; [oddArray release]; [detailViewController release]; [super dealloc]; } @end iPhone & iPad 高级编程 10 修改 RootDetailViewController.h 模板 RootDetailViewController 类显示实际偶数值或奇数值;当选中一个表格视图单元格时,主详 细信息页面中将显示所选单元格的值。完整的 RootDetailViewController 类如程序清单 1-8 所示。 程序清单 1-8 完整的 RootDetailViewController.h 文件(Chapter1/NavigationBar-iPad/Classes/ RootDetailViewController.h) #import @class DetailViewController; @interface RootDetailViewController : UITableViewController { DetailViewController *detailViewController; NSString *key; NSArray *values; } @property (nonatomic, retain) DetailViewController *detailViewController; @property (nonatomic, retain) NSString *key; @property (nonatomic, retain) NSArray *values; - initWithKey:(NSString *)aKey values:(NSArray *)aValues viewController:(id)viewController; @end 修改 RootDetailViewController.m 模板 前面更新了头文件,为模板定义了附加内容,下面开始修改 RootDetailViewController.m 模板。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 1-9 所示)。 程序清单 1-9 添加@synthesize #import "RootDetailViewController.h" #import "DetailViewController.h" @implementation RootDetailViewController @synthesize key; @synthesize values; @synthesize detailViewController; 为了对视图进行初始化,需要定义弹出菜单的大小、偶数数组和奇数数组,并对偶数数组和 奇数数组进行初始化(如程序清单 1-10 所示)。 程序清单 1-10 视图初始化 #pragma mark - #pragma mark Initialization - initWithKey:(NSString *)aKey values:(NSArray *)aValues viewController:(id)viewController { [self setKey:aKey]; [self setValues:aValues]; [self setDetailViewController:viewController]; return self; 第 1 章 导 航 11 } #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setClearsSelectionOnViewWillAppear:NO]; [self setContentSizeForViewInPopover:CGSizeMake(200.0, 500.0)]; [self setTitle:[self key]]; } #pragma mark - #pragma mark Rotation support // Ensure that the view controller supports rotation and that the // split view can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } 在初始化表格视图时,需要考虑两个因素。第一个考虑因素是列表区块数,所有数据将分别 放入区块中——本应用程序中只需一个列表区块,因为所有数据都是相关的。 第二个考虑因素是行数,这是用户可以从中作出选择的列表。在本应用程序中,数据行表示 所有偶数或所有奇数(如程序清单 1-11 所示)。 程序清单 1-11 定义 TableView 显示的内容 #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return [[self values] count]; } tableView:cellForRowAtIndexPath 方法用于将需要显示的详细信息填入表格视图单元格中。因 为 numberOfRowsInSection 使用 values 计数,所以表格视图单元格中显示的是实际奇数数字或偶 数数字(如程序清单 1-12 所示)。 程序清单 1-12 TableView 单元格显示的内容 #pragma mark - #pragma mark Table view appearance // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *value = [[self values] objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @"Cell"; iPhone & iPad 高级编程 12 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... [[cell textLabel] setText:value]; return cell; } 当点击一个表格视图单元格时,相应的行会被选中。就本应用程序而言,点击单元格会使相 应的数值显示在主详细信息视图中(如程序清单 1-13 所示)。 程序清单 1-13 选择 TableView 单元格 #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [[self detailViewController] setText:[[self values] objectAtIndex:[indexPath row]]]; } 完整的 RootDetailViewController.m 文件如程序清单 1-14 所示。 程序清单 1-14 完整的 RootDetailViewController.m 文件(Chapter1/NavigationBar-iPad/Classes/ RootDetailViewController.m) #import "RootDetailViewController.h" #import "DetailViewController.h" @implementation RootDetailViewController @synthesize key; @synthesize values; @synthesize detailViewController; #pragma mark - #pragma mark Initialization - initWithKey:(NSString *)aKey values:(NSArray *)aValues viewController:(id)viewController { [self setKey:aKey]; [self setValues:aValues]; [self setDetailViewController:viewController]; return self; } #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setClearsSelectionOnViewWillAppear:NO]; 第 1 章 导 航 13 [self setContentSizeForViewInPopover:CGSizeMake(200.0, 500.0)]; [self setTitle:[self key]]; } #pragma mark - #pragma mark Rotation support // Ensure that the view controller supports rotation and that the // split view can therefore // show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. return [[self values] count]; } #pragma mark - #pragma mark Table view appearance // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *value = [[self values] objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... [[cell textLabel] setText:value]; return cell; } #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [[self detailViewController] setText:[[self values] objectAtIndex:[indexPath row]]]; } #pragma mark - #pragma mark Memory management iPhone & iPad 高级编程 14 - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Relinquish ownership of anything that can be recreated // in viewDidLoad or on demand. // For example: self.myOutlet = nil; [self setDetailViewController:nil]; [self setKey:nil]; [self setValues:nil]; } - (void)dealloc { [detailViewController release]; [key release]; [values release]; [super dealloc]; } @end 修改 DetailViewController.h 模板 在导航栏中,无论是选择了奇数值还是偶数值,相应的值都会显示在 DetailViewController 的 视图中心的标签上。完整的 DetailViewController 文件如程序清单 1-15 所示。 程序清单 1-15 完整的 DetailViewController.h 文件(Chapter1/NavigationBar-iPad/Classes/ DetailViewController.h) #import @interface DetailViewController : UIViewController { UIPopoverController *popoverController; UIToolbar *toolbar; id detailItem; UILabel *detailDescriptionLabel; } @property (nonatomic, retain) IBOutlet UIToolbar *toolbar; @property (nonatomic, retain) id detailItem; @property (nonatomic, retain) IBOutlet UILabel *detailDescriptionLabel; - (void)setText:(NSString *)newText; @end 修改 DetailViewController.m 模板 前面更新了头文件,定义了添加到模板中的内容,下面开始修改 DetailViewController.m 模板。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 1-16 所示)。 程序清单 1-16 添加@synthesize #import "DetailViewController.h" #import "RootViewController.h" @interface DetailViewController () 第 1 章 导 航 15 @property (nonatomic, retain) UIPopoverController *popoverController; @end @implementation DetailViewController @synthesize toolbar, popoverController, detailItem, detailDescriptionLabel; 程序清单 1-17 中显示的 setText 方法用于设置视图中心的标签上的文本内容,并取消弹出菜单。 程序清单 1-17 setText 方法 #pragma mark - #pragma mark Managing the detail item - (void)setText:(NSString *)newText { [[self detailDescriptionLabel] setText:newText]; if (popoverController != nil) { [popoverController dismissPopoverAnimated:YES]; } } 完整的 DetailViewController.m 文件如程序清单 1-18 所示。 程序清单 1-18 完整的 DetailViewController.m 文件(Chapter1/NavigationBar-iPad/Classes/Detail- ViewController.m) #import "DetailViewController.h" #import "RootViewController.h" @interface DetailViewController () @property (nonatomic, retain) UIPopoverController *popoverController; @end @implementation DetailViewController @synthesize toolbar, popoverController, detailItem, detailDescriptionLabel; #pragma mark - #pragma mark Managing the detail item - (void)setText:(NSString *)newText { [[self detailDescriptionLabel] setText:newText]; if (popoverController != nil) { [popoverController dismissPopoverAnimated:YES]; } } #pragma mark - #pragma mark Split view support - (void)splitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController: (UIPopoverController*)pc { barButtonItem.title = @"Root List"; NSMutableArray *items = [[toolbar items] mutableCopy]; [items insertObject:barButtonItem atIndex:0]; [toolbar setItems:items animated:YES]; iPhone & iPad 高级编程 16 [items release]; self.popoverController = pc; } // Called when the view is shown again in the split view, invalidating // the button and popover controller. - (void)splitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { NSMutableArray *items = [[toolbar items] mutableCopy]; [items removeObjectAtIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = nil; } #pragma mark - #pragma mark Rotation support // Ensure that the view controller supports rotation and that the // split view can therefore show in both portrait and landscape. - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark - #pragma mark View lifecycle - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; // self.popoverController = nil; } #pragma mark - #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)dealloc { [popoverController release]; [toolbar release]; [detailItem release]; [detailDescriptionLabel release]; [super dealloc]; } @end 1.3.2 测试应用程序 各个步骤均已完成,现在启动 iPad 模拟器。此时,iPad 模拟器应该显示“一个简单导航栏” 一节开头描述的结果。 第 1 章 导 航 17 1.4 工具栏 工具栏和导航栏不同。首先,它们在视图中的显示位置是不同的,工具栏位于视图底部(导航 栏在顶部);其次,在使用工具栏时,可以通过工具栏上的按钮便捷地访问所有视图。用户不必通 过下查即可找到想要的视图,只需点击工具栏上的按钮,相关的视图就立刻显示出来。 工具栏中的项(包括固定项和扩展项)均匀布置,以保证工具栏整洁一致。需要注意的是,由 于手指比较粗而 iPhone 的屏幕空间有限,用手指在多于 5 个项的工具栏中选择时是非常容易选错 的。iPad 则有更大的视图来处理手指触控操作。 项(item)是 UIBarButtonItem 的实例,可以具有以下 3 种样式: ● UIBarButtonItemStylePlain ● UIBarButtonItemStyleBordered ● UIBarButtonItemStyleDone 它们与普通按钮相似,不同之处在于它们额外添加了一些供导航使用的功能。下面列出 4 种 初始化方法: ● initWithBarButtonSystemItem:target:action: ● initWithCustomView: ● initWithImage:style:target:action: ● initWithTitle:style:target:action: 1.5 一个简单的工具栏 在此应用程序中,一幅图像将位于显示屏中心。用户将通过点击几个角度工具栏项来变换图 像角度。图像将根据工具栏项的角度值而旋转,如图 1-4 所示。 图 1-4 iPhone & iPad 高级编程 18 1.5.1 开发步骤:一个简单的工具栏 要创建一个简单的工具栏应用程序,需执行以下步骤: (1) 启动 Xcode 软件,创建一个名为 SimpleToolbar-iPhone 的基于视图的 iPhone 应用程序。 附录 A 中列出了创建基于视图的应用程序的初始步骤,您可以从中了解相关信息。 (2) 为该项目添加一个 UIImageView 对象、一个 UIToolbar 对象和 4 个 UIBarButtonItem 对象。 ● 在 Xcode 的 Groups & Files 窗口左侧选择 Resources 选项。 ● 选择 Project | Add To Project。 ● 选择一幅图像并命名为 grandpa.png。选择像素约为 300×300 的图像,然后单击 Add 按钮 (如图 1-5 所示)。 ● 选中 Copy items into destination group's folder 选项,然后单击 Add 按钮(如图 1-6 所示)。 图 1-5 图 1-6 (3) 双击 SimpleToolbar_iPhoneViewController.xib 文件,启动 Interface Builder(如图 1-7 所示)。 图 1-7 第 1 章 导 航 19 (4) 从 Interface Builder Library(Tool | Library)中选择以下项目,并将其拖入 View 窗口。执行 此步骤后,界面如图 1-8 所示: ● 1 个大小为 260×260 的 UIImageView 对象。为此需要执行以下 3 个步骤: (1) 将 UIImageView 对象拖入 View 窗口,该对象将占满整个视图。 (2) 从主菜单中选择 Tool | Size Inspector 选项,在该检查器右上角的 Frame 下拉框之下,将看 到内容分别为 W:240 和 H:128 的两个输入框。 (3) 将视图大小改成 W:260 和 H:260,并使其居中。 ● 1 个 UIToolbar 对象,将其放在视图底部。 ● 3 个 UIBarButtonItems 对象,将它们放在工具栏上,然后选择 Tools | Attributes Inspector 选项,输入以下参数: ● 第一个按钮输入 Title:+45,Tag:0 ● 第二个按钮输入 Title:+180,Tag:1 ● 第三个按钮输入 Title:-180,Tag:2 ● 第四个按钮输入 Title:-45,Tag:3 ● 两个 Flexible Space Bar Button Item 组件,分别放在第一个按钮的左边和最后一个按钮的 右边(如图 1-9 所示)。 图 1-8 图 1-9 iPhone & iPad 高级编程 20 (5) 返回 Interface Builder Library 界面,单击界面顶部的 Classes,然后滚动选择 Simple- Toolbar_iPhoneViewController 类。在界面底部选择 Outlets 按钮,单击+按钮并添加以下 Outlet,如 图 1-10 所示: ● imageView(使用 UIImageView 类型,而非 id 类型) (6) 选择 Actions 按钮。单击+按钮并添加以下操作,如图 1-11 所示: ● rotateView 图 1-10 图 1-11 (7) 在 Interface Builder 的主菜单中选择 File | Write Class Files 选项,从第一个弹出窗口中选 择 Save 按钮,在下一个弹出窗口中选择 Merge 按钮。此时,添加过新内容的 SimpleToolbar_ iPhoneViewController.m 文件将显示在界面左边,而其原始模板则位于右边(如图 1-12 所示)。 ● 在右下角选择 Actions | Choose Left。 ● 选择 File | Save Merge,然后关闭窗口。 (8) 下一个窗口是 SimpleToolbar_iPhoneViewController.h 界面,添加过新内容的 SimpleToolbar_ iPhoneViewController.h 文件显示在界面左边,其原始模板显示在右边(如图 1-13 所示)。 ● 在右下角选择 Actions | Choose Left。 ● 选择 Find | Go to Next | Difference。 第 1 章 导 航 21 ● 在右下角选择 Actions | Choose Left。 ● 选择 File | Save Merge,然后关闭窗口。 图 1-12 图 1-13 iPhone & iPad 高级编程 22 (9) 至此您拥有了一个包含应用程序视图逻辑的Objective-C模板。在真正着手编写程序之前, 还需要在 Interface Builder 中完成另一项任务,即必须建立关联: ● 将 UIImageView 确定为 imageView。 为了建立关联,以便将 UIImageView 确定为 imageView,在按住 Ctrl 键的同时单击 File's Owner 图标,打开 File's Owner 检查器(如图 1-14 所示)。 (10) 查看 File's Owner 检查器的右边,在按住 Ctrl 键的同时将 imageView 的圆圈拖到 UIImageView imageView 上,当圆圈显示高亮状态时松开鼠标。圆圈填满则表明已经建立了关联。 (11) 查看 File's Owner 查看器的右边,在按住 Ctrl 键的同时将 rotateView 的圆圈分别拖到每 个 UIBarButtonItems 上(如图 1-15 所示)。选择 File | Save 选项,然后关闭 File's Owner 检查器。 图 1-14 图 1-15 下面开始设计逻辑。 简单工具栏的源程序清单 此应用程序直接使用自动生成的 SimpleToolbar_iPhoneAppDelegate.h 文件和 SimpleToolbar_ iPhoneAppDelegate.m 文件,并不对其进行修改。 修改 SimpleToolbar_iPhoneViewController.h 模板 在 Interface Builder 中已经声明了以下 Outlet: ● imageView 为了获取和设置该变量的值,必须定义其属性(如程序清单 1-19 所示)。 将 IBOutlet 移到属性声明中。 程序清单 1-19 完整的 SimpleToolbar_iPhoneViewController.h 文件(/Chapter1/SimpleToolbar- iPhone/Classes/SimpleToolbar_iPhoneViewController.h) #import @interface SimpleToolbar_iPhoneViewController : UIViewController { UIImageView *imageView; } @property (nonatomic, retain) IBOutlet UIImageView *imageView; - (IBAction)rotateView:(id)sender; @end 第 1 章 导 航 23 修改 SimpleToolbar_iPhoneViewController.m 模板 前面更新了头文件,定义了添加到模板的内容,下面开始修改 SimpleToolbar_iPhoneView- Controller.m 模板。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 1-20 所示)。 程序清单 1-20 添加@synthesize #import "SimpleToolbar_iPhoneViewController.h" @implementation SimpleToolbar_iPhoneViewController @synthesize imageView; 应用程序启动后,将加载和显示默认的 grandpa.png 图像(如程序清单 1-21 所示)。 程序清单 1-21 viewDidLoad 方法 #pragma mark - #pragma mark View lifecycle // Implement viewDidLoad to do additional setup after loading the view, // typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; [imageView setImage:[UIImage imageNamed:@"grandpa.jpg"]]; } 当点击工具栏项按钮时,将调用 rotateView 方法,根据项的标签值来确定旋转角度,如程序 清单 1-22 所示。 程序清单 1-22 rotateView 方法 #pragma mark - #pragma mark Action methods - (IBAction)rotateView:(id)sender { static CGFloat angle = 0.0; switch ([sender tag]) { case 0: angle += 45.0; break; case 1: angle += 180.0; break; case 2: angle -= 180.0; break; case 3: angle -= 45.0; break; default: break; } CGAffineTransform transform=CGAffineTransformMakeRotation(angle); [imageView setTransform:transform]; } iPhone & iPad 高级编程 24 就此完成了 SimpleToolbar_iPhoneViewController.m 文件。程序清单 1-23 列出了完整的实现 代码。 程序清单 1-23 完整的 SimpleToolbar_iPhoneViewController.m 文件/(Chapter1/SimpleToolbar- iPhone/Classes/SimpleToolbar-iPhoneViewController.h) #import "SimpleToolbar_iPhoneViewController.h" @implementation SimpleToolbar_iPhoneViewController @synthesize imageView; #pragma mark - #pragma mark View lifecycle // Implement viewDidLoad to do additional setup after loading the view, // typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; [imageView setImage:[UIImage imageNamed:@"grandpa.jpg"]]; } #pragma mark - #pragma mark Action methods - (IBAction)rotateView:(id)sender { static CGFloat angle = 0.0; switch ([sender tag]) { case 0: angle += 45.0; break; case 1: angle += 180.0; break; case 2: angle -= 180.0; break; case 3: angle -= 45.0; break; default: break; } CGAffineTransform transform=CGAffineTransformMakeRotation(angle); [imageView setTransform:transform]; } #pragma mark - #pragma mark Memory methods - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setImageView:nil]; 第 1 章 导 航 25 } - (void)dealloc { [imageView release]; [super dealloc]; } @end 1.5.2 测试应用程序 各个步骤均已完成,现在在 Xcode 面板上选择 Simulator,然后单击 Run & Build 按钮测试应 用程序。此时,模拟器应显示“一个简单的工具栏”一节开头描述的结果。 1.6 选项卡栏 如果应用程序中有一个步骤有多个不同的操作,或是应用程序中的一个数据源有多个不同视 图,就可以使用选项卡栏。例如,天气预报应用程序可以提供当前气温视图、每小时平均气温视 图、未来五天气温预报视图和卫星图像视图。 选项卡栏为用户提供了不需通过下查操作来寻找结果,就能从不同视角查看数据的便捷方式。 1.6.1 UITabBarDelegate 协议 UITabBarDelegate 协议定义了 UITabBar 委托的方法,用于定制选项卡栏本身。 1.6.2 定制选项卡栏 要定制选项卡栏(包括添加、删除和重排选项卡栏上的项),可使用以下 5 种方法(其中的 tabBar:didSelectItem 方法是必需的): ● tabBar:willBeginCustomizingItems: ● tabBar:didBeginCustomizingItems: ● tabBar:willEndCustomizingItems:changed: ● tabBar:didEndCustomizingItems:changed: ● tabBar:didSelectItem: 从编程角度讲,可以通过 beginCustomizingItems 方法定制选项卡栏。调用这个方法可以创建 一个包含多个项和一个 Done 按钮的模态视图,当需要关闭该模态视图时单击 Done 按钮即可。 1.7 一个简单的选项卡栏 此应用程序用于模拟银行账户交易,包括汇总功能。下面的每种选择都被表示为选项卡栏上 的一个按钮: ● 显示当前余额,如图 1-16 所示。 ● 支持的交易(存款和取款),如图 1-17 所示。 ● 显示交易汇总信息,如图 1-18 所示。 iPhone & iPad 高级编程 26 图 1-16 图 1-17 图 1-18 1.7.1 开发步骤:一个简单的选项卡栏 创建该应用程序需要执行以下步骤: (1) 启动 Xcode,创建一个名为 SimpleTabbar-iPhone 的基于视图的 iPhone 应用程序。附录 A 列出了创建基于视图的应用程序的初始步骤,请参阅附录 A 了解相关信息。 (2) 选择 File | New File…选项,选择 Ojective-C 类作为 UIViewController 的子类,并将其命名 为 SecondViewController(如图 1-19 所示),在此过程中不选中任何选项。 图 1-19 (3) 选择 File | New File…选项,选择 Objective-C 类作为 UIViewController 类的子类,并将其 命名为 ThirdViewController,然后选中以下两个选项: ● UITableViewController subclass ● With XIB for user interface 第 1 章 导 航 27 (4) 选择 File | New File…选项,选择 Objective-C 类作为 NSObject 的子类,并将其命名为 Transaction。 (5) 选择 File | New File…选项,选择 Objective-C 类作为 NSObject 的子类,并将其命名为 PropertyList。 (6) 双击 MainWindow.xib 文件启动 Interface Builder。注意 View Mode 在浏览器模式中,此时 选择 Tab Bar Controller(如图 1-20 所示)。 图 1-20 (7) 在 Interface Builder Library(Tools | Attributes Inspector)中,在 View Controllers 区域中执行 以下操作(如图 1-21 所示): ● 双击 First,将其替换为 Balance。 ● 双击 Second,将其替换为 Transaction。 ● 单击+按钮以便添加项,输入 Summary 作为 Title,输入 TableViewController 作为 Class。 (8) 查看 MainWindow.xib 窗口,在 Tab Bar Controller 区域下方执行下列操作(如图 1-22 所示): ● 单击第 3 项 TableViewController(Summary);在 Attributes Inspector 的 NIB Name 下拉框中 选择 ThirdViewController。 (9) 在主菜单中选择 Tools | Identity Inspector,选择 ThirdViewController 作为类标识。 (10) 在 MainWindow.xib 窗口的 Tab Bar Controller 项之下选择 View Controller(Transaction), 它正位于前面选择的 ThirdViewController 之上。在 Identity Inspector 中选择 SecondViewController 作 为类标识。 (11) 双击 MainWindow.xib 窗口中的 Tab Bar Controller 项,将看到如图 1-23 所示的 Tab View Controller 窗口。 iPhone & iPad 高级编程 28 图 1-21 图 1-22 图 1-23 (12) 单击第 3 个按钮 Summary,然后单击 Tools | Identity Inspector 选项,从 Class Identity 中 选择 ThirdViewController。最后选择 File | Save。 1.7.2 设计视图控制器 在本节中,选项卡栏中的每个按钮都与各自的视图控制器相关联。其中,每个视图控制器都 有自己的一套实现步骤。 1. 第一个视图控制器 第一个视图控制器用于显示银行账户的当前余额。为了创建此视图,需要执行以下步骤: (1) 返回 Xcode 界面,在 Groups & Files 窗口中双击 FirstView.xib,在打开的窗口中已经有一 些标签了。选择并删除这些标签。 (2) 在 Interface Builder 中(选择 Tools | Identity Inspector)选择 File's Owner,然后选择 FirstViewController 作为类。 ● 在视图中央添加两个相邻的 UILabels 控件。双击位置最靠左的标签,输入 Balance(如图 1-24 所示)。 (3) 在 Interface Builder Library 中单击顶部的 Classes 选项,然后滚动选择 FirstViewController 类。在底部选择 Outlets 按钮。单击+按钮,添加如下 Outlet(如图 1-25 所示): ● balanceLabel(使用 UILabel 类型,而不是 id 类型) (4) 在 Interface Builder 主菜单中选择 File | Write Class Files 选项,首先选择 Save,然后选择 第 1 章 导 航 29 Merge。关闭 FirstViewController.m 窗口,不做任何更改。 图 1-24 图 1-25 此时,屏幕左边显示添加了新内容的 FirstViewController.h 文件,而原始模板显示在右边,如 图 1-26 所示。 图 1-26 iPhone & iPad 高级编程 30 ● 在界面右下角选择 Actions | Choose Left。 ● 选择 File | Save Merge,然后关闭窗口。 (5) 至此已经拥有了一个包含应用程序视图逻辑的 Objective-C 模板。在真正着手编程之前,还 需要在 Interface Builder 程序中完成一项任务,即必须建立所有关联: ● 将 UILabel 确定为 balanceLabel。 (6) 为将 UILabel 确定为 balanceLabel,在按住 Ctrl 键的同时单击 File's Owner 图标打开 File's Owner 检查器(如图 1-27 所示)。 (7) 在 File's Owner 检查器的右边,在按住 Ctrl 键的同时将 balanceLabel 的圆圈拖到 UILabel balanceLabel 上,当圆圈高亮显示时松开鼠标。圆圈填满表明已经建立了关联(如图 1-28 所示)。 选择 File | Save 选项,然后关闭 File's Owner 检查器。 图 1-27 图 1-28 2. 第二个视图控制器 第二个视图控制器是交易视图,在此输入存取款数目。要创建此视图,需执行以下步骤: (1) 返回 Xcode 界面,在 Groups & Files 窗口中双击 SecondView.xib,在打开的相应窗口中已 有一些标签。选择并删除这些标签。 (2) 在 Interface Builder 中(选择 Tools | Identity Inspector)选择 File's Owner 选项,然后选择 SecondViewController 作为类。 ● 在视图顶部添加一个 UISegmentedControl。双击左边部分并输入 Deposit,然后双击右边 部分并输入 Withdrawal。 ● 在视图中间添加一个 UILabels 控件,在该控件的右边添加一个 UITextField 控件。双击 UILabels 控件然后输入 Amount。 ● 在标签和文本框字段的下边添加一个 UIButton。双击该按钮并输入 Save(如图 1-29 所示)。 (3) 在 Interface Builder Library 中单击顶部的 Classes 选项,滚动选择 FirstViewController 类。 在界面底部选择 Outlets 按钮。单击+按钮,添加如下 Outlet(如图 1-30 所示): ● amountTextField(使用 UITextField 类型,而不是 id 类型) ● transactionType(使用 UISegmentedControl 类型,而不是 id 类型) (4) 选择 Action 按钮。单击+按钮,然后添加如下操作(如图 1-31 所示): ● saveTransaction 第 1 章 导 航 31 图 1-29 图 1-30 图 1-31 (5) 在 Interface Builder 主菜单中选择 File | Write Class Files 选项,此后选择 Save,然后选择 Merge。在 SecondViewController.h 文件界面中,包含新添加内容的文件位于左边,原始模板位于 右边(如图 1-32 所示)。 ● 在界面右下角选择 Actions | Choose Left。 ● 选择 Find | Go to Next | Difference。 ● 在界面右下角选择 Actions | Choose Left。 ● 选择 File | Save Merge,然后关闭窗口。 (6) 从 Interface Builder 程序主菜单中选择 File | Write Class Files 选项,此后选择 Save,然后 选择 Merge。在 SecondViewController.m 文件界面中,包含新添加内容的文件位于左边,原始模板 位于右边(如图 1-33 所示)。 ● 在界面右下角选择 Actions | Choose Left。 ● 选择 File | Save Merge,关闭窗口。 (7) 至此已经拥有了包含应用程序视图逻辑的 Objective-C 模板。在真正着手编程之前,还需 要在 Interface Builder 中完成一项任务,即建立所有关联: ● 将 UITextField 确定为 amountTextField。 ● 将 UISegmentedControl 确定为 transactionType。 iPhone & iPad 高级编程 32 图 1-32 图 1-33 第 1 章 导 航 33 (8) 为了建立 UITextField 和 amountTextField 之间的关联,在按住 Ctrl 键的同时单击 File's Owner 图标打开 File's Owner 检查器(如图 1-34 所示)。 (9) 在 File's Owner 检查器的右边,在按住 Ctrl 键的同时将 amountTextField 的圆圈拖到 UITextField amountTextField 上,当圆圈高亮显示时松开鼠标。圆圈填满则表明已经建立了关联。 (10) 在 File's Owner 检查器的右边,在按住 Ctrl 键的同时将 transactionType 的圆圈拖到 UISegmentedControl transactionType 上,当圆圈高亮显示时松开鼠标。 (11) 查看 File's Owner 检查器的右边,在按住 Ctrl 键的同时将 saveTransaction 的圆圈拖到 Save 按钮上,当按钮高亮显示时松开鼠标,然后选择 Touch Up Inside(如图 1-35 所示)。选择 File | Save 选项,关闭 File's Owner 检查器。 图 1-34 图 1-35 3. 第三个视图控制器 第三个视图控制器显示已在应用程序中输入的所有交易。交易量分为存款记录和取款记录两 部分。 (1) 返回 Xcode 的 Groups & Files 窗口,双击 ThirdViewController.xib 文件打开其窗口。 (2) 从 Interface Builder(选择 Tools | Identity Inspector)中选择 File's Owner 选项,然后选择 ThirdViewController 作为类。 (3) 从Interface Builder(选择Tools | Attributes Inspector)中选择表格视图,然后在Simulated User Interface Elements 区域中的 Bottom Bar 设为 Tab Bar(如图 1-36 所示)。 (4) 在 Interface Builder Library 顶部单击 Classes,然后滚动选择 ThirdViewController 类。在其 底部选择 Outlets 按钮,单击+按钮并添加如下 Outlet(如图 1-37 所示): ● detailTableView(使用 UITableView 类型,而不是 id 类型) (5) 在 Interface Builder 的主菜单中选择 File | Write Class Files,此后选择 Save,然后选择 Merge。关闭 ThirdViewController.m 窗口,不做任何更改。在 ThirdViewController.h 文件界面中, 包含新添内容的文件位于左边,原始模板位于右边(如图 1-38 所示)。 ● 在界面右下角选择 Actions | Choose Left。 ● 选择 File | Save Merge,然后关闭窗口。 iPhone & iPad 高级编程 34 图 1-36 图 1-37 图 1-38 第 1 章 导 航 35 (6) 至此已经拥有了包含应用程序视图逻辑的 Objective-C 模板。在真正着手编辑之前,还需 要在 Interface Builder 中完成一项任务,即确定所有关联: ● 将 UITableView 确定为 detailTableView (7) 为了建立 UITableView 和 detailTableView 之间的关联,单击 File's Owner,然后选择 ThirdViewController 作为 Class Identity。在按住 Ctrl 键的同时单击 File's Owner 图标,打开 File's Owner 检查器(如图 1-39 所示)。 (8) 在 File's Owner 检查器的右边,在按住 Ctrl 键的同时将 detailTableView 的圆圈拖到 UITableView detailTableView 上,当圆圈高亮显示时松开鼠标。圆圈填满则表明已经建立了关联(如 图 1-40 所示)。选择 File | Save 选项,然后关闭 File's Owner 检查器。 图 1-39 图 1-40 用户界面设计部分已经完成。下一节将列出提供应用程序逻辑的源代码。 4. 简单选项卡栏应用程序的源代码清单 此应用程序直接使用 SimpleTabbar_iPhoneAppDelegate.h 文件和 SimpleTabbar_iPhoneApp- Delegate.m 文件,并不对其进行修改。 修改 FirstViewController.h 模板 在 Interface Builder 中已声明了以下 Outlet: ● balanceLabel 为了获取和设置该变量的值,必须定义其属性(如程序清单 1-24 所示)。 将 IBOutlet 移到属性声明中。 程序清单 1-24 完整的 FirstViewController.h 文件(Chapter1/Tabbar-iPhone/Classes/FirstView Controller.h) #import #import "PropertyList.h" @interface FirstViewController : UIViewController { UILabel *balanceLabel; } @property (nonatomic, retain) IBOutlet UILabel *balanceLabel; @end 修改 FirstViewController.m 模板 前面更新了头文件,定义了添加到该模板的内容,下面开始修改 FirstViewController.m 模板。 必须为每个已声明的视图属性添加相应的@synthesize(如程序清单 1-25 所示)。 iPhone & iPad 高级编程 36 程序清单 1-25 添加@synthesize #import "FirstViewController.h" @implementation FirstViewController @synthesize balanceLabel; 应用程序启动后,已经发生的任何交易都将被存储在一个已经加载的 Data.plist 文件中;同时 显示当前余额(如程序清单 1-26 所示)。 程序清单 1-26 viewWillAppear 方法 # pragma mark - # pragma mark Initialization routines - (void)viewWillAppear:(BOOL)animated { NSDictionary *d = [PropertyList readFromArchive:@"Data"]; if(d != nil) { NSNumber *bal = [d objectForKey:@"balance"]; if(bal != nil) { [balanceLabel setText:[bal stringValue]]; } else { [balanceLabel setText:@"0"]; } } else { [balanceLabel setText:@"0"]; } [super viewWillAppear:animated]; } 程序清单 1-27 显示了 FirstViewController.m 的完整实现代码。 程序清单 1-27 完整的 FirstViewController.m 文件(Chapter1/Tabbar-iPhone/Classes/FirstView Controller.m) #import "FirstViewController.h" @implementation FirstViewController @synthesize balanceLabel; # pragma mark - # pragma mark Initialization routines - (void)viewWillAppear:(BOOL)animated { NSDictionary *d = [PropertyList readFromArchive:@"Data"]; if(d != nil) { NSNumber *bal = [d objectForKey:@"balance"]; if(bal != nil) { [balanceLabel setText:[bal stringValue]]; } else { [balanceLabel setText:@"0"]; } } else { 第 1 章 导 航 37 [balanceLabel setText:@"0"]; } [super viewWillAppear:animated]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setBalanceLabel:nil]; } - (void)dealloc { [balanceLabel release]; [super dealloc]; } @end 修改 SecondViewController.h 模板 在 Interface Builder 中已经声明了如下 Outlet: ● amountTextField ● transactionType 为了能够获取和设置该变量的值,必须为其定义属性(如程序清单 1-28 所示)。 需要将 IBOutlet 移到属性声明中。 还需要添加以下 3 个变量: ● balance ● deposits ● withdrawals 程序清单 1-28 完整的 SecondViewController.h 文件(Chapter1/Tabbar-iPhone/Classes/Second ViewController.h) #import @interface SecondViewController : UIViewController { UITextField *amountTextField; UISegmentedControl *transactionType; NSNumber *balance; NSArray *deposits; NSArray *withdrawals; } @property (nonatomic, retain) IBOutlet UITextField *amountTextField; @property (nonatomic, retain) IBOutlet UISegmentedControl *transactionType; @property (nonatomic, retain) NSNumber *balance; @property (nonatomic, retain) NSArray *deposits; @property (nonatomic, retain) NSArray *withdrawals; - (IBAction)saveTransaction:(id)sender; @end iPhone & iPad 高级编程 38 修改 SecondViewController.m 模板 前面更新了头文件,定义了添加到该模板的内容,下面开始修改 SecondViewController.m 模板。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 1-29 所示)。 程序清单 1-29 添加@synthesize 语法 #import "SecondViewController.h" #import "PropertyList.h" @implementation SecondViewController @synthesize amountTextField; @synthesize transactionType; @synthesize balance; @synthesize deposits; @synthesize withdrawals; 应用程序启动后,已经发生的任何交易都被存储在一个已经加载的 Data.plist 文件中;同时保 留余额、存款和取款值(如程序清单 1-30 所示)。 程序清单 1-30 viewWillAppear 方法 # pragma mark - # pragma mark Initialization routines - (void)viewWillAppear:(BOOL)animated { [amountTextField setDelegate:self]; NSDictionary *d = [PropertyList readFromArchive:@"Data"]; if(d != nil) { NSDictionary *localItems = [d objectForKey:@"items"]; [self setBalance:[d objectForKey:@"balance"]]; [self setDeposits:[localItems objectForKey:@"deposits"]]; [self setWithdrawals:[localItems objectForKey:@"withdrawals"]]; } else { [self setBalance:[NSNumber numberWithDouble:0.0]]; [self setDeposits:[NSArray array]]; [self setWithdrawals:[NSArray array]]; } [super viewWillAppear:animated]; } 当用户点击 Save 按钮时,根据用户在分段控件上的选择(存款或取款),会增加或减少输入的 值(如程序清单 1-31 所示)。 程序清单 1-31 saveTransaction 方法 - (IBAction)saveTransaction:(id)sender { NSMutableDictionary *aDict = [NSMutableDictionary dictionary]; NSMutableDictionary *itemDict = [NSMutableDictionary dictionary]; NSMutableArray *transaction = nil; NSNumberFormatter * formatter = [[NSNumberFormatter alloc] init]; [formatter setNumberStyle:NSNumberFormatterCurrencyStyle]; NSString *amt = [amountTextField text]; 第 1 章 导 航 39 double localAmount = [amt doubleValue]; double localBalance = [[self balance] doubleValue]; [formatter release]; if ([transactionType selectedSegmentIndex] == 0) { localBalance += localAmount; transaction = [[self deposits] mutableCopy]; [transaction addObject:[NSNumber numberWithDouble:localAmount]]; [self setDeposits:transaction]; [self setBalance:[NSNumber numberWithDouble:localBalance]]; } else { localBalance -= localAmount; transaction = [[self withdrawals] mutableCopy]; [transaction addObject:[NSNumber numberWithDouble:localAmount]]; [self setWithdrawals:transaction]; [self setBalance:[NSNumber numberWithDouble:localBalance]]; } [aDict setObject:[self balance] forKey:@ "balance"]; [itemDict setObject:[self deposits] forKey:@ "deposits"]; [itemDict setObject:[self withdrawals] forKey:@ "withdrawals"]; [aDict setObject:itemDict forKey:@ "items"]; if(![PropertyList writeToArchive:@ "Data" fromDictionary:aDict]) { NSLog(@"Error writing data to pList."); } } 当用户点击 iPhone键盘上的 Return按钮时,UITextFieldDelegate必须实现 textFieldShould-Return 方法,键盘会随之消失(如程序清单 1-32 所示)。 程序清单 1-32 textFieldShouldReturn 方法 #pragma mark - #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { // the user pressed the "Done" button, so dismiss the keyboard [textField resignFirstResponder]; return YES; } 程序清单 1-33 显示了 SecondViewController.m 的完整实现代码。 程序清单 1-33 完整的 SecondViewController.m 文件(Chapter1/Tabbar-iPhone/Classes/Second ViewController.m) #import "FirstViewController.h" @implementation FirstViewController @synthesize balanceLabel; # pragma mark - # pragma mark Initialization routines - (void)viewWillAppear:(BOOL)animated { NSDictionary *d = [PropertyList readFromArchive:@"Data"]; if(d != nil) { iPhone & iPad 高级编程 40 NSNumber *bal = [d objectForKey:@"balance"]; if(bal != nil) { [balanceLabel setText:[bal stringValue]]; } else { [balanceLabel setText:@"0"]; } } else { [balanceLabel setText:@"0"]; } [super viewWillAppear:animated]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; [self setBalanceLabel:nil]; } - (void)dealloc { [balanceLabel release]; [super dealloc]; } @end 修改 ThirdViewController.h 模板 在 Interface Builder 中已经声明了如下 Outlet: ● detailTableView 为了能够获取和设置该变量的值,必须为其定义属性(如程序清单 1-34 所示)。 将 IBOutlet 移到属性声明中。 还需添加如下 3 个变量: ● balance ● deposits ● withdrawals 程序清单 1-34 完整的 ThirdViewController.h 文件(Chapter1/Tabbar-iPhone/Classes/ThirdView Controller.h) #import @interface ThirdViewController : UITableViewController { UITableView *detailTableView; NSNumber *balance; NSArray *deposits; NSArray *withdrawals; } @property (nonatomic, retain) IBOutlet UITableView *detailTableView; @property (nonatomic, retain) NSNumber *balance; 第 1 章 导 航 41 @property (nonatomic, retain) NSArray *deposits; @property (nonatomic, retain) NSArray *withdrawals; @end 修改 ThirdViewController.m 模板 前面更新了头文件,定义了要添加到模板的内容,下面开始修改 ThirdViewController.m 模板。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 1-35 所示)。 程序清单 1-35 添加@synthesize #import "ThirdViewController.h" #import "PropertyList.h" @implementation ThirdViewController @synthesize detailTableView; @synthesize balance; @synthesize deposits; @synthesize withdrawals; 启动应用程序后,已经发生的所有交易都将存储在一个已经加载的 Data.plist 文件中;同时保 留余额、存款和取款值。保留了这些存储值后,会调用表格视图重载数据,即刷新表格视图并显 示已存储的数据(如程序清单 1-36 所示)。 程序清单 1-36 viewWillAppear 方法 - (void)viewWillAppear:(BOOL)animated { NSDictionary *d = [PropertyList readFromArchive:@ "Data"]; if(d != nil) { NSDictionary *localItems = [d objectForKey:@ "items"]; [self setBalance:[d objectForKey:@ "balance"]]; [self setDeposits:[localItems objectForKey:@ "deposits"]]; [self setWithdrawals:[localItems objectForKey:@ "withdrawals"]]; } else { [self setBalance:[NSNumber numberWithDouble:0.0]]; [self setDeposits:[NSArray array]]; [self setWithdrawals:[NSArray array]]; } [[self detailTableView] reloadData]; [super viewWillAppear:animated]; } 如程序清单 1-37 所示,用于填充表格视图的数据必须为表格提供以下两个值: ● 表格视图分成的区域数 ● 每个区域的行数 程序清单 1-37 numberOfSectionInTableView 方法和 tableView:numberOfRowsInSection 方法 #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; iPhone & iPad 高级编程 42 } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. switch (section) { case 0: return [[self deposits] count]; break; case 1: return [[self withdrawals] count]; break; default: return 0; break; } } 本应用程序分为两个区域,第一个区域显示存款信息,第二个区域显示取款信息。每个区域 都有各自的标题,以便使用户了解内容如程序清单 1-38 所示。 程序清单 1-38 tableView:titleForHeaderInSection 方法 // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { switch (section) { case 0: if([[self deposits] count] > 0) { return @"Deposits"; } else { return @"No Deposits"; } break; case 1: if([[self withdrawals] count] > 0) { return @"Withdrawals"; } else { return @"No Withdrawals"; } break; default: return @""; break; } } 为了显示每个明细行,需要检查表格视图的区域和行。区域确定要使用的数组,如存款或取 款;行确定所选数组中要检索的元素。因为此表格视图只提供了显示功能,因此不需要点击表格 视图单元格。 程序清单 1-39 显示了 tableView:cellForRowAtIndexPath 方法。 程序清单 1-39 tableView:cellForRowAtIndexPath 方法 // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 第 1 章 导 航 43 static NSString *CellIdentifier = @"Cell"; NSString *cellText = @""; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; } // Configure the cell... switch ([indexPath section]) { case 0: cellText = [NSString stringWithFormat:@"Deposit #%d = %@", ([indexPath row]+1), [[self deposits] objectAtIndex:[indexPath row]]]; break; case 1: cellText = [NSString stringWithFormat:@"Withdrawal #%d = %@", ([indexPath row]+1), [[self withdrawals] objectAtIndex:[indexPath row]]]; break; default: break; } // Configure the cell. [[cell textLabel]setText: cellText]; return cell; } 程序清单 1-40 显示了 ThirdViewConroller.m 的完整实现代码。 程序清单 1-40 完整的 ThirdViewController.m 文件(Chapter1/Tabbar-iPhone/Classes/ThirdView Controller.m) #import "ThirdViewController.h" #import "PropertyList.h" @implementation ThirdViewController @synthesize detailTableView; @synthesize balance; @synthesize deposits; @synthesize withdrawals; #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; } - (void)viewWillAppear:(BOOL)animated { NSDictionary *d = [PropertyList readFromArchive:@"Data"]; if(d != nil) { NSDictionary *localItems = [d objectForKey:@"items"]; [self setBalance:[d objectForKey:@"balance"]]; iPhone & iPad 高级编程 44 [self setDeposits:[localItems objectForKey:@"deposits"]]; [self setWithdrawals:[localItems objectForKey:@"withdrawals"]]; } else { [self setBalance:[NSNumber numberWithDouble:0.0]]; [self setDeposits:[NSArray array]]; [self setWithdrawals:[NSArray array]]; } [[self detailTableView] reloadData]; [super viewWillAppear:animated]; } #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. switch (section) { case 0: return [[self deposits] count]; break; case 1: return [[self withdrawals] count]; break; default: return 0; break; } } // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { switch (section) { case 0: if([[self deposits] count] > 0) { return @"Deposits"; } else { return @"No Deposits"; } break; case 1: if([[self withdrawals] count] > 0) { return @"Withdrawals"; } else { return @"No Withdrawals"; } break; default: return @""; break; } } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 第 1 章 导 航 45 static NSString *CellIdentifier = @"Cell"; NSString *cellText = @""; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; } // Configure the cell... switch ([indexPath section]) { case 0: cellText = [NSString stringWithFormat:@"Deposit #%d = %@", ([indexPath row]+1), [[self deposits] objectAtIndex:[indexPath row]]]; break; case 1: cellText = [NSString stringWithFormat:@"Withdrawal #%d = %@", ([indexPath row]+1), [[self withdrawals] objectAtIndex:[indexPath row]]]; break; default: break; } // Configure the cell. [[cell textLabel]setText: cellText]; return cell; } #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { } #pragma mark - #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { // Relinquish ownership of anything that can be recreated in // viewDidLoad or on demand. // For example: self.myOutlet = nil; [self setBalance:nil]; [self setDeposits:nil]; [self setWithdrawals:nil]; } - (void)dealloc { [detailTableView release]; [balance release]; [deposits release]; [withdrawals release]; iPhone & iPad 高级编程 46 [super dealloc]; } @end 修改 Transaction.h 模板 Transaction 类将余额、存款和取款数组存储在 NSDictionary(items)中。为了存储为 plist 类型 的文件,必须实现 NSCoding 协议。需要存储的两个值是余额项和交易项。 ● balance ● items 为了获取和设置这些变量的值,必须为其定义属性(如程序清单 1-41 所示)。 程序清单 1-41 完整的 Transaction.h 文件(Chapter1/Tabbar-iPhone/Classes/Transaction.h) #import @interface Transaction : NSObject { NSNumber *balance; NSDictionary *items; } @property (nonatomic, retain) NSNumber *balance; @property (nonatomic, retain) NSDictionary *items; @end 修改 Transaction.m 模板 前面更新了头文件,定义了要添加到该模板的内容,下面开始修改 Transaction.m 模板。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 1-42 所示)。 程序清单 1-42 添加@synthesize #import "Transaction.h" @implementation Transaction @synthesize balance; @synthesize items; encodeWithCoder 方法定义了数据存储顺序,在从文件检索到值后,相关的 initWithCoder 方法对值进行解码,并用存储的值初始化对象(如程序清单 1-43 所示)。 程序清单 1-43 encodeWithCoder 和 initWithCoder 方法 #pragma mark - #pragma mark NSCoder methods - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self balance] forKey:@"balance"]; [coder encodeObject:[self items] forKey:@"items"]; } - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setBalance:[coder decodeObjectForKey:@"balance"]]; 第 1 章 导 航 47 [self setItems:[coder decodeObjectForKey:@"items"]]; } return self; } 程序清单 1-44 显示了 Transaction.m 的完整实现代码。 程序清单 1-44 完整的 Transaction.m 文件(Chapter1/Tabbar-iPhone/Classes/Transaction.m) #import "Transaction.h" @implementation Transaction @synthesize balance; @synthesize items; #pragma mark - #pragma mark NSCoder methods - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self balance] forKey:@"balance"]; [coder encodeObject:[self items] forKey:@"items"]; } - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setBalance:[coder decodeObjectForKey:@"balance"]]; [self setItems:[coder decodeObjectForKey:@"items"]]; } return self; } @end 修改 PropertyList.h 模板 PropertyList 类提供了工厂方法(factory method),并使用这些方法检索 Data.plist 文件和存储每 次交易后的最新值。PropertyList 类使用一个变量存储数据: ● pList 为了能够获取和设置该变量的值,必须为其定义属性(如程序清单 1-45 所示)。 PropertyList 类还提供了以下两个用于数据处理的工厂方法: ● readFromArchive ● writeToArchive 程序清单 1-45 完整的 PropertyList.h 文件(Chapter1/Tabbar-iPhone/Classes/PropertyList.h) #import @interface PropertyList : NSObject { NSDictionary *pList; } @property (nonatomic, retain) NSDictionary *pList; + (NSDictionary *)readFromArchive:(NSString *)aFileName; + (BOOL)writeToArchive:(NSString *)aFileName fromDictionary:(NSDictionary *)aDict; @end iPhone & iPad 高级编程 48 修改 PropertyList.m 模板 前面已经更新了头文件,定义了添加到该模板的内容,下面开始修改 PropertyList.m 模板。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 1-46 所示)。 程序清单 1-46 添加@synthesize #import "PropertyList.h" @implementation PropertyList @synthesize pList; 程序清单 1-47 显示了 PropertyList.m 的完整实现代码。 程序清单 1-47 完整的 PropertyList.m 文件(Chapter1/Tabbar-iPhone/Classes/PropertyList.m) #import "PropertyList.h" @implementation PropertyList @synthesize pList; + (NSDictionary *)readFromArchive:(NSString *)aFileName { NSDictionary *result = nil; NSString *fname = [NSString stringWithFormat:@"%@.plist", aFileName]; NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *bundlePath = [rootPath stringByAppendingPathComponent:fname]; NSData *data = [NSData dataWithContentsOfFile:bundlePath]; if(data != nil) { result = [NSKeyedUnarchiver unarchiveObjectWithData:data]; } return result; } + (BOOL)writeToArchive:(NSString *)aFileName fromDictionary:(NSDictionary *)aDict { NSString *fname = [NSString stringWithFormat:@"%@.plist", aFileName]; NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *bundlePath = [rootPath stringByAppendingPathComponent:fname]; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:aDict]; return [data writeToFile:bundlePath atomically:YES]; } - (void)dealloc { [pList release]; [super dealloc]; } @end 第 1 章 导 航 49 1.7.3 测试应用程序 在完成上述步骤后,在 Xcode 面板上选择 Simulator,然后单击 Run & Build 按钮测试应用程 序。此时,模拟器应显示“一个简单的选项卡栏”一节开头描述的结果。 1.8 小结 本章介绍了三种 iPhone 数据导航技术。使用导航栏,用户只需执行点击操作就可以遍历数据 层次结构(访问更深层次的数据或返回上层数据)。使用工具栏,用户可以在当前上下文中的相同 数据上执行多个不同的任务。使用选项卡栏则基于共有数据,并通过多个不同视图来处理这些数据。 总之,要根据数据的表示方式和/或修改方式,来确定在应用程序中使用哪种导航视图。 定制表格视图 本章内容: ● 在 Interface Builder 中设计定制表格视图单元格的方法 ● 将定制表格视图单元格合并到标准表格视图的方法 ● 将属性列表文件加载到定制表格视图的方法 如何在移动设备上高效、合理、清晰地表示数据是一件十分棘手的事情。在移动设备中最常见 的显示方式是表格视图,它通过多行单列列表显示数据。iOS 4 提供了标准的表格视图,包括普通 (plain)、索引(indexed)和分组(grouped)三种类型。 但是,有时候用户可能希望使外观不同于苹果公司提供的表格视图单元格。本章将讲述如何定 制自己的表格视图单元格,从而实现具有定制外观的表格视图应用程序。 3.1 表格视图 简单来讲,表格视图使用列表形式显示数据。可以通过以下两种方法来确定如何在一个表格视 图中显示数据: ● numberOfSectionsInTableView——确定存在的区块数目。对于普通列表而言,只存在一个区 块。 ● tableView:numberOfRowsInSection:——告诉表格视图每个区块中的行数。 tableView:cellForRowAtIndexPath:方法用于将信息放入每个表格视图单元格中,其中的 indexPath 包含区块([indexPath section])和行值([indexPath row])。 当用户选择一个特定的表格视图单元格时,tableView:didSelectRowAtIndexPath:方法将处理该选 择操作。 如果应用程序需要支持从表格视图中删除元素的功能,必须提供 tableView: commitEditingStyle:方 法。请注意,还必须支持从存储器元素中删除数据元素的功能;举个例子来说,将数据从表格视图 中删除时,并不能自动将其从一个数组中删除。 3 第 章 iPhone & iPad 高级编程 116 如果要在区块中提供表头,应用程序需要支持 tableView:titleForHeaderInSection:方法。 如果要在区块中提供表尾,应用程序需要支持 tableView:titleForFooterInSection:方法。 3.1.1 表格视图单元格 表格视图由相互独立的表格视图单元格组成,每个单元格中包含将要显示的信息,也是用户在 视图中看到的信息。为了设计定制的表格视图单元格,Interface Builder 程序提供了一个 UITableViewCell 实例。可以像操作常规视图一样从 Interface Builder Library 中拖动 UI 元素、标签和 图像视图。不过,因为表格视图单元格空间有限,所以不要在其中放入太多的项。 3.1.2 UITableViewDataSource 协议 采用 UITableViewDataSource 协议的类为表格视图提供在表格视图单元格中显示的数据。该协 议负责告知表格视图行数和区块数,还负责处理单元格的插入和删除。 3.1.3 UITableViewDelegate 协议 采用 UITableViewDelegate 协议的类为表格视图提供多个方法,来管理表格视图单元格的显示方 式,并配置区块中的表头和表尾。当用户点击表格视图单元格时,它们还负责处理所发生的操作 事件。 下节将描述创建一个表格视图应用程序的步骤,并介绍在此应用程序中创建和添加定制表格视 图单元格的步骤。 3.2 一个定制表格视图应用程序 此应用程序启动时,主视图是一个表格视图中显示的联系人列表,其表格视图单元格的外观是 定制的(如图 3-1 所示)。 图 3-1 此应用程序的开发工作包含以下两项主要任务。 第 3 章 定制表格视图 117 第一个任务是收集联系人信息。此应用程序使用属性列表编辑器创建了一个属性列表,用于存 储联系人信息。 第二个任务是设计一个定制表格视图单元格,能够在一个单元格中显示联系人信息。需要注意, 如果在设计时改变了表格视图单元格的高度,还需要在视图控制器中设置表格视图的高度。 下节将描述开发使用定制表格视图单元格的表格视图应用程序的步骤。 3.2.1 开发步骤:一个定制表格视图应用程序 为创建此应用程序,需要执行以下步骤: (1) 启动 Xcode,创建一个基于导航的 iPhone 应用程序,命名为 CustomTableViewApp。附录 A 中列出了创建基于导航的应用程序的初始步骤,您可从中了解有关该步骤的信息。 (2) 在 Xcode 的 Groups&Files 区域中单击 Classes 分组。选择 File | New File,然后选择 Cocoa Touch Class,最后选择 Objective-C 类作为 UITableViewCell 的子类并将文件命名为 CustomTable- ViewCell。这就是将要显示的定制表格视图单元格,它取代了默认表格视图单元格。 (3) 选择 File | New File,在 iPhone OS 下选择 Empty XIB and iPhone(for the product)。将文件命 名为 CustomTableViewCell。 (4) 选择 File | New File,选择 Objective-C 作为 NSObject 的一个子类,并将其命名为 PropertyList。 (5) 在 NIB Files 分组中双击 CustomTableViewCell.xib 文件来启动 Interface Builder。需要注意, 此时只有一个 File's Owner 图标和一个 First Responder 图标。因此,需要自己添加定制表格视图单元 格。 (6) 从主菜单中选择 Tools | Library,然后选择 Objects 选项卡。选择 Table View Cell 并将其拖入 包含 File's Owner 图标的窗口中,最后双击该图标使得表格视图单元格可视。 (7) 选择 Tools | Identity Inspector,然后选择 Class CustomTableViewCell。 (8) 选择 Tools | Size Inspector,然后设置 H:65。 (9) 从主菜单中选择 Tools | Library,然后选择 Objects 选项卡。 (10) 选择一个 UILabel 对象,并将其拖到主视图中,放在左上方,然后松开鼠标。 (11) 选择 Tools | Size Inspector,然后设置 W:130。 (12) 选择 Tools | Attributes Inspector,然后设置字号为 25。 (13) 选择一个 UILabel 对象,并将其拖到主视图中,放在屏幕左下位置,然后松开鼠标。设置 字号为 13。 (14) 选择 Tools | Size Inspector,然后设置 W:130。 (15) 选择 3 个 UILabel 对象,并将其拖到主视图中,将 3 个 UILabel 对象放在屏幕中间(纵向排 列),然后松开鼠标。设置字号为 13。 (16) 选择 Tools | Size Inspector,设置最上面和中间的 UILabel 为 W:130,设置最下面的 UILabel 为 W:60。 (17) 选择 Tools | Attributes Inspector,设置字号为 25。 (18) 选择一个 UILabel 对象,并将其拖到主视图,将 其放在主视图的右下方,然后松开鼠标。设置布局为右对 齐,字号为 13。选择 File | Save。 整个界面如图 3-2 所示。 图 3-2 iPhone & iPad 高级编程 118 (19) 选择 Tools | Library,然后选择界面顶部的 Classes 选项卡,滚动选择 CustomTableViewCell 类。在界面底部选择 Outlets 按钮。单击+号并添加如下 Outlet(如图 3-3 所示): ● cityLabel(使用 UILabel 类型替代 id 类型) ● firstNameLabel(使用 UILabel 类型替代 id 类型) ● lastNameLabel(使用 UILabel 类型替代 id 类型) ● stateLabel(使用 UILabel 类型替代 id 类型) ● streetLabel(使用 UILabel 类型替代 id 类型) ● zipLabel(使用 UILabel 类型替代 id 类型) (20) 在 Interface Builder 主菜单中选择 File | Write Class Files 选项,从第一个弹出对话框中选择 Save 按钮,在下一个弹出对话框中选择 Merge 按钮。此时,添加过新内容的 CustomTableViewCell.h 文件将显示在窗口左边,而其原始模板则位于右边(如图 3-4 所示)。 ● 在右下角选择 Actions | Choose Left。 ● 选择 File | Save Merge,然后关闭窗口。 图 3-3 图 3-4 (21) 不需要为 CustomTableViewCell.m 文件添加内容,关闭该文件。 到此为止,已完成一个包含应用程序逻辑的 Objective-C 模板。现在需要建立所有关联: ● 将 UILabel 确定为 cityLabel。 ● 将 UILabel 确定为 firstNameLabel。 ● 将 UILabel 确定为 lastNameLabel。 ● 将 UILabel 确定为 stateLabel。 第 3 章 定制表格视图 119 ● 将 UILabel 确定为 streetLabel。 ● 将 UILabel 确定为 zipLabel。 (22) 为了建立关联将UILabel确定为 cityLabel,在按住Ctrl键的同时单击Custom Table View Cell 图标,打开 Custom Table View Cell 检查器(如图 3-5 所示)。 (23) 查看 Custom Table View 检查器的右边,在按住 Ctrl 键的同时将 cityLabel 旁边的圆圈拖到 UILabel cityLabel 上,当圆圈高亮显示时松开鼠标。圆圈填满表明已经建立了关联。对步骤(21)中剩 余的字段重复本步骤建立关联(如图 3-6 所示)。最后,选择 File | Save。 (24) 在 NIB Files 分组中双击 RootViewController.xib 文件。 (25) 选择 Tools | Library,然后选择界面顶部的 Classes 选项,滚动选择 RootViewController 类。 在界面底部选择 Outlets 按钮。单击+号并添加以下 Outlet(如图 3-7 所示): ● customTableViewCell(使用 CustomTableViewCell 类型替代 id 类型) 图 3-5 图 3-6 图 3-7 (26) 在 Interface Builder 主菜单中选择 File | Write Class Files 选项,从第一个弹出对话框中选择 Save 按钮,在下一个弹出对话框中选择 Merge 按钮。此时,添加过新内容的 RootView-Controller.h 文件将显示在窗口左边,而其原始模板则位于右边(如图 3-8 所示)。 ● 在右下角选择 Actions | Choose Left。 ● 选择 File | Save Merge,然后关闭窗口。 iPhone & iPad 高级编程 120 图 3-8 (27) 不需要为 RootViewController.m 文件添加任何内容,关闭该文件。 (28) 返回 CustomTableViewCell.xib 文件,选择 Tools | Identity Inspector,然后选择 File's Owner 图标,最后选择 RootViewController 类。 (29) 在按住 Ctrl 键的同时将 File's Owner 图标拖到 Custom Table View 图标上,然后选择 customTableViewCell outlet 建立连接。选择 File | Save。 至此,用户界面所需要添加的元素和需要建立的连接已全部完成。下面开始设计程序逻辑。 定制表格视图应用程序的源程序清单 此应用程序直接使用自动生成的 CustomTableViewAppDelegate.h 文件和 CustomTableView- AppDelegate.m 文件,不对其进行任何修改。 修改 RootViewController.h 模板文件 对 RootViewController.h 文件只有少许改动:为了将 CustomTableViewCell 链接到表格视图中, 需要声明 CustomViewTableCell,同时还需要声明一个NSDictionary(contacts)读写存储器中的属性列表。 程序清单 3-1 显示了完整的 RootViewController.h 文件。 程序清单 3-1 完整的 RootViewController.h 文件(/Desktop/ForChapter3/CustomTableViewApp/Classes/ RootViewController.h) #import @class CustomTableViewCell; 第 3 章 定制表格视图 121 @interface RootViewController : UITableViewController { CustomTableViewCell *customTableViewCell; NSDictionary *contacts; } @property (nonatomic, retain) IBOutlet CustomTableViewCell *customTableViewCell; @property (nonatomic, retain) NSDictionary *contacts; @end 修改 RootViewController.m 模板文件 前面更新了头文件,定义了添加到模板的内容,下面开始修改 RootViewController.m 模板。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 3-2 所示)。 程序清单 3-2 添加@synthesize #import "RootViewController.h" #import "CustomTableViewCell.h" #import "PropertyList.h" #define DARK_BACKGROUND \ [UIColor colorWithRed:151.0/255.0 green:152.0/255.0 \ blue:155.0/255.0 alpha:1.0] #define LIGHT_BACKGROUND \ [UIColor colorWithRed:172.0/255.0 green:173.0/255.0 \ blue:175.0/255.0 alpha:1.0] @implementation RootViewController @synthesize contacts; @synthesize customTableViewCell; 在 viewDidLoad 方法中初始化 RootViewController 视图时,将会读取存储的联系人属性列表, 然后将读取的值存储到定制表格视图单元格中(如程序清单 3-3 所示)。 程序清单 3-3 viewDidLoad 方法 #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; NSDictionary *contactList = [PropertyList readFromArchive:@"Contacts"]; [self setContacts:[contactList objectForKey:@"contacts"]]; [self setTitle:@"Contacts"]; [[self tableView] setRowHeight:65.0]; [[self tableView] setBackgroundColor:DARK_BACKGROUND]; [[self tableView] setSeparatorStyle: UITableViewCellSeparatorStyleSingleLineEtched]; } 区块数量和每个区块中的行数决定了表格视图中数据值的布置方式。因为当前的数据是一个成 iPhone & iPad 高级编程 122 组的联系人列表,所有在这里只有一个区块,而所有的行都分布在这个区块中(如程序清单 3-4 所示)。 程序清单 3-4 数据源参数的特征 #pragma mark - #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[self contacts] allKeys] count]; } tableView:cellForRowAtIndexPath 方法会在表格中插入每一行的定制单元格然后使用联系人信息 进行填充(如程序清单 3-5 所示)。 程序清单 3-5 tableView:cellForRowAtIndexPath:方法 // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; NSDictionary *contact = [[self contacts] objectForKey:key]; static NSString *CellIdentifier = @"Cell"; CustomTableViewCell *cell = (CustomTableViewCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { [[NSBundle mainBundle] loadNibNamed:@"CustomTableViewCell" owner:self options:nil]; cell = customTableViewCell; self.customTableViewCell = nil; } // Configure the cell. [[cell lastNameLabel] setText:[contact objectForKey:@"lastName"]]; [[cell firstNameLabel] setText:[contact objectForKey:@"firstName"]]; [[cell streetLabel] setText:[contact objectForKey:@"street"]]; [[cell cityLabel] setText:[contact objectForKey:@"city"]]; [[cell stateLabel] setText:[contact objectForKey:@"state"]]; [[cell zipLabel] setText:[contact objectForKey:@"zip"]]; return cell; } 因为此应用程序没有导航,所以当用户点击某个单元格时,该单元格会呈取消选中状态而不是 选中状态。另外,为了从视觉上区分视图中的行,会交替使用深色和浅色背景(如程序清单 3-6 所示)。 第 3 章 定制表格视图 123 程序清单 3-6 tableView 委托方法 #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if((indexPath.row % 2 == 0)) { cell.backgroundColor = DARK_BACKGROUND; } else { cell.backgroundColor = LIGHT_BACKGROUND; } } 至此已经完成了 RootViewController.m 类。程序清单 3-7 显示了完整的实现代码。 程序清单 3-7 完整的 RootViewController.m 文件(/Desktop/ForChapter3/CustomTableViewApp/Classes/ RootViewController.m) #import "RootViewController.h" #import "CustomTableViewCell.h" #import "PropertyList.h" #define DARK_BACKGROUND \ [UIColor colorWithRed:151.0/255.0 green:152.0/255.0 \ blue:155.0/255.0 alpha:1.0] #define LIGHT_BACKGROUND \ [UIColor colorWithRed:172.0/255.0 green:173.0/255.0 \ blue:175.0/255.0 alpha:1.0] @implementation RootViewController @synthesize contacts; @synthesize customTableViewCell; #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; NSDictionary *contactList = [PropertyList readFromArchive:@"Contacts"]; [self setContacts:[contactList objectForKey:@"contacts"]]; [self setTitle:@"Contacts"]; iPhone & iPad 高级编程 124 [[self tableView] setRowHeight:65.0]; [[self tableView] setBackgroundColor:DARK_BACKGROUND]; [[self tableView] setSeparatorStyle: UITableViewCellSeparatorStyleSingleLineEtched]; } #pragma mark - #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[[self contacts] allKeys] count]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *key = [keys objectAtIndex:[indexPath row]]; NSDictionary *contact = [[self contacts] objectForKey:key]; static NSString *CellIdentifier = @"Cell"; CustomTableViewCell *cell = (CustomTableViewCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { [[NSBundle mainBundle] loadNibNamed:@"CustomTableViewCell" owner:self options:nil]; cell = customTableViewCell; self.customTableViewCell = nil; } // Configure the cell. [[cell lastNameLabel] setText:[contact objectForKey:@"lastName"]]; [[cell firstNameLabel] setText:[contact objectForKey:@"firstName"]]; [[cell streetLabel] setText:[contact objectForKey:@"street"]]; [[cell cityLabel] setText:[contact objectForKey:@"city"]]; [[cell stateLabel] setText:[contact objectForKey:@"state"]]; [[cell zipLabel] setText:[contact objectForKey:@"zip"]]; return cell; } #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; } - (void)tableView:(UITableView *)tableView 第 3 章 定制表格视图 125 willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if((indexPath.row % 2 == 0)) { cell.backgroundColor = DARK_BACKGROUND; } else { cell.backgroundColor = LIGHT_BACKGROUND; } } #pragma mark - #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setCustomTableViewCell:nil]; [self setContacts:nil]; } - (void)dealloc { [customTableViewCell release]; [contacts release]; [super dealloc]; } @end 修改 PropertyList.h 模板文件 在 PropertyList.h 文件中只需改动一处:使用 readFromArchive 方法从属性列表文件中加载数据 然后填充表格视图。 程序清单 3-8 显示了完整的 PropertyList.h 文件。 程序清单 3-8 完整的 PropertyList.h 文件(/Desktop/ForChapter3/CustomTableViewApp/Classes/ PropertyList.h) #import @interface PropertyList : NSObject { } + (NSDictionary *)readFromArchive:(NSString *)aFileName; @end 修改 PropertyList.m 模板文件 PropertyList 类中只有一个方法,即 readFromArchive。readFromArchive 方法读取姓名属性列表, 然后使用 NSDictionary 形式将读取的内容返回给调用方法(如程序清单 3-9 所示)。 iPhone & iPad 高级编程 126 程序清单 3-9 完整的 PropertyList.m 文件(/Desktop/ForChapter3/CustomTableViewApp/Classes/ PropertyList.m) #import "PropertyList.h" @implementation PropertyList + (NSDictionary *)readFromArchive:(NSString *)aFileName { NSString *errorDesc = nil; NSPropertyListFormat format; NSString *plistPath = [[NSBundle mainBundle] pathForResource:aFileName ofType:@"plist"]; NSData *plistXML = [[NSFileManager defaultManager] contentsAtPath:plistPath]; NSDictionary *temp = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:plistXML mutabilityOption:NSPropertyListMutableContainersAndLeaves format:&format errorDescription:&errorDesc]; if (!temp) { NSLog(@"%s at line %d with message: %@", __FUNCTION__, __LINE__, errorDesc); [errorDesc release]; } return temp; } @end 修改 CustomTableViewCell.h 模板文件 已经在 Interface Builder 中声明了以下 6 个 Outlet: ● cityLabel ● firstNameLabel ● lastNameLabel ● streetLabel ● stateLabel ● zipLabel 为了能够获取和设置这些变量的值,必须为这些变量定义属性。 程序清单 3-10 显示了完整的 CustomTableViewCell.h 文件。 程序清单3-10 完整的 CustomTableViewCell.h 文件(/Desktop/ForChapter3/CustomTable ViewApp/ Classes/CustomTableViewCell.h) #import @interface CustomTableViewCell : UITableViewCell { UILabel *cityLabel; UILabel *firstNameLabel; UILabel *lastNameLabel; UILabel *streetLabel; UILabel *stateLabel; UILabel *zipLabel; 第 3 章 定制表格视图 127 } @property (nonatomic, retain) IBOutlet UILabel *cityLabel; @property (nonatomic, retain) IBOutlet UILabel *firstNameLabel; @property (nonatomic, retain) IBOutlet UILabel *lastNameLabel; @property (nonatomic, retain) IBOutlet UILabel *streetLabel; @property (nonatomic, retain) IBOutlet UILabel *stateLabel; @property (nonatomic, retain) IBOutlet UILabel *zipLabel; @end 修改 CustomTableViewCell.m 模板文件 必须为每个已经声明的视图属性添加相应的@synthesize。程序清单 3-11 显示了完整的 CustomTableViewCell.m 文件。 程序清单 3-11 完整的 CustomTableViewCell.m 文件(/Desktop/ForChapter3/CustomTableViewApp/ Classes/CustomTableViewCell.m) #import "CustomTableViewCell.h" @implementation CustomTableViewCell @synthesize cityLabel; @synthesize firstNameLabel; @synthesize lastNameLabel; @synthesize streetLabel; @synthesize stateLabel; @synthesize zipLabel; - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if ((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { // Initialization code } return self; } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } - (void)dealloc { [cityLabel release]; [firstNameLabel release]; [lastNameLabel release]; [streetLabel release]; [stateLabel release]; [zipLabel release]; [super dealloc]; } @end iPhone & iPad 高级编程 128 3.2.2 创建 Contacts.plist 属性列表文件 此时,用户界面所需要添加的元素和需要建立的连接已全部完成。下面将创建一个 Contacts.plist 文件并将联系人信息输入该文件。为了创建 plist 文件,需要执行以下步骤: (1) 在 Xcode 的 Groups&Files 区域中单击 Resources 分组。选择 File | New File,然后从 Mac OS X 的 Resource 区域中选择 Property List(如图 3-9 所示)。将文件命名为 Contacts.plist。 图 3-9 (2) 在 Xcode 编辑器中选择 Root,然后单击旁边的三角形符号,以便鼠标能够单击到其下的内 容。单击最靠右的标签添加一个新的单元格,然后执行以下步骤: ● 用 Jones 替换 New Item,然后在 Type 中选择 Dictionary。 ● 单击 Jones 旁边的三角形符号,然后单击最靠右的标签添加一个单元格。 ● 用 lastName 替换 New Item,在 Type 中选择 String 并在 Value 中填入 Jones。 ● 单击右边的加号添加 firsName,在 Type 中选择 String 并在 Value 中填入 Joe。 ● 单击右边的加号添加 street,在 Type 中选择 String 并在 Value 中填入 123 Normal。 ● 单击右边的加号添加 city,在 Type 中选择 String 并在 Value 中填入 Normal。 ● 单击右边的加号添加 state,在 Type 中选择 String 并在 Value 中填入 IL。 ● 单击右边的加号添加 zip,在 Type 中选择 String 并在 Value 中填入 69304。 (3) 重复步骤(2)再添加 8 个左右联系人,完成后该文件应与图 3-10 类似。 现在开始测试应用程序。 第 3 章 定制表格视图 129 图 3-10 3.2.3 测试应用程序 各项步骤均已完成,现从 Xcode 面板选择 Simulator,然后单击 Run&Build 按钮测试应用程序。 此时,模拟器应该显示出“一个定制表格视图应用程序”一节开头描述的结果。 3.3 小结 表格视图是在 iPhone 和 iPod touch 中使用最频繁的控件之一。如本章所述,为了向表格视图中 填充数据,应用程序必须知道区块数量和行数,以便数据值能够保存在表格视图单元格中。 虽然苹果公司提供了一个具有默认类型的多功能组件 UITableViewCell,但是您也可以实现定制 的表格视图单元格,也就是说,可以为您的表格视图应用程序创建独特的外观。 需要注意,本章仅演示了一个标签与表格视图结合使用的例子,因为 UITableViewCell 是一个 视图,所以您可以根据应用程序的需要,在其中添加各种各样的用户界面组件。 数 据 存 储 本章内容: ● 如何在属性列表(Property List)中保存本地用户输入信息 ● 如何使用核心数据(Core Data)框架 ● 如何使用 Xcode 数据建模工具 试想一下这种情况:在完成对一个应用程序所有的细节设计之后,您会问:“我怎样在本地存储 信息呢?” 这里想要存储的数据并不是应用程序首选项(由 settings 捆绑包处理首选项),而是指用户输入的 信息。iOS 4 提供了两种十分简洁的方式来自动处理数据存储问题:属性列表和核心数据。 本章使用上述两种数据存储方式创建用于存储名、姓和联系人信息的应用程序。使用不同的数 据存储格式创建相同的应用程序,能够突出说明您可以在自己的应用程序中提供的不同数据存储 技术。 在进行数据存储时到底使用哪种格式,需要根据应用程序需要而定。数据量小,使用 XML 属 性列表实现最容易;数据量较大,则使用核心数据解决方案更合适。 需要注意,应用程序的目标是一个移动设备,因此,无法将其设计成一个大型的中央数据仓库。 苹果公司已经提供了多种足以满足数据存储需求的有效方法。对于更大型的存储需求,苹果公司也 提供了 MobileMe 作为一种可能的解决方案。 10.1 属性列表 属性列表(文件扩展名为.plist)是能够存储层次结构数据的简单 XML 格式文件。属性列表的优点 在于,可以使用它以可移植格式创建结构化数据,从而能够与其他平台和应用程序交换数据。 10.1.1 属性列表的使用 当需要存储少量数据时,例如少于 1MB 的数据,属性列表可以提供高度结构化的解决方案。 但如果数据是二进制形式的,那么并不推荐使用属性列表,因为它们是基于文本的。 10 第 章 iPhone & iPad 高级编程 328 从本章的应用程序中可以看到,您可以定制自己的对象,并将其存储在一个 plist 文件中。为了 使类能够顺利地归档和解档,该类必须实现 NSCoding 协议并提供以下两个方法: ● initWithCoder: ● encodeWithCoder: 10.1.2 推荐的数据元素类型 如前所述,并不推荐使用属性列表来存储二进制数据,但推荐使用属性列表来存储以下 6 种数 据元素类型: ● NSArray ● NSDictionary ● NSString ● NSData ● NSDate ● NSNumber 如果要存储定制对象,必须实现以下协议和方法来成功地保存和检索相应对象: ● NSCoding 协议 @interface Person : NSObject ● initWithCoder 方法和 encodeWithCoder 方法: - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setLastName:[coder decodeObjectForKey:@"firstName"]]; [self setFirstName:[coder decodeObjectForKey:@"lastName"]]; [self setPhone:[coder decodeObjectForKey:@"phone"]]; } return self; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self firstName] forKey:@"firstName"]; [coder encodeObject:[self lastName] forKey:@"lastName"]; [coder encodeObject:[self phone] forKey:@"phone"]; } 需要注意,数据编码的顺序必须与初始化时的顺序相同。 10.1.3 保存和还原属性列表 本章描述的应用程序会将属性列表读入一个 NSDictionary 中,然后将 NSDictionary 作为数据源 将其写入文件。 为了将 plist 文件读入 NSDictionary 中,需要使用 NSData dataWithContentsOfFile 方法,然后使 用 NSKeyArchiver 对象将其载入 NSDictionary: NSDictionary *result = nil; NSData *aData = [NSData dataWithContentsOfFile:bundlePath]; if(aData != nil) { result = [NSKeyedUnarchiver unarchiveObjectWithData:aData]; } 为了将一个NSDictionary写入文件,首先需要使用NSKeyedArchiver archivedDataWithRootObject: 第 10 章 数 据 存 储 329 将对象归档为 NSData 格式,然后使用 writeToFile:atomically:方法将其写入文件: NSData *aData = [NSKeyedArchiver archivedDataWithRootObject:plistDict]; BOOL status = [aData writeToFile:bundlePath atomically:YES]; 10.2 核心数据 属性列表提供关键字-值解决方案来存储数据,核心数据框架也提供了如下方案: ● 一对多关系 ● 分组和过滤 ● 对象的迟延加载 ● 内置属性数值校验 核心数据框架通过托管对象上下文来提供内置数据功能。 10.2.1 核心数据栈 核心数据栈是一个托管对象上下文中的托管对象集合。该上下文与对象存储(由一个托管对象模 型表示)协同工作。对象模型与托管对象上下文一起,以一种类似于数据库的形式持久存储数据。虽 然持久性存储未必是一个数据库,但是类似于 SQLite 的数据库还是可以直接与核心数据框架一起工 作来持久存储数据。 10.2.2 托管对象 核心数据框架提供了 NSManagedObject 类,用于表示数据库中的记录。托管对象表示的是应用 程序中使用的数据。 每个托管对象总是直接链接到一个托管对象上下文。 10.2.3 托管对象上下文 核心数据框架提供了表示一个对象区域(其唯一用途是管理托管对象集合)的 NSManaged- ObjectContext 类。 根据模型的不同,托管对象上下文通过一组模型对象提供了一个持久存储的视图。 通过托管对象上下文来提供核心数据框架的功能,例如,验证、关系和完整的对象管理。 10.2.4 托管对象模型 核心数据框架提供了 NSManagedObjectModel(是数据库架构的对象表示)。该模型本身是一个 NSEntityDescriptions 集合(是数据库实体的对象表示)。 核心数据使用托管对象模型作为托管对象和数据库记录之间的直接映射。 10.2.5 持久存储协调器 核心数据框架提供了一个完整架构,用于从应用程序中获取数据,并存储在一个或多个持久存 储(DWM)中:从而对数据进行保存、还原和撤消操作。NSPersistentStoreCoordinator 负责管理上述 所有持久存储。 应用程序不直接与此对象交互,但是它负责管理应用程序使用的所有持久对象存储。此对象最 大的优点是,当所有存储到达托管对象上下文时,所有数据看似来自一个合理组织的存储。 iPhone & iPad 高级编程 330 10.2.6 Xcode 建模工具 核心数据托管对象是一个实体、属性描述或模型的集合。使用 Xcode 提供的建模工具,可以形 象地创建核心数据模型。 使用数据建模工具,不仅可以创建需要的实体以及实体属性,还能以图形化的方式建立实体间 的关系。图 10-1 显示了本章中核心数据应用程序使用的模型。图中使用的实体是 Person 类的成员。 图 10-1 10.2.7 获取托管对象 从持久存储中获取托管对象的过程,需要用到一个托管对象上下文和一个NSFetchRequest实例。 可以由 NSPredicate 实例过滤获取请求。 [fetchRequest setPredicate:[NSPredicate predicateWithFormat: @"(accountNum IN %@)", accountNums]]; 使用 NSSortDescriptor 类对获取请求进行排序: NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastName" ascending:YES]; 如果使用表格视图显示,那么使用 NSFetchResultsController 来管理结果集,涉及的方面包括内 存管理以及表格视图单元格的内容。 10.2.8 删除托管对象 托管对象创建于托管对象上下文中,这并不意味着托管对象已经存入数据库中,理解这一点是 十分重要的。要持久保存对象,必须保存托管对象上下文。 删除托管对象时同样如此。在删除托管对象时,尽管托管对象上下文已经标明托管对象为删除 状态了,但是仍然需要保存托管对象上下文,这样才能从数据库中删除托管对象,如以下示例所示: if (editingStyle == UITableViewCellEditingStyleDelete) { // Delete the managed object for the given index path 第 10 章 数 据 存 储 331 NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext]; [context deleteObject:[[self fetchedResultsController] objectAtIndexPath:indexPath]]; // Save the context. NSError *error = nil; if (![context save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } } 下一节将描述的核心数据应用程序提供了与属性列表应用程序示例相同的功能,只是使用核心 数据框架来存储联系人数据。 10.3 数据存储的共同基础 以下应用程序对于属性列表和核心数据而言具备共同基础,只是联系人信息的存储方法不同。 第一个应用程序使用属性列表(有时称作 plists);第二个应用程序则使用核心数据来存储联系人数据。 这两个应用程序首次启动时,屏幕中会显示一个空的表格视图,原因是此时还没有添加联系人 信息。除了空表格视图外,在屏幕左方有一个可供删除列表中联系人的 Edit 按钮,在屏幕右方有一 个用于添加新联系人的+号按钮。以下步骤描述了添加、显示和删除一个联系人的过程(如图 10-2所示)。 图 10-2 ● 点击+号按钮(添加新的联系人)会打开一个用于录入名、姓和联系人电话号码的屏幕。在录 入联系人信息以后,用户点击 Save 按钮或者 Cancel 按钮。如果选择了 Save 按钮,列表中会 显示新录入的项。 iPhone & iPad 高级编程 332 ● 列表中只显示联系人的姓。要查看联系人详细信息,用户点击姓名项,此时,屏幕会显示 详细信息。 ● 要从列表中删除一项,需要点击 Edit 按钮,此时,联系人姓氏左边会出现一个红色的减号。 ● 如果点击红色的减号,那么减号会旋转到纵向位置,联系人姓氏右边会出现一个红色的 Delete 按钮。 点击 Delete 按钮从列表和存储中同时删除项。点击 Done 按钮退出 Edit 模式,此时,应用程序 会等待添加新的内容。下一节将分步讲述如何创建使用属性列表来存储联系人信息的应用程序。 10.3.1 开发步骤:一个使用属性列表的简单应用程序 为了创建使用属性列表存储联系人数据的应用程序,执行以下步骤: (1) 启动 Xcode,创建一个基于导航的 iPhone 应用程序,但不要选中“Use Core Data for storage”。 将项目命名为 PListStorage。附录 A 中列出了创建基于导航的应用程序的初始步骤,请参阅附录 A 了解相关信息。 (2) 在 Xcode 的 Groups&Files 区域中单击 Classes 分组。选择 File | New File,选择 Cocoa Touch Class 选项,然后选择 Objective-C 类作为 NSObject 的子类,并将其命名为 PropertyList。PropertyList 是用来处理读写属性列表数据的数据存储对象。 (3) 要存储的数据是 Person 对象。为了创建此类,选择 File | New File,然后选择 Objective-C 类 作为 NSObject 的子类,并将其命名为 Person。 (4) 为了添加一个将存储到属性列表中的新的 Person,需要创建一个新的视图控制器。选择 File | New File,然后选择 UIViewController 子类,确保“With XIB for user interface”被选中。将这个新 类命名为 PersonAddViewController。 (5) 为了显示被选中联系人的详细信息,需要创建一个新的视图控制器。选择 File | New File, 然后选择 UIViewController 子类,确保“UITableViewController subclass”和“ With XIB for user interface” 被选中。将这个新类命名为 PersonDetailViewController。 (6) 双击 PersonAddViewController.xib 文件,启动 Interface Builder(如图 10-3 所示)。 图 10-3 第 10 章 数 据 存 储 333 (7) 从 Interface Builder 的主菜单中选择 Tools | Attributes Inspector 选项,单击 View 窗口,然后 设置 Background 选项为 Group Table View Background Color。 (8) 从主菜单中选择 Tool | Library 选项,然后选择 Objects 选项卡。 (9) 选择一个 UITextField,将其拖入主视图中并放在靠近主视图顶部的区域,然后松开鼠标。 再重复这个过程两次,最后形成 3 个纵向排列的文本框(如图 10-4 所示)。 (10) 选中第一个文本字段并执行如下操作: ● 在 Placeholder 处输入 First Name。 ● 设置字体大小为 17。 ● 将 Capitalize 设置为 Words。 (11) 对接下来的两个字段重复上述步骤,使用 Last Name 作为第二个文本字段的占位符,使用 Phone 作为第三个文本字段的占位符(如图 10-5 所示)。 (12) 选择 Tools | Library,然后选择顶部的 Classes 选项,滚动选择 PersonAddViewController 类。 最后在界面的底部选择 Outlets 按钮,单击+号按钮并添加以下 Outlet(如图 10-6 所示): ● firstNameTextField(使用 UITextField 类型替代 id 类型) ● lastNameTextField(使用 UITextField 类型替代 id 类型) ● phoneTextField(使用 UITextField 类型替代 id 类型) 图 10-4 图 10-5 图 10-6 (13) 在 Interface Builder 主菜单中选择 File | Write Class Files,从第一个弹出对话框中选择 Save, 在下一个弹出对话框中选择 Merge。此时,添加过新内容的 PersonAddViewController.h 文件将显示在 窗口左边,而其原始模板则位于右边(如图 10-7 所示)。 ● 在右下角选择 Actions | Choose Left。 iPhone & iPad 高级编程 334 ● 选择 File | Save Merge,关闭窗口。 图 10-7 现在无需再对文件进行任何修改,关闭文件。到此为止,已完成一个包含应用程序逻辑的 Objective-C 模板。在真正着手编写程序之前,还需要在 Interface Builder 中完成一项工作,即建立所 有关联: ● 将 UITextField 确定为 firstNameTextField。 ● 将 UITextField 确定为 lastNameTextField。 ● 将 UITextField 确定为 phoneTextField。 (14) 为了在 UITextField 和 firstNameTextField 之间建立关联,在按住 Ctrl 键的同时单击 File's Owner 图标,打开 File's Owner 检查器(如图 10-8 所示)。 (15) 查看 File's Owner 检查器的右边,在按住 Ctrl 键的同时将 firstNameTextField 旁边的圆圈拖 到 UITextField firstNameTextField 上,当圆圈高亮显示时松开鼠标。圆圈填满表明已经建立了关联。 对 lastNameTextField 和 phoneTextField 重复这个过程(如图 10-9 所示)。 图 10-8 图 10-9 (16) 为了将 PersonAddViewController 设置为上述 3 个文本框的委托,在按住 Ctrl 键的同时将 firstNameTextField 拖到 File's Owner 图标上,然后选择委托。对 lastNameTextField 和 phoneTextField 重 复这个过程。设置之后,用户点击键盘上的 Done 按钮时,键盘将消失。选择 File | Save 保存文件。 到此为止,已为应用程序添加了所有用户界面需要的内容,并建立了所有关联。 第 10 章 数 据 存 储 335 现在开始设计程序逻辑。 一个使用属性列表的简单应用程序的源程序清单 此应用程序直接使用自动生成的 PListStorageAppDelegate.h 文件和 PlistStorageAppDelegate.m 文 件,并不对其进行修改。 修改 PersonAddViewController.h 模板文件 在 Interface Builder 中已经声明了 3 个 Outlet:firstNameTextField、lastNameTextField 和 phoneTextField。为了获取和设置这些变量的值,必须定义其属性。 将 IBOutlet 移到属性声明中。 为了识别委托类,需要声明一个 id delegate 变量,在本例中为 RootViewController。当用户点击 Save 按钮来保存刚输入的联系人信息时,RootViewController 将处理该保存行为。 为了处理文本字段委托消息,必须实现 UITextFieldDelegate 协议,因此,将其添加到类定义中。 程序清单 10-1 显示了完整的 PersonAddViewController.h 文件。 程序清单10-1 完整的 PersonAddViewController.h 文件(/Chapter10/PListStorage/Classes/Person- AddViewController.h) #import @interface PersonAddViewController : UIViewController { UITextField *firstNameTextField; UITextField *lastNameTextField; UITextField *phoneTextField; id delegate; } @property (nonatomic, retain) IBOutlet UITextField *firstNameTextField; @property (nonatomic, retain) IBOutlet UITextField *lastNameTextField; @property (nonatomic, retain) IBOutlet UITextField *phoneTextField; @property (nonatomic, retain) id delegate; @end 修改 PersonAddViewController.m 模板文件 前面更新了头文件,定义了添加到模板的内容,下面开始修改 PersonAddViewController.m 模板。 因为用于处理保存行为的委托类是 RootViewController,所以必须为该类添加#import 指令。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 10-2 所示)。 程序清单 10-2 添加@synthesize #import "PersonAddViewController.h" #import "RootViewController.h" @implementation PersonAddViewController @synthesize firstNameTextField; @synthesize lastNameTextField; @synthesize phoneTextField; @synthesize delegate; iPhone & iPad 高级编程 336 当 PersonAddViewController 视图在 viewDidLoad 方法中初始化时,会开始对导航栏进行配置(如 程序清单 10-3 所示)。为了让用户确定视图的功能,需要将视图标题指定为 Add Contact。还必须添 加 Save 按钮和 Cancel 按钮,最后,需要将第一个文本字段设置为光标输入点。 程序清单 10-3 viewDidLoad 方法 #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Configure the navigation bar self.navigationItem.title = @"Add Contact"; UIBarButtonItem *cancelBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStyleBordered target:self action:@selector(cancel)]; [[self navigationItem] setLeftBarButtonItem:cancelBarButtonItem]; [cancelBarButtonItem release]; UIBarButtonItem *saveBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Save" style:UIBarButtonItemStyleDone target:self action:@selector(save)]; [[self navigationItem] setRightBarButtonItem:saveBarButtonItem]; [saveBarButtonItem release]; [firstNameTextField becomeFirstResponder]; } 当用户点击文本字段时,键盘会自动出现,为了让键盘消失,拥有输入光标的当前文本框必须放 弃作为第一个应答者的资格。此过程通过名为 textFieldShouldReturn 的 UITextFieldDelegate 方法实现 (如程序清单 10-4 所示)。 程序清单 10-4 textFieldShouldReturn 方法 #pragma mark - #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } 最后,为了能够响应 Save 按钮和 Cancel 按钮,必须提供相关的方法。这两个方法都通过调用 委托类(在本例中为 RootViewController)来处理相应的操作(如程序清单 10-5 所示)。 程序清单 10-5 save 方法和 cancel 方法 #pragma mark - #pragma mark Action methods 第 10 章 数 据 存 储 337 - (void)save { [[self delegate] savePerson:self]; } - (void)cancel { [[self delegate] cancel]; } 至此就完成了 PersonAddViewController.m 文件。程序清单 10-6 显示了完整的实现代码。 程序清单 10-6 完整的 PersonAddViewController.m 文件(/Chapter10/PListStorage/Classes/Person- AddViewController.m) #import "PersonAddViewController.h" #import "RootViewController.h" @implementation PersonAddViewController @synthesize firstNameTextField; @synthesize lastNameTextField; @synthesize phoneTextField; @synthesize delegate; #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Configure the navigation bar self.navigationItem.title = @"Add Contact"; UIBarButtonItem *cancelBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStyleBordered target:self action:@selector(cancel)]; [[self navigationItem] setLeftBarButtonItem:cancelBarButtonItem]; [cancelBarButtonItem release]; UIBarButtonItem *saveBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Save" style:UIBarButtonItemStyleDone target:self action:@selector(save)]; [[self navigationItem] setRightBarButtonItem:saveBarButtonItem]; [saveBarButtonItem release]; [firstNameTextField becomeFirstResponder]; } #pragma mark - #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; iPhone & iPad 高级编程 338 } #pragma mark - #pragma mark Action methods - (void)save { [[self delegate] savePerson:self]; } - (void)cancel { [[self delegate] cancel]; } #pragma mark - #pragma mark Memory methods - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setFirstNameTextField:nil]; [self setLastNameTextField:nil]; [self setPhoneTextField:nil]; [super viewDidUnload]; } - (void)dealloc { [firstNameTextField release]; [lastNameTextField release]; [phoneTextField release]; [super dealloc]; } @end 修改 RootViewController.h 模板文件 在基于导航的应用程序中,应用程序启动时,RootViewController 是用户看到的中心视图。所有 数据导航都是从该视图开始的。 本应用程序声明了联系人的 NSDictionary。该字典包含了之前创建的 Person 类的成员。 PropertyList 类从该字典中获取 Person 对象,然后将 Person 对象存储到一个 plist 文件中。 必须为 PersonAddViewController 类提供两个可以调用的委托方法——savePerson 委托方法和 cancel 委托方法。 程序清单 10-7 显示了完整的 RootViewController.h 文件。 程序清单10-7 完整的RootViewController.h文件(/Chapter10/PListStorage/Classes/RootViewController.h) #import @class PersonAddViewController; @interface RootViewController : UITableViewController { NSDictionary *contacts; } 第 10 章 数 据 存 储 339 @property (nonatomic, retain) NSDictionary *contacts; - (void)addPerson:(id)sender; - (void)savePerson:(PersonAddViewController *)sender; - (void)cancel; @end 修改 RootViewController.m 模板文件 前面更新了头文件,定义了要添加到模板的内容,下面开始修改 RootViewController.m 模板。 因为 RootViewController 是本应用程序的基础视图,所以需要导入所有类。PersonAddView- Controller 类负责添加新的联系人。PersonDetailViewController 类负责显示联系人的详细信息。Person 类的成员负责创建、保存和显示对象。PropertyList 类负责存储联系人信息。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 10-8 所示)。 程序清单 10-8 添加@synthesize #import "RootViewController.h" #import "PersonAddViewController.h" #import "PersonDetailViewController.h" #import "Person.h" #import "PropertyList.h" @implementation RootViewController @synthesize contacts; 在 viewDidLoad 方法中初始化 RootViewController 视图时,会对先前存储的所有信息进行加载, 并用这些值对 contacts(NSDictionary 类型)变量初始化。为了让用户确定视图的功能,需要为导航栏 指定标题 contacts。此外,还需在导航栏添加 Edit 按钮和+号按钮(如程序清单 10-9 所示)。 程序清单 10-9 viewDidLoad 方法 #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setContacts:[PropertyList dictionaryFromPropertyList:@"Contacts"]]; [self setTitle:@"Contacts"]; [[self navigationItem] setLeftBarButtonItem:[self editButtonItem]]; UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPerson:)]; [[self navigationItem] setRightBarButtonItem:addButtonItem]; [addButtonItem release]; } 当用户点击+号按钮添加一个新的联系人时,PersonAddViewController 会接收用户提交的内容, iPhone & iPad 高级编程 340 并通知 RootViewController 处理委托消息(保存和取消),如程序清单 10-10 所示。 程序清单 10-10 addPerson 方法 #pragma mark - #pragma mark Action methods - (void)addPerson:(id)sender { PersonAddViewController *addController = [[PersonAddViewController alloc] init]; [addController setDelegate:self]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController]; [self presentModalViewController:navigationController animated:YES]; [navigationController release]; [addController release]; } RootViewController 响应由 PersonAddViewController 发送的 savePerson 和 cancel 操作。cancel 方法 会取消输入视图。savePerson 方法会获取最新的 contacts 字典,初始化一个新的 Person 联系人,将 其添加到 contacts 字典中,并将更新的数据写入属性列表(如程序清单 10-11 所示)。 程序清单 10-11 savePerson 和 cancel 委托方法 #pragma mark - #pragma mark Delegate Action methods - (void)savePerson:(PersonAddViewController *)sender { NSMutableDictionary *aContacts = [contacts mutableCopy]; Person *person = [[Person alloc] init]; [person setFirstName:[[sender firstNameTextField] text]]; [person setLastName:[[sender lastNameTextField] text]]; [person setPhone:[[sender phoneTextField] text]]; [aContacts setObject:person forKey:[person lastName]]; [self setContacts:aContacts]; [PropertyList writePropertyListFromDictionary:@"Contacts" dictionary:aContacts]; [aContacts release]; [person release]; [[self tableView] reloadData]; [self dismissModalViewControllerAnimated:YES]; } - (void)cancel { [self dismissModalViewControllerAnimated:YES]; } 在表格视图中,信息的显示格式取决于以下两个因素: ● 信息区块的数目 ● 每个区块中的行数 第 10 章 数 据 存 储 341 此应用程序中的数据均为联系人信息,可以看作一类数据,所以只需要一个区块。此区块中的 行数由 NSDictionary contacts 中存储的联系人数目决定(如程序清单 10-12 所示)。 程序清单 10-12 numberOfSectionsInTableView 和 tableView:numberOfRowsInSection 方法 #pragma mark - #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (contacts == nil) { [self setContacts:[NSDictionary dictionary]]; } return [[self contacts] count]; } 表格视图使用 tableView:cellForRowAtIndexPath:方法显示列表中的姓。同时,姓也是 contacts 字典 的关键字。为了显示排序列表,contacts 字典中的关键字保存在一个临时数组中,并进行了排序。表 格视图的 indexPath 行提供了数组的索引值。可以使用该索引从字典中检索值(如程序清单 10-13 所示)。 程序清单 10-13 tableView:cellForRowAtIndexPath 方法 // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *cellText = nil; NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; cellText = [keys objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } // Configure the cell. [[cell textLabel] setText:cellText]; return cell; } 如果想删除列表中的某个联系人,用户首先点击 Edit 按钮,然后点击红色减号按钮,最后点击 Delete 按钮。虽然 tabelView:commitEditingStyle:forRowAtIndexPath 委托方法可以处理上述操作并从 iPhone & iPad 高级编程 342 表格视图中删除单元格,但您必须从字典和存储的 plist 文件中删除它(如程序清单 10-14 所示)。 程序清单 10-14 tableView:commitEditingStyle:forRowAtIndexPath 方法 #pragma mark - #pragma mark Table view editing // Override to support editing the table view. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *contact = [keys objectAtIndex:[indexPath row]]; NSMutableDictionary *aContacts = [[self contacts] mutableCopy]; [aContacts removeObjectForKey:contact]; [self setContacts:aContacts]; [PropertyList writePropertyListFromDictionary:@"Contacts" dictionary:aContacts]; [aContacts release]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } } 用户点击某个联系人来查看详细信息时,作为响应,会将从 contacts 字典中检索到的 Person 对 象将存储在 PersonDetailViewController 类的一个实例中,该实例将 Person 对象压入导航栈来显示详 细信息(如程序清单 10-15 所示)。 程序清单 10-15 tableView:didSelectRowAtIndexPath 方法 #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *contact = [keys objectAtIndex:[indexPath row]]; Person *person = [[self contacts] objectForKey:contact]; PersonDetailViewController *personDetailViewController = [[PersonDetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [personDetailViewController setContact:person]; [[self navigationController] pushViewController:personDetailViewController animated:YES]; [personDetailViewController release]; } 第 10 章 数 据 存 储 343 至此完成了 RootViewController.m 类。程序清单 10-16 显示了完整的实现代码。 程序清单 10-16 完整的 RootViewController.m 文件(/Chapter10/PListStorage/Classes/RootView Controller.m) #import "RootViewController.h" #import "PersonAddViewController.h" #import "PersonDetailViewController.h" #import "Person.h" #import "PropertyList.h" @implementation RootViewController @synthesize contacts; #pragma mark - #pragma mark View lifecycle #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setContacts:[PropertyList dictionaryFromPropertyList:@"Contacts"]]; [self setTitle:@"Contacts"]; [[self navigationItem] setLeftBarButtonItem:[self editButtonItem]]; UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPerson:)]; [[self navigationItem] setRightBarButtonItem:addButtonItem]; [addButtonItem release]; } #pragma mark - #pragma mark Action methods - (void)addPerson:(id)sender { PersonAddViewController *addController = [[PersonAddViewController alloc] init]; [addController setDelegate:self]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController]; [self presentModalViewController:navigationController animated:YES]; [navigationController release]; [addController release]; } #pragma mark - #pragma mark Delegate Action methods - (void)savePerson:(PersonAddViewController *)sender { iPhone & iPad 高级编程 344 NSMutableDictionary *aContacts = [contacts mutableCopy]; Person *person = [[Person alloc] init]; [person setFirstName:[[sender firstNameTextField] text]]; [person setLastName:[[sender lastNameTextField] text]]; [person setPhone:[[sender phoneTextField] text]]; [aContacts setObject:person forKey:[person lastName]]; [self setContacts:aContacts]; [PropertyList writePropertyListFromDictionary:@"Contacts" dictionary:aContacts]; [aContacts release]; [person release]; [[self tableView] reloadData]; [self dismissModalViewControllerAnimated:YES]; } - (void)cancel { [self dismissModalViewControllerAnimated:YES]; } #pragma mark - #pragma mark Table view data source // Customize the number of sections in the table view. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } // Customize the number of rows in the table view. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (contacts == nil) { [self setContacts:[NSDictionary dictionary]]; } return [[self contacts] count]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *cellText = nil; NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; cellText = [keys objectAtIndex:[indexPath row]]; static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } 第 10 章 数 据 存 储 345 // Configure the cell. [[cell textLabel] setText:cellText]; return cell; } #pragma mark - #pragma mark Table view editing // Override to support editing the table view. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *contact = [keys objectAtIndex:[indexPath row]]; NSMutableDictionary *aContacts = [[self contacts] mutableCopy]; [aContacts removeObjectForKey:contact]; [self setContacts:aContacts]; [PropertyList writePropertyListFromDictionary:@"Contacts" dictionary:aContacts]; [aContacts release]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } } #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *keys = [[[self contacts] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; NSString *contact = [keys objectAtIndex:[indexPath row]]; Person *person = [[self contacts] objectForKey:contact]; PersonDetailViewController *personDetailViewController = [[PersonDetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [personDetailViewController setContact:person]; [[self navigationController] pushViewController:personDetailViewController animated:YES]; [personDetailViewController release]; } #pragma mark - #pragma mark Memory management iPhone & iPad 高级编程 346 - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setContacts:nil]; } - (void)dealloc { [contacts release]; [super dealloc]; } @end 修改 PersonDetailViewController.h 模板文件 用户从 RootViewController 的列表中选中某个联系人,并想要查看该联系人的详细信息时,将 初始化 PersonDetailViewController 类并显示相应的详细信息。 只需维护被选中的 Person 对象的值。 程序清单 10-17 显示了完整的 PersonDetailViewController.h 文件。 程序清单 10-17 完整的 PersonDetailViewController.h 文件(/Chapter10/PListStorage/Classes/Person- DetailViewController.h) #import @class Person; @interface PersonDetailViewController : UITableViewController { Person *contact; } @property (nonatomic, retain) Person *contact; @end 修改 PersonDetailViewController.m 模板文件 前面更新了头文件,定义了添加到模板的内容,下面开始修改 PersonDetailViewController.m 模板。 因为只添加了一个 Person 类型的 contact,所以只需为其添加相应的@synthesize(如程序清单 10-18 所示)。 程序清单 10-18 添加@synthesize #import "PersonDetailViewController.h" #import "Person.h" @implementation PersonDetailViewController @synthesize contact; 在 viewDidLoad 方法中初始化 PersonDetailViewController 视图时,必须让用户能够确定视图的 功能;因此,要把导航栏标题指定为 Contact Detail(如程序清单 10-19 所示)。 第 10 章 数 据 存 储 347 程序清单 10-19 viewDidLoad 方法 #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@"Contact Detail"]; } 在显示联系人详细信息时,联系人的姓氏与其他信息是分离的。为此需要定义两个区块。第一 个区块存放联系人姓氏;第二个区块存放名字和电话号码。 在定义了两个区块之后,第一个区块只有一行,存放姓氏;第二个区块设置两行,分别存放名 字和电话号码(如程序清单 10-20 所示)。 程序清单 10-20 numberOfSectionsInTableView 和 tableView:numberOfRowsInSection 方法 #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int rows = 0; if(section == 0) { rows = 1; // last name only } else { rows = 2; // first name and phone number } return rows; } 因为有两个区块,所以需要使用 tableView:titleForHeaderInSection 方法为每个区块添加一个描 述性的标题以便用户确定(如程序清单 10-21 所示)。 程序清单 10-21 tableView:titleForHeaderInSection 方法 // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *sectionTitle = nil; switch (section) { case 0: sectionTitle = @"Last Name"; break; case 1: sectionTitle = @"Details"; break; default: break; } iPhone & iPad 高级编程 348 return sectionTitle; } 第一个区块显示联系人姓氏。第二个区块中的第一行显示联系人的名字,第二行显示电话号码 (如程序清单 10-22 所示)。 程序清单 10-22 tableView:cellForRowAtIndexPath 方法 // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { int section = [indexPath section]; int row = [indexPath row]; NSString *cellText = nil; static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... switch (section) { case 0: cellText = [contact lastName]; break; case 1: switch (row) { case 0: cellText = [contact firstName]; break; case 1: cellText = [contact phone]; break; default: break; } default: break; } [[cell textLabel] setText:cellText]; return cell; } 至此已经完成了 PersonDetailViewController.m 类。程序清单 10-23 显示了完整的实现代码。 程序清单10-23 完整的PersonDetailViewController.m文件(/Chapter10/PListStorage/Classes/Person- DetailViewController.m) #import "PersonDetailViewController.h" #import "Person.h" 第 10 章 数 据 存 储 349 @implementation PersonDetailViewController @synthesize contact; #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@"Contact Detail"]; } #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int rows = 0; if(section == 0) { rows = 1; // last name only } else { rows = 2; // first name and phone number } return rows; } // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *sectionTitle = nil; switch (section) { case 0: sectionTitle = @"Last Name"; break; case 1: sectionTitle = @"Details"; break; default: break; } return sectionTitle; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { int section = [indexPath section]; int row = [indexPath row]; NSString *cellText = nil; static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = iPhone & iPad 高级编程 350 [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... switch (section) { case 0: cellText = [contact lastName]; break; case 1: switch (row) { case 0: cellText = [contact firstName]; break; case 1: cellText = [contact phone]; break; default: break; } default: break; } [[cell textLabel] setText:cellText]; return cell; } #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // Navigation logic may go here. Create and push another view controller. } #pragma mark - #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setContact:nil]; } - (void)dealloc { [contact release]; [super dealloc]; } @end 第 10 章 数 据 存 储 351 修改 Person.h 模板文件 使用一个 Person 类的实例存储联系人信息。该实例中保存了以下 3 个值: ● 名 ● 姓 ● 电话号码 为了将一个类保存到文件中,该类必须实现 NSCoding 协议。 程序清单 10-24 显示了完整的 Person.h 文件。 程序清单 10-24 完整的 Person.h 文件(/Chapter10/PListStorage/Classes/Person.h) #import @interface Person : NSObject { NSString *firstName; NSString *lastName; NSString *phone; } @property (nonatomic, retain) NSString *firstName; @property (nonatomic, retain) NSString *lastName; @property (nonatomic, retain) NSString *phone; @end 修改 Person.m 模板文件 前面更新了头文件,定义了添加到模板的内容,下面开始修改 Person.m 模板。 必须为每个已声明的视图属性添加相应的@synthesize(如程序清单 10-25 所示)。 程序清单 10-25 添加@synthesize #import "Person.h" @implementation Person @synthesize firstName; @synthesize lastName; @synthesize phone; 实现了 NSCoding 协议以后,该对象必须使用两个方法来保证信息的正确读写——encodeWith- Coder 方法和 initWithCoder 方法(如程序清单 10-26 所示)。 程序清单 10-26 encodeWithCoder 方法和 initWithCoder 方法 #pragma mark - #pragma mark NSCoder methods - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self firstName] forKey:@"firstName"]; [coder encodeObject:[self lastName] forKey:@"lastName"]; [coder encodeObject:[self phone] forKey:@"phone"]; } iPhone & iPad 高级编程 352 - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setLastName:[coder decodeObjectForKey:@"firstName"]]; [self setFirstName:[coder decodeObjectForKey:@"lastName"]]; [self setPhone:[coder decodeObjectForKey:@"phone"]]; } return self; } 至此已经完成了 Person.m 类。程序清单 10-27 显示了完整的实现代码。 程序清单 10-27 完整的 Person.m 文件(/Chapter10/PListStorage/Classes/Person.m) #import "Person.h" @implementation Person @synthesize firstName; @synthesize lastName; @synthesize phone; #pragma mark - #pragma mark NSCoder methods - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self firstName] forKey:@"firstName"]; [coder encodeObject:[self lastName] forKey:@"lastName"]; [coder encodeObject:[self phone] forKey:@"phone"]; } - (id)initWithCoder:(NSCoder *)coder { if (self = [super init]) { [self setLastName:[coder decodeObjectForKey:@"firstName"]]; [self setFirstName:[coder decodeObjectForKey:@"lastName"]]; [self setPhone:[coder decodeObjectForKey:@"phone"]]; } return self; } #pragma mark - #pragma mark Memory methods - (void)dealloc { [firstName release]; [lastName release]; [phone release]; [super dealloc]; } @end 修改 PropertyList.h 和 PropertyList.m 模板文件 为了在属性列表中存储和检索数据,需要创建一个 PropertyList 类。只需声明两个方法: ● dictionaryFromPropertyList ● writePropertyListFromDictionary:dictionary 需要注意,上述两个方法属于工厂方法,因为无需创建 PropertyList 类的一个实例。 第 10 章 数 据 存 储 353 在保存或删除联系人时,需要在 RootViewController 的初始化过程中调用 dictionaryFrom- PropertyList 方法,然后调用 writePropertyListFromDictionary:dictionary 方法。 程序清单 10-28 显示了完整的 PropertyList.h 文件,程序清单 10-29 显示了完整的 PropertyList.m 文件。 程序清单 10-28 完整的 PropertyList.h 文件(/Chapter10/PListStorage/Classes/PropertyList.h) #import @interface PropertyList : NSObject { } + (NSDictionary *)dictionaryFromPropertyList:(NSString *)filename; + (BOOL)writePropertyListFromDictionary:(NSString *)filename dictionary:(NSDictionary *)plistDict; @end 程序清单 10-29 完整的 PropertyList.m 文件(/Chapter10/PListStorage/Classes/PropertyList.m) #import "PropertyList.h" @implementation PropertyList + (NSDictionary *)dictionaryFromPropertyList:(NSString *)filename { NSDictionary *result = nil; NSString *fname = [NSString stringWithFormat:@"%@.plist", filename]; NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *bundlePath = [rootPath stringByAppendingPathComponent:fname]; NSData *aData = [NSData dataWithContentsOfFile:bundlePath]; if(aData != nil) { result = [NSKeyedUnarchiver unarchiveObjectWithData:aData]; } return result; } + (BOOL)writePropertyListFromDictionary:(NSString *)filename dictionary:(NSDictionary *)plistDict { NSString *fname = [NSString stringWithFormat:@"%@.plist", filename]; NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *bundlePath = [rootPath stringByAppendingPathComponent:fname]; NSData *aData = [NSKeyedArchiver archivedDataWithRootObject:plistDict]; return [aData writeToFile:bundlePath atomically:YES]; } @end iPhone & iPad 高级编程 354 10.3.2 测试应用程序 各项步骤均已完成,现在在 Xcode 面板上选择 Simulator,然后单击 Run & Build 按钮测试应用 程序。此时,模拟器应该显示“一个简单的使用属性列表和核心数据的应用程序”一节开头描述的 结果。 现已完成了使用属性列表的应用程序,下面开始创建拥有相同功能的另一个应用程序。这两个 应用程序的不同之处在于联系人信息的存储、检索和删除方式。上一个应用程序使用属性列表来实 现这些功能;本应用程序则使用核心数据来实现。 10.3.3 开发步骤:一个使用核心数据的简单应用程序 要创建使用核心数据存储联系人数据的应用程序,执行以下步骤: (1) 启动 Xcode,创建一个基于导航的 iPhone 应用程序,然后选中“Use Core Data for storage” 选项。将项目命名为 CoreDataStorage。附录 A 中列出了创建基于导航的应用程序的初始步骤,可从 中了解有关该步骤的信息。 (2) 数据将被保存到 Person 对象中。为了创建 Person 类,选择 File | New File 选项,然后选中 Objective-C 类作为 NSObject 的子类,并将其命名为 Person。 (3) 为了添加一个新的存储在核心数据中的 Person,需要创建一个新的视图控制器。选择 File | New File,然后选择 UIViewController 子类,确保只有“With XIB for user interface”复选框被选中。 将这个新类命名为 PersonAddViewController。 (4) 为了显示所选联系人的详细信息,需要创建一个新的视图控制器。选择 File | New File,然 后选择 UIViewController 子类,确保“UITableViewController subclass”复选框和“With XIB for user interface”复选框被选中。将这个新类命名为 PersonDetailViewController。 (5) 双击 PersonAddViewController.xib 文件,启动 Interface Builder(如图 10-10 所示)。 图 10-10 第 10 章 数 据 存 储 355 (6) 从 Interface Builder 的主菜单中选择 Tools | Attributes Inspector 选项,单击 View 窗口,然后 将 Background 选项设置为 Group Table View Background Color。 (7) 从主菜单中选择 Tools | Library,然后选择 Objects 选项卡。 (8) 选择一个 UITextField,将其拖入主视图并放在靠近主视图顶部的区域,然后松开鼠标。再 重复此过程两次,最后形成 3 个纵向排列的文本字段(如图 10-11 所示)。 (9) 选中第一个文本字段,并执行如下操作: ● 在 Placeholder(占位符)处输入 First Name。 ● 设置字体大小为 17。 ● 将 Capitalize 设置为 Words。 (10) 对接下来的两个字段重复上述步骤,使用 Last Name 作为第二个文本字段的占位符,Phone 作为第三个文本字段的占位符(如图 10-12 所示)。 (11) 选择 Tools | Library,然后选择界面顶部的 Classes,滚动选择 PersonAddViewController 类。 在界面底部选择 Outlets 按钮,单击+号按钮并添加以下 Outlet(如图 10-13 所示): ● firstNameTextField(使用 UITextField 类型替代 id 类型) ● lastNameTextField(使用 UITextField 类型替代 id 类型) ● phoneTextField(使用 UITextField 类型替代 id 类型) 图 10-11 图 10-12 图 10-13 (12) 在 Interface Builder 主菜单中选择 File | Write Class Files,从第一个弹出对话框中选择 Save 按钮,在下一个弹出对话框中选择 Merge 按钮。此时,添加过新内容的 PersonAddViewController.h 文件将显示在窗口左边,而其原始模板则位于右边(如图 10-14 所示)。 iPhone & iPad 高级编程 356 ● 在右下角选择 Actions | Choose Left。 ● 选择 File | Save Merge,关闭窗口。 图 10-14 现在无需再对 PersonAddViewController.m 文件进行任何修改,关闭文件。到此为止,已经拥有 了一个包含应用程序视图逻辑的 Objective-C 模板。在真正着手编写程序之前,还需要在 Interface Builder 中完成一项工作,即建立所有关联: ● 将 UITextField 确定为 firstNameTextField。 ● 将 UITextField 确定为 lastNameTextField。 ● 将 UITextField 确定为 phoneTextField。 (13) 为了在 UITextField 和 firstNameTextField 之间建立关联,在按住 Ctrl 键的同时单击 File's Owner 图标,打开 File's Owner 检查器(如图 10-15 所示)。 (14) 查看 File's Owner 检查器的右边,在按住 Ctrl 键的同时将 firstNameTextField 旁边的圆圈拖 到 UITextField firstNameTextField 上,当圆圈高亮显示时松开鼠标。圆圈填满表明已经建立了关联。 对 lastNameTextField 和 phoneTextField 重复这个过程(如图 10-16 所示)。 图 10-15 图 10-16 (15) 为了将 PersonAddViewController 设置为这 3 个文本字段的委托,在按住 Ctrl 键的同时将 第 10 章 数 据 存 储 357 firstNameTextField 拖到 File's Owner 图标上,然后选择委托。对 lastNameTextField 和 phoneTextField 重复此过程。这样,当用户点击键盘上的 Done 按钮时,键盘就会消失。选择 File | Save 选项保存文件。 到此为止,已经为应用程序添加了用户界面需要的所有内容,并建立了所有需要的关联。现在 开始设计程序逻辑。 一个使用核心数据的简单应用程序的源程序清单 此应用程序直接使用自动生成的 PListStorageAppDelegate.h 文件和 PlistStorageAppDelegate.m 文 件,并不对其进行修改。 修改 PersonAddViewController.h 模板文件 在Interface Builder中已经声明了3个Outlet:firstNameTextField、lastNameTextField和phoneTextField。 为了获取和设置这些变量的值,必须定义其属性。 将 IBOutlet 移到属性声明中。 为了识别委托类,声明了一个 id delegate 变量,在本例中为 RootViewController。当用户点击 Save 按钮来保存刚输入的联系人信息时,RootViewController 将处理该保存行为。 为了处理文本字段委托消息,必须实现 UITextFieldDelegate 协议,因此,需要将其添加到类定 义中。 程序清单 10-30 显示了完整的 PersonAddViewController.h 文件。 程序清单 10-30 完整的PersonAddViewController.h 文件(/Chapter10/CoreDataStorage/Classes/Person- AddViewController.h) #import @interface PersonAddViewController : UIViewController { UITextField *firstNameTextField; UITextField *lastNameTextField; UITextField *phoneTextField; id delegate; } @property (nonatomic, retain) IBOutlet UITextField *firstNameTextField; @property (nonatomic, retain) IBOutlet UITextField *lastNameTextField; @property (nonatomic, retain) IBOutlet UITextField *phoneTextField; @property (nonatomic, retain) id delegate; @end 修改 PersonAddViewController.m 模板文件 前面更新了头文件,定义了要添加到模板中的内容,下面开始修改PersonAddViewController.m 模板。 因为用于处理保存行为的委托类是 RootViewController,所以必须为该类添加#import 指令。 此外,必须为每一个已经声明的视图属性添加相应的@synthesize(如程序清单 10-31 所示)。 程序清单 10-31 添加@synthesize #import "PersonAddViewController.h" iPhone & iPad 高级编程 358 #import "RootViewController.h" @implementation PersonAddViewController @synthesize firstNameTextField; @synthesize lastNameTextField; @synthesize phoneTextField; @synthesize delegate; 在 viewDidLoad 方法中初始化 RootViewController 视图时,会开始对导航栏进行配置。为了让 用户确定视图的作用,将视图标题指定为 Add Contact。还必须添加 Save 按钮和 Cancel 按钮,并将 第一个文本字段设置为光标输入点(如程序清单 10-32 所示)。 程序清单 10-32 viewDidLoad 方法 #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Configure the navigation bar self.navigationItem.title = @"Add Contact"; UIBarButtonItem *cancelBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStyleBordered target:self action:@selector(cancel)]; [[self navigationItem] setLeftBarButtonItem:cancelBarButtonItem]; [cancelBarButtonItem release]; UIBarButtonItem *saveBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Save" style:UIBarButtonItemStyleDone target:self action:@selector(save)]; [[self navigationItem] setRightBarButtonItem:saveBarButtonItem]; [saveBarButtonItem release]; [firstNameTextField becomeFirstResponder]; } 当用户点击文本字段时,将自动出现键盘,为了关闭键盘,拥有输入光标的当前文本字段必 须放弃作为第一个应答者的资格。这个过程由一个名为 textFieldShouldReturn 的 UITextFieldDelegate 方法实现(如程序清单 10-33 所示)。 程序清单 10-33 textFieldShouldReturn 方法 #pragma mark - #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } 第 10 章 数 据 存 储 359 最后,为了能够响应 Save 按钮和 Cancel 按钮,必须提供相关的方法。这两个方法都通过调用 委托类(在本例中为 RootViewController)来处理相应的操作,如程序清单 10-34 所示。 程序清单 10-34 save 方法和 cancel 方法 #pragma mark - #pragma mark Action methods - (void)save { [[self delegate] savePerson:self]; } - (void)cancel { [[self delegate] cancel]; } 到此为止,已经完成了 PersonAddViewController.m 类。程序清单 10-35 显示了完整的实现代码。 程序清单 10-35 完整的 PersonAddViewController.m 文件(/Chapter10/CoreDataStorage/Classes/Person- AddViewController.m) #import "PersonAddViewController.h" #import "RootViewController.h" @implementation PersonAddViewController @synthesize firstNameTextField; @synthesize lastNameTextField; @synthesize phoneTextField; @synthesize delegate; #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; // Configure the navigation bar self.navigationItem.title = @"Add Contact"; UIBarButtonItem *cancelBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStyleBordered target:self action:@selector(cancel)]; [[self navigationItem] setLeftBarButtonItem:cancelBarButtonItem]; [cancelBarButtonItem release]; UIBarButtonItem *saveBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Save" style:UIBarButtonItemStyleDone target:self action:@selector(save)]; [[self navigationItem] setRightBarButtonItem:saveBarButtonItem]; [saveBarButtonItem release]; iPhone & iPad 高级编程 360 [firstNameTextField becomeFirstResponder]; } #pragma mark - #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { [textField resignFirstResponder]; return YES; } #pragma mark - #pragma mark Action methods - (void)save { [[self delegate] savePerson:self]; } - (void)cancel { [[self delegate] cancel]; } #pragma mark - #pragma mark Memory methods - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setFirstNameTextField:nil]; [self setLastNameTextField:nil]; [self setPhoneTextField:nil]; [super viewDidUnload]; } - (void)dealloc { [firstNameTextField release]; [lastNameTextField release]; [phoneTextField release]; [super dealloc]; } @end 修改 RootViewController.h 模板文件 在基于导航的应用程序中,当应用程序启动时,RootViewController 是用户看到的中心视图。所 有数据导航都从该视图开始。 在此应用程序中,会从 NSManagedObjectContext 中的 Person 实体获取结果集,NSFetched- ResultsController 负责对获取的结果集进行管理。 还必须为 PersonAddViewController 类提供两个可以调用的委托方法——savePerson 委托方法和 cancel 委托方法。 程序清单 10-36 显示了完整的 RootViewController.h 文件。 第 10 章 数 据 存 储 361 程序清单 10-36 完整的 RootViewController.h 文件(/Chapter10/CoreDataStorage/Classes/RootView- Controller.h) #import #import @class PersonAddViewController; @interface RootViewController : UITableViewController { @private NSFetchedResultsController *fetchedResultsController_; NSManagedObjectContext *managedObjectContext_; } @property (nonatomic, retain) NSManagedObjectContext *managedObjectContext; @property (nonatomic, retain) NSFetchedResultsController *fetchedResultsController; - (void)addPerson:(id)sender; - (void)savePerson:(PersonAddViewController *)sender; - (void)cancel; @end 修改 RootViewController.m 模板文件 前面更新了头文件,定义了添加到模板的内容,下面开始修改 RootViewController.m 模板。 因为 RootViewController 是此应用程序的基础视图,所以需要导入所有的类。PersonAdd- ViewController 类负责添加新的联系人。PersonDetailViewController 类负责显示联系人的详细信息。 Person 类的成员负责创建、保存和显示对象。 另外需要注意,有一个由模板生成的私有方法——configureCell:IndexPath:,该方法用于确定显 示在每个表格视图单元格中的值。 必须为每个已经声明的视图属性添加相应的@synthesize(如程序清单 10-37 所示)。 程序清单 10-37 添加@synthesize #import "RootViewController.h" #import "PersonAddViewController.h" #import "PersonDetailViewController.h" #import "Person.h" @interface RootViewController () - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath; @end @implementation RootViewController @synthesize fetchedResultsController=fetchedResultsController_; @synthesize managedObjectContext=managedObjectContext_; 在 viewDidLoad 方法中初始化 RootViewController 视图时,会使用 NSFetchedResultsController 实 例对先前存储的所有信息进行加载。可以通过 NSManagedObjectContext 访问 Person 实体(使用 iPhone & iPad 高级编程 362 NSManagedObjects 表示)。 为了让用户确定该视图的功能,将导航栏标题指定为 Contacts。此外,必须在导航栏添加 Edit 按钮和+号按钮(如图 10-9 所示)。 在获取到数据之后,为了刷新表格视图,需要在 viewWillAppear 方法中进行表格视图重载(如程 序清单 10-38 所示)。 程序清单 10-38 viewDidLoad 方法和 viewWillAppear 方法 #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@"Contacts"]; [[self navigationItem] setLeftBarButtonItem:[self editButtonItem]]; UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPerson:)]; [[self navigationItem] setRightBarButtonItem:addButtonItem]; [addButtonItem release]; NSError *error; if (![[self fetchedResultsController] performFetch:&error]) { // Update to handle the error appropriately. NSLog(@"Unresolved error %@, %@", error, [error userInfo]); exit(-1); // Fail } } // Implement viewWillAppear: to do additional setup before the // view is presented. - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [[self tableView] reloadData]; } 当用户点击+号按钮来添加一个新联系人时,addPerson 方法会处理该操作,与 PersonAdd- ViewController 类关联的视图中会显示所有输入的联系人信息(如程序清单 10-39 所示)。 程序清单 10-39 addPerson 方法 #pragma mark - #pragma mark Action methods - (void)addPerson:(id)sender { PersonAddViewController *addController = [[PersonAddViewController alloc] init]; [addController setDelegate:self]; UINavigationController *navigationController = 第 10 章 数 据 存 储 363 [[UINavigationController alloc] initWithRootViewController:addController]; [self presentModalViewController:navigationController animated:YES]; [navigationController release]; [addController release]; } RootViewController响应由 PersonAddViewController发送的savePerson操作和 cancel操作。cancel 方法会关闭输入视图。savePerson 方法会获取 Person 实体的 managedObjectsContext,然后创建一个 新的 NSManagedObject。会使用从 PersonAddViewController 类中输入的值来填充 Person 对象,然后 保存上下文(如程序清单 10-40 所示)。 程序清单 10-40 savePerson 委托方法和 cancel 委托方法 #pragma mark - #pragma mark Delegate Action methods - (void)savePerson:(PersonAddViewController *)sender { // Create a new instance of the entity managed by the // fetched results controller. NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext]; NSEntityDescription *entity = [[[self fetchedResultsController] fetchRequest] entity]; NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context]; [newManagedObject setValue:[[sender firstNameTextField] text] forKey:@"firstName"]; [newManagedObject setValue:[[sender lastNameTextField] text] forKey:@"lastName"]; [newManagedObject setValue:[[sender phoneTextField] text] forKey:@"phone"]; // Save the context. NSError *error = nil; if (![context save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } [[self tableView] reloadData]; [self dismissModalViewControllerAnimated:YES]; } - (void)cancel { [self dismissModalViewControllerAnimated:YES]; } 在表格视图中,信息的显示格式取决于以下两个因素: ● 信息区块的数目 ● 每个区块中的行数 此应用程序中的数据均为联系人信息,可以看作一类数据,所以只需要一个区块。此区块中的 iPhone & iPad 高级编程 364 行数由 NSFetchedResultsSectionInfo 中的对象数目决定(如程序清单 10-41 所示)。 程序清单 10-41 numberOfSectionsInTableView 方法和 tableView:numberOfRowsInSection 方法 #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [[[self fetchedResultsController] sections] count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { id sectionInfo = [[[self fetchedResultsController] sections] objectAtIndex:section]; return [sectionInfo numberOfObjects]; } 表格视图仅将联系人姓氏显示在列表中。为了显示排序列表,需要配置 fetchedResultsController 方 法,将其排序字段设置为升序或者降序(如程序清单 10-42 所示)。 程序清单 10-42 fetchedResultsController 方法 #pragma mark - #pragma mark Fetched results controller - (NSFetchedResultsController *)fetchedResultsController { if (fetchedResultsController_ != nil) { return fetchedResultsController_; } /* Set up the fetched results controller. */ // Create the fetch request for the entity. NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; // Edit the entity name as appropriate. NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:self.managedObjectContext]; [fetchRequest setEntity:entity]; // Set the batch size to a suitable number. [fetchRequest setFetchBatchSize:20]; // Edit the sort key as appropriate. NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastName" ascending:YES]; NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil]; [fetchRequest setSortDescriptors:sortDescriptors]; // Edit the section name key path and cache name if appropriate. // nil for section name key path means "no sections". 第 10 章 数 据 存 储 365 NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[self managedObjectContext] sectionNameKeyPath:nil cacheName:@"Root"]; [aFetchedResultsController setDelegate:self]; [self setFetchedResultsController:aFetchedResultsController]; [aFetchedResultsController release]; [fetchRequest release]; [sortDescriptor release]; [sortDescriptors release]; NSError *error = nil; if (![fetchedResultsController_ performFetch:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } return fetchedResultsController_; } 为了在表格视图中显示姓氏,必须从核心数据中进行检索。configureCell:atIndexPath:方法从 fetchedResultsController 中获取到姓氏,并将其插入 NSManagedObject 中。此后,需要从托管对象中 检索姓氏值,并将其显示在表格视图单元格中。 用于检索姓氏的特定行将作为一个 NSIndexPath 对象,传入 tableView:cellForRowAtIndexPath:方法 中(如程序清单 10-43 所示)。 程序清单 10-43 configureCell:atIndexPath:方法和 tableView:cellForRowAtIndexPath:方法 #pragma mark - #pragma mark TableView cell appearance - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath { NSManagedObject *managedObject = [[self fetchedResultsController] objectAtIndexPath:indexPath]; [[cell textLabel] setText:[[managedObject valueForKey:@"lastName"] description]]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } iPhone & iPad 高级编程 366 // Configure the cell. [self configureCell:cell atIndexPath:indexPath]; return cell; } 要删除列表中的某个联系人,用户需要点击 Edit 按钮,然后点击红色减号按钮,最后点击 Delete 按钮。虽然 tabelView:commitEditingStyle:forRowAtIndexPath 委托方法可以处理上述操作并从表格视 图中删除单元格,但您还是必须从核心数据中删除联系人数据(如程序清单 10-44 所示)。 程序清单 10-44 tableView:commitEditingStyle:forRowAtIndexPath 方法 // Override to support editing the table view. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // Delete the managed object for the given index path NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext]; [context deleteObject:[[self fetchedResultsController] objectAtIndexPath:indexPath]]; // Save the context. NSError *error = nil; if (![context save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } } } 用户点击某个联系人表示需要查看详细信息。为响应该请求,用 fetchedResultController 检索到 的 Person 对象将存储到 PersonDetailViewController 类的一个实例中,随后将压入导航栈来显示联系 人详细信息(如程序清单 10-45 所示)。 程序清单 10-45 tableView:didSelectRowAtIndexPath 方法 #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { Person *person = (Person *)[[self fetchedResultsController] objectAtIndexPath:indexPath]; PersonDetailViewController *personDetailViewController = [[PersonDetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [personDetailViewController setContact:person]; [[self navigationController] pushViewController:personDetailViewController animated:YES]; [personDetailViewController release]; } 第 10 章 数 据 存 储 367 到此为止,已经完成了对 RootViewController.m 类模板的修改。程序清单 10-46 显示了完整的实 现代码。 程序清单 10-46 完整的 RootViewController.m 文件(/Chapter10/CoreDataStorage/Classes/Root- ViewController.m) #import "RootViewController.h" #import "PersonAddViewController.h" #import "PersonDetailViewController.h" #import "Person.h" @interface RootViewController () - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath; @end @implementation RootViewController @synthesize fetchedResultsController=fetchedResultsController_; @synthesize managedObjectContext=managedObjectContext_; #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@"Contacts"]; [[self navigationItem] setLeftBarButtonItem:[self editButtonItem]]; UIBarButtonItem *addButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPerson:)]; [[self navigationItem] setRightBarButtonItem:addButtonItem]; [addButtonItem release]; NSError *error; if (![[self fetchedResultsController] performFetch:&error]) { // Update to handle the error appropriately. NSLog(@"Unresolved error %@, %@", error, [error userInfo]); exit(-1); // Fail } } // Implement viewWillAppear: to do additional setup before the // view is presented. - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [[self tableView] reloadData]; } #pragma mark - #pragma mark Action methods iPhone & iPad 高级编程 368 - (void)addPerson:(id)sender { PersonAddViewController *addController = [[PersonAddViewController alloc] init]; [addController setDelegate:self]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:addController]; [self presentModalViewController:navigationController animated:YES]; [navigationController release]; [addController release]; } #pragma mark - #pragma mark Delegate Action methods - (void)savePerson:(PersonAddViewController *)sender { // Create a new instance of the entity managed by the // fetched results controller. NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext]; NSEntityDescription *entity = [[[self fetchedResultsController] fetchRequest] entity]; NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context]; [newManagedObject setValue:[[sender firstNameTextField] text] forKey:@"firstName"]; [newManagedObject setValue:[[sender lastNameTextField] text] forKey:@"lastName"]; [newManagedObject setValue:[[sender phoneTextField] text] forKey:@"phone"]; // Save the context. NSError *error = nil; if (![context save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } [[self tableView] reloadData]; [self dismissModalViewControllerAnimated:YES]; } - (void)cancel { [self dismissModalViewControllerAnimated:YES]; } #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [[[self fetchedResultsController] sections] count]; } 第 10 章 数 据 存 储 369 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { id sectionInfo = [[[self fetchedResultsController] sections] objectAtIndex:section]; return [sectionInfo numberOfObjects]; } #pragma mark - #pragma mark TableView cell appearance - (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath { NSManagedObject *managedObject = [[self fetchedResultsController] objectAtIndexPath:indexPath]; [[cell textLabel] setText:[[managedObject valueForKey:@"lastName"] description]]; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; [cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; } // Configure the cell. [self configureCell:cell atIndexPath:indexPath]; return cell; } // Override to support editing the table view. - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // Delete the managed object for the given index path NSManagedObjectContext *context = [[self fetchedResultsController] managedObjectContext]; [context deleteObject:[[self fetchedResultsController] objectAtIndexPath:indexPath]]; // Save the context. NSError *error = nil; if (![context save:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } iPhone & iPad 高级编程 370 } } - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { // The table view should not be re-orderable. return NO; } #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { Person *person = (Person *)[[self fetchedResultsController] objectAtIndexPath:indexPath]; PersonDetailViewController *personDetailViewController = [[PersonDetailViewController alloc] initWithStyle:UITableViewStyleGrouped]; [personDetailViewController setContact:person]; [[self navigationController] pushViewController:personDetailViewController animated:YES]; [personDetailViewController release]; } #pragma mark - #pragma mark Fetched results controller - (NSFetchedResultsController *)fetchedResultsController { if (fetchedResultsController_ != nil) { return fetchedResultsController_; } /* Set up the fetched results controller. */ // Create the fetch request for the entity. NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; // Edit the entity name as appropriate. NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:self.managedObjectContext]; [fetchRequest setEntity:entity]; // Set the batch size to a suitable number. [fetchRequest setFetchBatchSize:20]; // Edit the sort key as appropriate. NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastName" ascending:YES]; NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil]; [fetchRequest setSortDescriptors:sortDescriptors]; 第 10 章 数 据 存 储 371 // Edit the section name key path and cache name if appropriate. // nil for section name key path means "no sections". NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[self managedObjectContext] sectionNameKeyPath:nil cacheName:@"Root"]; [aFetchedResultsController setDelegate:self]; [self setFetchedResultsController:aFetchedResultsController]; [aFetchedResultsController release]; [fetchRequest release]; [sortDescriptor release]; [sortDescriptors release]; NSError *error = nil; if (![fetchedResultsController_ performFetch:&error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } return fetchedResultsController_; } #pragma mark - #pragma mark Fetched results controller delegate - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { [[self tableView] beginUpdates]; } - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { switch(type) { case NSFetchedResultsChangeInsert: [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeDelete: [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; break; } } - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { iPhone & iPad 高级编程 372 UITableView *tableView = self.tableView; switch(type) { case NSFetchedResultsChangeInsert: [tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeDelete: [tableView deleteRowsAtIndexPaths: [NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; break; case NSFetchedResultsChangeUpdate: [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath]; break; case NSFetchedResultsChangeMove: [tableView deleteRowsAtIndexPaths: [NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; [tableView insertRowsAtIndexPaths: [NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break; } } - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { [[self tableView] endUpdates]; } /* // Implementing the above methods to update the table view in response to individual changes may have performance implications if a large number of changes are made simultaneously. If this proves to be an issue, you can instead just implement controllerDidChangeContent: which notifies the delegate that all section and object changes have been processed. - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { // In the simplest, most efficient, case, reload the table view. [self.tableView reloadData]; } */ #pragma mark - #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { 第 10 章 数 据 存 储 373 [self setFetchedResultsController:nil]; } - (void)dealloc { [fetchedResultsController_ release]; [managedObjectContext_ release]; [super dealloc]; } @end 修改 PersonDetailViewController.h 模板文件 当用户从 RootViewController 的列表中选中一个联系人,并想要查看该联系人的详细信息时, 会初始化 PersonDetailViewController 类并进行显示。 只需维护被选中的 Person 对象的值。 程序清单 10-47 显示了完整的 PersonDetailViewContrller.h 文件。 程序清单10-47 完整的 PersonDetailViewController.h文件(/Chapter10/CoreDataStorage/Classes/Person- DetailViewController.h) #import @class Person; @interface PersonDetailViewController : UITableViewController { Person *contact; } @property (nonatomic, retain) Person *contact; @end 修改 PersonDetailViewController.m 模板文件 前面更新了头文件,定义了要添加到模板的内容,下面开始修改PersonDetailViewController.m 模板。 因为只添加了一个 Person 类型的 contact,所以只需为其添加相应的@synthesize(如程序清单 10-48 所示)。 程序清单 10-48 添加@synthesize #import "PersonDetailViewController.h" #import "Person.h" @implementation PersonDetailViewController @synthesize contact; 在 viewDidLoad 方法中初始化 PersonDetailViewController 视图时,需要让用户确定视图的功能, 因此,需要将导航栏标题指定为 Contact Detail(如程序清单 10-49 所示)。 程序清单 10-49 viewDidLoad 方法 #pragma mark - iPhone & iPad 高级编程 374 #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@"Contact Detail"]; } 在显示详细信息时,联系人姓氏和其他内容是分开显示的。要完成该操作,需要定义两个区块。 第一个区块存放姓氏;第二个区块存放名字和电话号码。 在分组定义以后,第一个区块只有一行,用于存放姓氏;第二个区块有两行内容,分别存放名 字和电话号码(如程序清单 10-50 所示)。 程序清单 10-50 numberOfSectionsInTableView 方法和 tableView:numberOfRowsInSection 方法 #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int rows = 0; if(section == 0) { rows = 1; // last name only } else { rows = 2; // first name and phone number } return rows; } 因为有两个区块,所以需要为每个区块添加一个描述性的标题用于识别(如程序清单 10-51 所示)。 程序清单 10-51 tableView:titleForHeaderInSection 方法 // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *sectionTitle = nil; switch (section) { case 0: sectionTitle = @"Last Name"; break; case 1: sectionTitle = @"Details"; break; default: break; } return sectionTitle; } 第一个区块显示联系人的姓氏。第二个区块中的第一行显示联系人的名字,第二行显示电话号 第 10 章 数 据 存 储 375 码(如程序清单 10-52 所示)。 程序清单 10-52 tableView:cellForRowAtIndexPath 方法 // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { int section = [indexPath section]; int row = [indexPath row]; NSString *cellText = nil; static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... switch (section) { case 0: cellText = [contact lastName]; break; case 1: switch (row) { case 0: cellText = [contact firstName]; break; case 1: cellText = [contact phone]; break; default: break; } default: break; } [[cell textLabel] setText:cellText]; return cell; } 至此已经完成了 PersonDetailViewController.m 类。程序清单 10-53 显示了完整的实现代码。 程序清单 10-53 完整的 PersonDetailViewController.m 文件(/Chapter10/CoreDataStorage/Classes/Person- DetailViewController.m) #import "PersonDetailViewController.h" #import "Person.h" @implementation PersonDetailViewController @synthesize contact; iPhone & iPad 高级编程 376 #pragma mark - #pragma mark View lifecycle - (void)viewDidLoad { [super viewDidLoad]; [self setTitle:@"Contact Detail"]; } #pragma mark - #pragma mark Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int rows = 0; if(section == 0) { rows = 1; } else { rows = 2; } return rows; } // Customize the Header Titles of the table view. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *sectionTitle = nil; switch (section) { case 0: sectionTitle = @"Last Name"; break; case 1: sectionTitle = @"Details"; break; default: break; } return sectionTitle; } // Customize the appearance of table view cells. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { int section = [indexPath section]; int row = [indexPath row]; NSString *cellText = nil; static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 第 10 章 数 据 存 储 377 reuseIdentifier:CellIdentifier] autorelease]; } // Configure the cell... switch (section) { case 0: cellText = [contact lastName]; break; case 1: switch (row) { case 0: cellText = [contact firstName]; break; case 1: cellText = [contact phone]; break; default: break; } default: break; } [[cell textLabel] setText:cellText]; return cell; } #pragma mark - #pragma mark Table view delegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { } #pragma mark - #pragma mark Memory management - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } - (void)viewDidUnload { [self setContact:nil]; } - (void)dealloc { [contact release]; [super dealloc]; } @end 修改 Person.h 模板文件 使用 Person 类的一个实例来存储联系人信息。该实例中保存了以下 3 个值: ● 名 ● 姓 iPhone & iPad 高级编程 378 ● 电话号码 为了使用 Core Data 存储类,必须添加头文件,并将 Person 类声明为 NSManagedObject 的子类。 程序清单 10-54 显示了完整的 Person.h 文件。 程序清单 10-54 完整的 Person.h 文件(/Chapter10/CoreDataStorage/Classes/Person.h) #import @interface Person : NSManagedObject { } @property (nonatomic, retain) NSString *firstName; @property (nonatomic, retain) NSString *lastName; @property (nonatomic, retain) NSString *phone; @end 修改 Person.m 模板文件 前面更新了头文件,定义了添加到模板的内容,下面开始修改 Person.m 模板。只需为每个已经 声明的视图属性添加相应的@synthesize 即可(如程序清单 10-55 所示)。 至此完成了 Person.m 类。程序清单 10-55 显示了完整的实现代码。 程序清单 10-55 完整的 Person.m 文件(/Chapter10/CoreDataStorage/Classes/Person.m) #import "Person.h" @implementation Person @synthesize firstName; @synthesize lastName; @synthesize phone; @end 为一个使用核心数据的简单应用程序创建数据模型 在完成了对用户界面和源代码的设计和修改以后,现在开始定制数据模型模板,在其中添加所 需的 Person 类。 修改 CoreDataStorage.xcdatamodel 模板文件 如果在第一步的创建基于导航的应用程序时选中了使用核心数据的选项,那么 Xcode 将自动生 成一个默认数据模型。默认状态下会定义以下内容: ● Event 实体 ● Date 类的 timeStamp 属性 为了定制适合本应用程序的数据模型,执行以下步骤: (1) 在Xcode 的Groups&Files 窗口中选中Resources 选项,然后选择CoreDataStorage.xcdatamodel 打开数据建模器(如图 10-17 所示)。 第 10 章 数 据 存 储 379 (2) 在 Entity 区域选中 Entity,然后将 Name 和 Class 同时改为 Person(如图 10-18)。 图 10-17 图 10-18 (3) 在 Property 区域中选中 timestamp,然后将 Name 改为 firstName,并取消选中“Optional” 复选框,最后将 Type 改为 String(如图 10-19 所示)。 (4) 单击底部 Property 区域的+号按钮,选择 Add Attribute 选项,为 lastName 和 phone 重复上述 操作,将其 Type 设置为 String(如图 10-20 所示)。 iPhone & iPad 高级编程 380 图 10-19 图 10-20 (5) 在完成上述步骤以后,该数据模型应该与图 10-21 类似。 第 10 章 数 据 存 储 381 图 10-21 10.3.4 测试应用程序 各项步骤均已完成,现在在 Xcode 面板上选择 Simulator,然后单击 Run & Build 按钮测试应用 程序。此时,模拟器应该显示“一个使用属性列表和核心数据的简单应用程序”一节开头描述的结果。 10.4 小结 使用属性列表来存储和检索少量数据是一种快速而易于实现的方法,对用于处理少量数据的应 用程序来说,属性列表是一种非常理想的解决方案。plist 文件本身是可读的,而且可以在任何文本 编辑器中加以修改,这使得它的用途非常广泛。 虽然核心数据框架在处理数据存储时需要的步骤更多,但您不能忽视核心数据提供的 Xcode 数 据建模工具和框架的卓越能力。 如果在模拟器中运行时遇到了“The model used to open the store is incompatible with the one used to create the store”错误,则必须从模拟器中完全删除生成文件夹和应用程序。
还剩129页未读

继续阅读

下载pdf到电脑,查找使用更方便

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

需要 8 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

kidzss

贡献于2013-04-17

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf