cocoa programming for mac os

220

Upload: liu-feng

Post on 19-Mar-2016

263 views

Category:

Documents


1 download

DESCRIPTION

Cocoa programming for mac os

TRANSCRIPT

Page 1: Cocoa programming for mac os
Page 2: Cocoa programming for mac os

2 页

Cocoa Programming for Mac OS X 3rd

翻译版

版 权:属于 appleboy

原文链接:http://blog.sina.com.cn/s/articlelist_1576524975_0_1.html

PDF 制作:jinifly

英文原版:共 35章

现有章节:共 17章

--2009.4.8--

Page 3: Cocoa programming for mac os

3 页

目 录

开篇(2008-12-30 17:37:51) ......................................................................................................... 6

第一章:什么是 Cocoa .................................................................................................................... 6

第二章:起步 ................................................................................................................................. 13

XCode ...................................................................................................................................... 13

Interface Builder ...................................................................................................................... 18

回到 XCode .............................................................................................................................. 26

文档......................................................................................................................................... 33

你做了什么? ........................................................................................................................... 33

第三章: Objective-C 语言 ........................................................................................................... 36

创建,使用对象 ....................................................................................................................... 37

使用已有的类 ......................................................................................................................... 39

创建自己的类 ......................................................................................................................... 49

调试器..................................................................................................................................... 60

你做了什么? ........................................................................................................................... 63

思考: 消息机制工作原理是什么? ........................................................................................ 64

挑战......................................................................................................................................... 65

第四章: 内存管理 ......................................................................................................................... 66

开关 garbage collector ...................................................................................................... 67

使用 Garbage Collection .................................................................................................... 68

使用 Retain 计数 .................................................................................................................. 69

第五章: Target/Action ............................................................................................................... 78

一些常用的 NSControl 子类 ................................................................................................. 80

开始 SpeakLine例子 ............................................................................................................. 83

布局界面 (nib file) ........................................................................................................... 84

挑战......................................................................................................................................... 88

调试建议 ................................................................................................................................. 89

第六章: 辅助(helper)对象 .......................................................................................................... 91

代理 - Delegates ................................................................................................................. 91

NSTableView 和它的 dataSource ....................................................................................... 95

布局用户界面 ......................................................................................................................... 97

连接......................................................................................................................................... 98

编辑 AppController.m ........................................................................................................ 100

思考:代理是怎么工作的? ................................................................................................... 103

挑战: 生成一个 Delegate .................................................................................................. 104

挑战: 生成一个 Data Source ............................................................................................ 105

第七章: Key-Value Coding. Key-Vaule Observing ................................................................. 106

Key-Value Coding ............................................................................................................... 106

绑定 (Binding) ................................................................................................................... 108

Key-Value Observing ......................................................................................................... 109

观察 key ................................................................................................................................ 110

Properties 和它们的属性 ................................................................................................. 111

Page 4: Cocoa programming for mac os

4 页

Property的属性 .................................................................................................................. 112

思考: Key Path ................................................................................................................... 113

思考: Key-Value Observing ............................................................................................. 114

第八章:NSArrayController ....................................................................................................... 116

开始 RaiseMan程序 ............................................................................................................. 117

Key-Vaule Coding 和 nil ................................................................................................. 124

增加排序 ............................................................................................................................... 125

思考: 不使用 NSArrayController 来进行排序 ............................................................... 126

挑战 1.................................................................................................................................... 126

挑战 2.................................................................................................................................... 127

第九章:NSUndoManager ............................................................................................................... 129

NSInvocation ....................................................................................................................... 129

NSUndoManager是怎样工作的 ............................................................................................ 129

为 RaiseMan添加 Undo功能 ............................................................................................... 132

Key-Vaule Observing ......................................................................................................... 135

Undo编辑 .............................................................................................................................. 136

插入后开始编辑 ................................................................................................................... 138

思考: Windows和 Undo Manager ....................................................................................... 140

第十章:Archiving ....................................................................................................................... 142

NSCoder 和 NSCoding .......................................................................................................... 143

Document Architecture ..................................................................................................... 145

Saving 和 NSKeyedArchiver ............................................................................................. 149

Loading和 NSKeyedUnarchiver ......................................................................................... 150

设置后缀名和图标 ............................................................................................................... 151

思考:避免死循环 ................................................................................................................. 153

思考: 创建 Protocol .......................................................................................................... 154

通用类型描述[UTI] ............................................................................................................. 155

第十一章: Core Data基本原理 .................................................................................................. 157

NSManagedObjectModel ....................................................................................................... 157

Interface ............................................................................................................................. 159

Core Data 是怎么工作的 ................................................................................................... 165

第十二章: Nib 文件和 NSWindowController ............................................................................ 167

NSPanel ................................................................................................................................. 167

给程序添加一个 Panel ........................................................................................................ 168

思考: NSBundle ................................................................................................................... 177

挑战....................................................................................................................................... 177

第十三章: User Defaut ............................................................................................................... 178

NSDictionary 和 NSMutableDictionary ......................................................................... 178

NSUserDefaults ................................................................................................................... 180

设置程序的 Identifier ...................................................................................................... 182

给 Defaults Key 命名 ......................................................................................................... 182

Registering Defaults ....................................................................................................... 183

让用户编辑 defaults .......................................................................................................... 184

Page 5: Cocoa programming for mac os

5 页

使用 Defaults ...................................................................................................................... 185

思考: NSUserDefaultsController ................................................................................... 187

思考:使用 Command line来读写 Defaults ...................................................................... 187

挑战....................................................................................................................................... 188

第十四章: 使用 Notifications ................................................................................................. 189

什么是 Notification? ........................................................................................................ 189

Notifications 不是什么 ................................................................................................... 189

NSNotification 和 NSNotificationCenter ................................................................... 190

发送一个 Notification ...................................................................................................... 192

注册成为 Observer .............................................................................................................. 192

处理 Notification .............................................................................................................. 193

userInfo Dictionary ......................................................................................................... 193

思考....................................................................................................................................... 194

挑战....................................................................................................................................... 195

第十五章: 使用 Alert Panels .................................................................................................... 196

让用户确认删除 ................................................................................................................... 197

挑战....................................................................................................................................... 199

第十六章: 本地化 ........................................................................................................................ 200

本地化 nib文件 ................................................................................................................... 201

String Tables ..................................................................................................................... 202

思考:ibtool ....................................................................................................................... 205

思考:用格式化串标明 Token的顺序 ............................................................................... 206

第十七章: 自定义 View ............................................................................................................... 207

View的层次 .......................................................................................................................... 207

让一个 View画自己 ............................................................................................................. 209

使用 NSBezierPath绘制 ..................................................................................................... 213

NSScrollView ....................................................................................................................... 215

通过程序创建 View .............................................................................................................. 217

思考:cells ........................................................................................................................... 218

思考: isFlipped ................................................................................................................. 219

挑战....................................................................................................................................... 220

Page 6: Cocoa programming for mac os

6 页

开篇(2008-12-30 17:37:51)

做 mac 也有很长时间了。一直以来都觉得 mac 的资料及其匮乏,(当然是指中文资料。

其实英文够好,ADC 就可以了)。以后记录一些看的相关东西在这里吧

第一章:什么是 Cocoa

开始..

我也不太清楚为什么会有翻译这边书的打算. 大概是因为国内开发 mac 的同学越来越

多了(拜 iPhone所赐吧).想起自己当初开发mac,找不到很好的开发资料(尤其是中文)的苦.

想做点什么吧.希望能给大家带来点帮助

以前从来没有翻译过什么东西,加上自己英文也不是很好.所以可能会有些错误以及不能

很好的体现原作的原汁原味. 还好自己在 mac 系统上开发多年,不能说是大牛,但多少还

是能体会到 cocoa 的一些底层思想.所以应该会以意译为住,并可能会在某些地方加注一

些自己的体会. 好吧,开始上路了. (不知道什么时候能够完成这个计划)..

++++++++++++++++++

第一章:什么是 cocoa

1- 一点历史

让我们从一个有意思的故事开始我们的 cocoa 旅程吧.很久前(我还没出生呢)有两个叫

Steve 的天才创建了一个公司,名为苹果电脑,这家公司成长的非常快,所以他们聘请了一

个叫 John Sculley 的人来担任他们公司的 CEO. 没想到的是,在一些矛盾冲突后,

John Sculley 居然把其中一个 Steve 的赶出了苹果公司,这个 Steve 就是现在大名顶顶的

Steve Jobs. Jobs 在离开苹果后组建了一个新的公司 Next Computer

NeXT 公司雇佣了一些有才华的工程师组建了一个小团队. 这个团队开发了自己的电脑,

操作系统,打印机和一堆的开发工具.这些在当时那个年度都是超前的.不幸的是他们的硬

件没有好的市场.在 1993 年,工厂也关门了,NeXT Computer 也变成了 Next Software.

操作系统和那些开发工具以 NeXTSTEP 为名继续在卖.可能一般的计算机用户都没有听

Page 7: Cocoa programming for mac os

7 页

过 NeXTSTEP.但 NeXTSTEP 在某些领域很流行,并且有一些人一直在使用它来开发自己

的应用程序.他们觉得 NeXTSTEP 能更快的让他们的想法能变成计算机实现.

好了,那就让我们看看这是怎样的一个操作系统. NeXT 使用 Unix 作为 NeXTSTEP 的内核.

为什么是 Unix 呢?可能是因为 Unix 比 Windows 或 Mac OS 更稳定,

Apple 的 Darwin 就是基于 Unix 创建作为 Mac OSX 核心.有兴趣的朋友可以访问

http://macosforge.org/得到更多的信息.

NeXT 给他们的操作系统编写了一个 window server. window server 可以把用户输入的

事件转发个应用程序. 而应用程序会给 window server 发送 drawing 给 window server 来

刷新 UI. NeXTSTEP 的一个优点是发给应用程序的绘制指令和发给打印机的一样,所以程

序员只需要写一次控制代码,便可以用于显示在显示器上也可以用于打印机上。在

NextStep 的时代,程序员书写可以生成 PostScript 的代码,而在 Mac OS X 时代,程序员

使用 Core Graphics(通常所说的 Quartz)框架的代码,Quartz 可以在屏幕上合成这些图

形或者把他们发送到打印机,或者生成 PDF 数据。PDF 是一个由 Adobe 公司制定的开

放的协议,主要用于矢量图形的存储。

如果你以前使用过 Unix,你可能会比较熟悉 X 窗口服务器,Mac OS X 的 window

server 于 X 窗口服务器完全不同,但是它完成了 X 窗口服务器相同的功能:从用户那里

接受事件并将事件发给应用程序,然后将应用程序发来的数据显示在屏幕上。可是 X 协

议对反锯齿字体和透明化支持不是很好,这就是 Mac OS X 窗口比 X-window 窗口 看

起来更加美观的一个原因。

NeXTSTEP 内置的许多库和工具,让程序员以一种优雅的方式与窗口管理器进行交互,这

些 libraries 叫做 Framework,在 1993 年这些 frameworks 和工具被重新修改并重命名为

OpenStep,后来被重命名为 Cocoa(大家应该知道为什么 Cocoa 的类都有着 NS 的前缀了

吧)

如图 1.1: 窗口服务器和应用程序都是 unix 进程,Cocoa 可以使应用程序从窗口服务器

接受事件以及在屏幕上绘图。

Page 8: Cocoa programming for mac os

8 页

我们使用的是一种叫做 Objective-C 的语言应用这个 Frameworks 来编程。就像 C++

一样,Objective-C 是在 C 语言上拓展,使得它基于对象。但是不像 C++ ,Objective-C 是

一种弱类型语言,同时也是极其强大的语言。Objective-C 允许程序员犯一些可笑的小错

误。 Objective-C 是 C 语言的一个简单的扩充,你会觉得它很容易学。

程序员喜欢 OpenStep。OpenStep 可以让他们更容易的试验他们的新想法。事实上,

Tim Berners-lee 在 NexTSTEP 上开发出了第一个网页浏览器和 web 服务器。证券分析

员们可以很快的开发和测试新的金融模块。大学生们可以开发他们的研究程序。我不

知道情报局的人们拿NexTStep来做什么,但是情报局购买了数千份OpenStep 的拷贝。

因为这些工具是如此的实用,OpenStep开发工具被移植到 Solaries和Windows NT 上,

NextStep 操作系统被移植到当时多数流行的 CPU 平台上包括:Intel, Motorola,

Hewlett-Packard's PA-RISC, 及 SPARC。

许多年以来,苹果公司致力于开发一个具有 NextStep 特性的操作系统。这就是所知的

Copland 计划。Copland 计划逐渐的失控了,苹果公司最终决定放弃开发,转向从别的

公司购买下一个版本的 Mac OS。在经过调查现存的操作系统之后,苹果公司选择了

NextSTEP,因为 NeXT 很小,苹果 1996 年收购了 Next 整个公司。

我为什么讲这段历史呢?我一直在华尔街为 NeXT 公司写代码直到他们雇佣我教授其他

的开发人员 OpenStep 技术。当 NeXT 和苹果合并的时候我是 NeXT 的一员。我教授了许

多苹果工程师怎样为 Mac OS X 开发程序。现在的我不是苹果公司的一员了,我现在为

Big Nerd Ranch 公司教授 Cocoa。

NeXTSTEP 变成 Mac OS X,它是 Unix 的一个分支,你可以在 Mac OS X 上找到所有的标准

Unix 程序,比如 Apache Web Server,在 Mac OS X 上,它比在 Windows 和 Mac OS 9

更加的稳定.用户界面也更加的漂亮,作为一名开发人员,你将会爱上 Mac OS X,因为

Cocoa 可以使你快捷、高效、优雅的开发出功能强大的应用程序。

2- 开发工具

你会爱上 Cocoa 的,虽然可能不是马上。首先,你将学习基本知识,让我们从将用到的工

具开始吧.Cocoa 开发用到的所有工具都作为 Mac OS X Developer Tool 的一部分一起提

供给用户,你可以从 Mac OS X 安装盘中免费的得到它们。虽然 Mac OS X Developer

Tool 将会在你的系统中添加许多有用的程序。但是我们将主要使用这两个程序:Xcode

和 Interface Builder。而在这些 IDE 内部,gcc 作为编译器来编译代码,gdb 来调试。

Xcode掌控应用程序中所有使用到的资源,代码,图片,声音等等.你只要在 Xcode 中编写代

Page 9: Cocoa programming for mac os

9 页

码,由 Xcode 来编译和运行你的程序。同时 Xcode 也能调用和控制调试器。

Interface Builder 是一个 GUI 构建工具,通过它你可以创建窗口布局并且把各个组件添

加到窗口中,Interface Builder 使程序员能方便的创建各种对象和编辑它们的属性,这些对

象多数是用户界面元件,比如按钮和文本输入框,但是也可以是你自定义的对象

3- 开发语言

这本书中所有的例子都将使用 Objective-c 语言来编写。Objective-C 语言是 C 语言简单

而优雅的扩充.如果你已经掌握了 c 或者其他面向对象的语言,如 C++或者 java,你大概

花两个小时能掌握它.

也可以使用 Ruby 或者 Python 来开发 Cocoa 应用程序,本书不会涵盖这部分内容,如果你

感兴趣可以在网上找到大量的资源,不过你想理解它,你仍然需要了解一些 Objective-c 的

原理。

Objective-C 已经发展到一个主要版本,这本书中的所有的代码都是基于 Objective-C 2.0

版本的.苹果在2.0版的Objective-C中添加了垃圾回收机制.垃圾回收机制是可以使用或

者不使用.在本书中你将使用它。

Objective-C 的代码由 gcc 编译,gcc 编译器允许你自由的在同一文件中混合 C ,C++和

以及 Objective-C 语言。

gdb 将会被用来设置断点,查看运行中的程序中的变量的值,Objective-C 在调试中给了你

很大的自由.你会很乐意地去使用这样一个合适的调试器。

4- 对象,类,方法和消息

Cocoa 编程使用面向对象技术.这一节我们来简单回顾一下面向对象的知识.如果你没有

这方面的经验.我建议学习一本书:The Objective-C Language . 你可以在苹果网站上下

载到 PDF 版本 http://developer.apple.com/documentation/Cocoa/Conceptual/ObjectiveC/

ObjC.pdf(当然也是英文版的.是否有必要下次把它翻译下:) )

什么是对象?对象就像C语言中的结构.它占用内存空间来保存自己的变量.这些变量对于

对象而言叫:instance variables. 所以在处理对象时,我们首先相到的是:你怎样分配空间

么?这个对象拥有那些 instance variables?在不再使用对象的时候,你有释放它么?

对象的某些 instance variables 可能是其他一些对象的指针.拥有这样的指针,我们称为该

Page 10: Cocoa programming for mac os

10 页

对象"know about"那个所指向的对象.

类是可以用来创建对象的结构.类中定义了对象中拥有的变量并且负责为对象申

请内存空间,我们可以这样说.对象是类的一个实例.

对象要优于结构的地方是对象有相关的函数.我们把这些函数叫做方法. 你可以发送一

个消息来调用指定的某个方法.

5- Framework

框架是一些类的集合,通常是一些可能一起使用的类集合.换句话说,这些类被一起编译成

可复用的代码.加上相关的所有资源组成一个代扩展名.framework的目录(Mac OSX

bundle 的概念可以参考苹果的一些文档). 系统自带的 framework 可以在

/System/Library/Frameworks 找到.

Cocoa 有 3 个 framework 组成

Page 11: Cocoa programming for mac os

11 页

1. Foundation: 所有的面向对象语言都会有一些标准结构: value , collection ,

String, dates, lists, thread 等等.所有的这些都在 Foundation framework 里面(可以对比 C

++的 STL)

2. Appkit: 所有和用户界面相关的类都在这里. Windows,buttons,text field, event,

drawing. 它还有个名字: ApplicationKit

3. Core Data: Core Data 可以方便的让你把你的对象存储成文件,或是从文件中加

载你的对象.

当然 Mac OSX 开发包还提供了其他的很多 framework. 比如 QuickTime, CD 刻录等等.

这本书我们只关注上面提到的 3 个基本 framework. 一旦你掌握了这些基本 framework.

你将会更容易理解其他的 framework.(cocoa 只是 MacOSX 的一个开发环境,它提供基本

的开发 Max OSX 应用程序的 SDK. 当然如果你要开发某种特定的应用,你可以需要使用

Apple 提供的其他 SDK. 例如要开发播放 DVD 的程序.你可能会使用到 DVDPlayback.frame

work.它甚至是 C++接口的)

当然,你也可以创建你自己的 framework. 一个典型的例子,当相同的类被好几个应用程

序使用的时候.你可能会扔给他们一个 framework.

6- 怎样阅读这本书

当我做下来打算写这本书的时候,我在想怎么样能能让朋友们通过实践来了解学 Cocoa

编程.这本书可以算做是一本实践指导书.通常,书中让你做些什么,然后再解析答案细节

和理论基础.如果你有疑问,请再往后面阅读,通常,答案就在接下来的一两段.

如果你仍然感到有困难,你可以从这个站点找到帮助: www.bignerdranch.com/products.

在那里你可以找到修订表,提示,以及例子. 练习的答案也可以在那里找到.

每一章,我都会引导你给一个应用程序来添加一些功能.这本书不是一本菜谱(依葫芦画

瓢的意思吧).它主要是教会大家思想,所以大胆的动手吧.

Cocoa 大概定义了 300 个类.所有的这些类的说明文档都可以通过在线帮助找到( 通过

XCode 工具的 Help 菜单可以访问到,免费的噢). 可是面对这么多的类文档,如果你不对

Cocoa 的整体有个大概了解,你是很难找到怎么有效的找出你所想要的.当我在这本书介

绍到一个新的类时,请试试在在线帮助浏览它,你不一定把你浏览到的全部理解,但是至少

会加深你的认知.当你结束本书的学习后,在线帮助将成为你 Cocoa 开发的指航明灯.

Cocoa 程序开发通常是这样:常见的应该是简单的,不常见的是可能的.如果你为了一个普

Page 12: Cocoa programming for mac os

12 页

通的功能写了很多的代码,那你要考虑到你的方向存在问题了.

7- 约定

略过

8- 常见错误

很多人在都遇到过这些错误,甚至同一个错误出现过上百次:搞错大小写,忘记链接大小

写错误的存在是因为 C 和 Objective-C 语言是大小写敏感的. Foo 和 foo,对于编译器来说

不是一回事.所以在编译上遇到问题是,请注意是不是大小写写正确了.

当你创建一个应用程序时.通常是会用到 Interface Builder 来连接对象.而忘记做这些连

接,一般会是你的应用程序在运行的时候产生莫名奇妙的问题.所以如果你的应用程序反

应异常,请回过头来检查一下 Interface Builder, 是否忘记了连接对象.

当你第一次编译一个文件时,你可能很容易漏掉一些编译警告.而由于 XCode 是增量型编

译器,如果你不重新编译这个文件就看不到那些警告了.所以,当陷入到一些怪问题时,尝

试清除,重新编译,再检查那些警告.

9- 怎样学习

我教过各式各样的学生,聪明的,不聪明的.勤奋的,懒惰的.有经验的,没有经验的.但是通

过学习我的课程收获最多的同学都有一个特性,那就是专注.

第一个诀窍是足够的睡眠.我建议当你准备学习一个新的东西时,每天保证 10 个小时的

睡眠.不信你可以试试看,当你醒来时一定是精力充沛的. 咖啡是不能代替睡眠的.

第二个诀窍是要自信.很多同学经常这样想:"这个对我来说太难了吧.我是不是太笨了?".

如果有这样的想法,你就没有办法专注了.(不要害怕,Cocoa 还是比较有意思,也比较好学

的).

Rock,我以前的老板.他取得了 Cal Tech 的天体物理学位.可是在他后来的工作中从来没

有使用过相关知识.有一次我问他是否后悔去读这个学位.他说"不.事实上证明我的学位

是很有价值的,世界上确实有些很困难的事情.当我面对它们的时候可能会问'这个对我来

说太难了,我是不是不够聪明?' , 可这时候我会想起我曾经获得过 Cal Tech 的天体物理

学位,我一定不笨"

在开始前,确信虽然是有一些困难的事情,但自己一定不笨.有了坚定的信心和充沛的精

力,你就做好了征服 Cocoa 的准备!

Page 13: Cocoa programming for mac os

13 页

第二章:起步

很多的书一般会从一堆基本理论开始,但是我打算先指引你完成你的第一个 Cocoa 应用

程序.当你完成它以后,一定是即激动又困惑. 这是就是学习理论的时候了.

我们的第一个工程是一个有两个 button 的随机数获取程序.有一个文本框来显示所得到

的随机数.这个简单的例子设计了怎样获得用户输入以及怎样输出.可能在这章中的一些

讲解是简单了点,你会有很多的为什么.不过不要担心,我会在后面有很详细的说明.所以,

先让我们开始吧.

图 2.1 是程序运行起来的样子

本章内容:

XCode

Interface Builder

回到 XCode

文档

你做了什么?

XCode

装好开发工具后,你可以在/Developer/Applications/下面找到 XCode. 把它拖到 Dock 上吧,

你会经常用到它的. 启动 XCode.

前面有提到 XCode 把应用程序用到的所有资源放到一个叫 project directory 的目录下面.

Page 14: Cocoa programming for mac os

14 页

所以我们第一步就是生成这样的一个目录

新建工程

点击 File 菜单,选择 New Project... 你可以工程模板(看到图 2.2),选择我们所要创建的程

序模板: Cocoa Application.注意,这里还有很多其他的模板.

Figure 2.2. Choose Project Type

在这本书中,我们会创建以下几个类型的工程

Application: 创建窗体的程序

Tool : 没有 UI 的程序.也就是命令行后台程序

Bundle 或 framework: 可以被Application和 Tool使用的资源包. Bundle(也叫

Plug-in)在运行时动态加载.通常应用程序在编译需要连接某些 framework (比如

Cocoa.framework)

我们给我们的应用程序取个名字: RandomApp, 就像图 2.3 . 通常第一个字母大写. 你

也可以选择一个目录来放置工程目录,默认它会被放置在用户主目录下面. 点击 Finish 来

完成

Page 15: Cocoa programming for mac os

15 页

Figure 2.3. Name Project

我们就这样创建了一个工程, 其目录包含了一个程序的结构,你现在可以浏览整个工作

结构,编译源程序了.回来看看我们创建的 XCode 工程长什么样.在 XCode 的左边有一个

Outline View. Outline View 中的每一项包含了对一个程序员有用的信息. 文件, 消息(例

如编译错误,查找结果等). 我们可以开始编辑源文件了.点选 RandomApp 看看哪些文件

将被编译.

XCode 模板创建了基本的工程结构,你可以直接编译运行它了.点击工具条上一个带铁锤

的绿色按钮开始 build run 工程. 见图 2.4

Figure 2.4. Skeleton of a Project

Page 16: Cocoa programming for mac os

16 页

当我们的程序正在启动是,你可以在 Dock 上看到一个跳跃的图标. 然后程序的名字将会

出现在 Menu Bar 上.这样我们的程序就启动激活了.程序的窗口可能被别的窗口挡住, 如

果看不到我们自己的程序窗口,可以从 RandomApp Menu 上选择 Hide Others. 这样我们

就看到一个空的窗口了.就像 图 2.5

麻雀虽小,五脏俱全. 这个程序不能做更多的事,但它包含了基本的程序结构.它有窗口,有

菜单可以做相应基本的操作. 其实所有的这些功能,我们程序的代码只有一行. 让我们关

掉程序,回到 XCode 来看看.

main 函数

单选 main.m 如果你双击它. 它将会在另外一个窗口打开.因为我一天经常出来很多的

文件,所以我选择 single-window 风格. 点击工具条上的 Editor 可以劈开右边的区域得到

一个编辑区域. 代码这是就呈现在你的面前了图 2.6.(关于 XCode 开发工具,读者可以自

己先查查资料练练手.用上它后你应该会喜欢上它的.)

Page 17: Cocoa programming for mac os

17 页

Figure 2.6. main() Function

到现在,你没有修改过main.m, main函数只是简单的调用了NSApplicationMain(). 这个函

数会把用户界面对象从一个 nib 文件中加载. Nib 文件是由 Interface Builder 创建的

(NIB - NeXT Interface Builder; NS - NeXTSTEP).一旦把用户界面对象加载后,我们的程

序就处在等待用户的输入状态中.当用户作了相应事件操作,我们的代码将自己被调用.如

果之前你没有编写过这种用户界面程序,那么这是一个令您吃惊的变化:用户掌握控制权,

你所写的代码只是去响应用户的操作.

Page 18: Cocoa programming for mac os

18 页

Interface Builder

是时候看看 nib文件了,你可以在 XCode 左边Outline View 的 Resources 下面找到

MainMenu.nib . 双击它就可以叫 Interface Builder(图 2.7)来打开它.看上去会有很多的窗

口出现了.这时你可以把其他的应用程序先隐藏起来.在 Interface Builder 的菜单下,你可

以找到熟悉的 Hide Others

Figure 2.7. MainMenu.nib

我们通过 Interface Builder 来创建编辑用户界面对象(比如, 窗体和按钮)并把它们存储成

文件.我们也可以生成我们自己定义的类的对象.并且把它和那些标准提供的用户界面对

象连接起来.这时如果用户通过用户界面对象交互,那么那些连接的对象将会相应而执行

其中我们编写的代码. (解析一下: 比如我们创建一个 Foo 的类,在 Interface Builder 中实

例化一个 Foo 类的对象叫 foo. 然后把 foo 和窗体上的一个 NSButton 对象连接起来. 这

是当用户点击那个 NSButton 对象是,程序将调用到 foo 中的代码来执行想要的动作了).

The Library Window

在这里你可以找到很多用户界面对象的模板,你可以把他们拖拽到你的界面上.比如,拖一

个你想要得按钮

Page 19: Cocoa programming for mac os

19 页

The Blank Window

它代表了一个储存在你的 nib 文件中的 NSWindow 类的实例对象.当你从 The Library

Window 拖拽一些界面对象到这个窗体里时,这些界面对象将添加到你的 nib 文件中.

当你创建了用户界面对象,并设置好它们的属性. 把它们保存成 nib 文件就像是把这些

对象冷冻成一个文件.当应用程序运行读取这些 nib 文件时,就像是把这些用户界面对象

解冻使用.用官方的话说就是"对象被 Interface Builder 封装成 nib 文件.当程序运行时再把

它们解包."

布局界面

好了,我们现在要创建我们的界面了,就像图 2.8

从 Library window拖一个按钮到我们的空白窗体上(如图 2.9).(为了更好的找到按钮对象,

你可以在 Library window 的顶部选择 Cocoa->Views&Cells 组)

Figure 2.9. Dragging a Button

Page 20: Cocoa programming for mac os

20 页

双击放置好的按钮,把它的 title 修改成 Seed random number generator using time

把该按钮拷贝,再粘帖生成一个新的按钮.title 修改成 Generate random number.再从

Library window 拖一个 Label 文本框到窗体上(图 2.10)

Figure 2.10. Dragging a Text Field

为了使文本框和按钮一样宽,我么可以拖动它的左右两边来进行调整.(你可能已经注意

到当你拖动到快接近窗体的边缘时,会有一些蓝色的线条出现.这是指示是为了让你更好

得遵循 Apple GUI 风格故意设计的)

把窗体弄小点

为了让文本框居中,你需要使用到 Inspector. 选中文本框,从 Tools 菜单上选择

Attributes Inspector, 点选居中按钮(图 2.11)

Page 21: Cocoa programming for mac os

21 页

Figure 2.11. Center-Justify Text Field

提示: 我的 Inspector 窗口从早上到现在一直没有关闭过

The Doc Window

在 nib 文件中,有些对象是可见的(eg, 按钮对象), 而有些是不可见的(eg,自定义 cotroll 类

的对象). Doc Window 中就包含了这些不可见的对象.

在 Doc Window(标题为 MainMenu.nib), 你可以找到 main menu 和 window. 这两个前

面有介绍,就是程序的菜单和窗体. First Responder 是一个假定对象,我们会在 21 章来

讨论它. File's Owner 是我们会在 12 章来讨论.

创建类

对于 Objective - C, 我们使用两个文件来定义一个类 (当然你也可以把它们合在一个文

件中). 头文件和实现文件. 头文件也就是接口文件,声明了类的成员变量和方法.实现文

件定义了这些方法.

回到 XCode,选择菜单 File->New File 来创建一个新的 Cocoa->Objective-C 类. 并命名文

件为 Foo.m (图 2.12)

Page 22: Cocoa programming for mac os

22 页

文件 Foo.h 和 Foo.m 就创建出来并加入到你的工程了.你可以把它们拖拽到 Classes 组中.

如图 2.13

Figure 2.13. Put Foo.h and Foo.m in the Classes Group

现在我们添加成员变量和方法,指向其他对象的成员变量我们称之为 outlets. 可以被用

户操作触发调用的方法我们称之为 actions. 编辑 Foo.h 如下

Page 23: Cocoa programming for mac os

23 页

#import <Cocoa/Cocoa.h>

@interface Foo : NSObject {

IBOutlet NSTextField *textField;

}

-(IBAction)seed:(id)sender;

-(IBAction)generate:(id)sender;

@end

一个 Objective - C 程序员可以看出来 3 点

1. Foo 是 NSObject 的子类

2. Foo 有一个成员变量 textField. 它指向一个 NSTextField 对象

3. Foo 有两个 action 方法: seed 和 generate

在 cocoa 编程规范里, 成员变量和方法的第一个字母为小写. 如果名字由多个单词组成,

除了第一个单词外,后面的单词的首字母为大写,如 favoriteColor. 而类名字首字母为大

写 (读者应该去 ADC 上查看下 Cocoa Code Convention.好的习惯,好品质和效率)

创建对象

接下来,我们为 nib 文件创建一个 Foo 类的对象.回到 Interface Builder. 从 Library window

拖出一个蓝色的图标(Cocoa->Objects & Controllers) 到 Doc window 如图 2.14

Figure 2.14. Skeleton of a Project

Page 24: Cocoa programming for mac os

24 页

通过 Identity Inspector, 把它的类属性设置为 Foo(图 2.15).(这时,actions 和 outlets 将会出

现在 Inspector 中.如果没有,那么检查一下 Foo.h. 或还未保存 Foo.h) [可以思考下,为什么

它们就会出现了呢?]

Figure 2.15. Setting the Class

创建连接

大多数到面向对象语言多会处理对象之间的关系.现在我们就要做这件事了.对于 Cocoa

来说就是"我要设置对象的 Outlet 了". 为了让对象 A 连接到对象 B. 我们使用 Control -

drag. 从对象 A drag 到 对象 B . 图 2.16 描述了我们这个例子中的对象关系

Page 25: Cocoa programming for mac os

25 页

首先我们要把 foo 的成员变量 textField 指向 NSTextField 对象 label. 右键点击(如果你是

单键鼠标,那就 Control-click 吧或是换个双键鼠标吧)foo 对象图标,这使会出现 Inspector

面板. 从 textField 边上的圆 drag 到窗体的 Label.如图 2.17

Figure 2.17. Set the textField Outlet

这样我们就完成了让 foo 对象的 textField 指向窗体上的 NSTextField 对象了.

好了,现在我们要设置窗体上按钮对象 Seed的 target outlet. 让它指向我们自定义的类对

象 foo,来响应用户点击 seed按钮,执行 foo中定义的响应. 使用Control - drag,从按钮drag

到 foo 对象,当面板出现,选择 seed: 图 2.18

Figure 2.18. Set the Target and Action of the Seed Button

Page 26: Cocoa programming for mac os

26 页

同样的,设置好按钮 Generate,让它的 target outlet 指向 foo. 并设置其 action 为 generate:

方法. 如图 2.19

Figure 2.19. Set the Target and Action of the Generate Button

OK, Interface Builder 可以暂时休息了.让我 hide 它,回到 XCode 吧

回到 XCode

如果你是第一次看到 Objective-C 代码,你一定会不相信,它和 C++或是 java 怎么有这么大

不同.其实这些只是语法上得不同而已,基本到原理还是一样的.例如,对于 java 声明一个

import com.megacorp.Bar;

import com.megacorp.Baz;

public class Rex extends Bar implements Baz {

...methods and instance variables...

}

Rex 继承 Bar,并实现了 Bar 的某些方法.再看看 Objective-C.就是这样

#import <megacorp/Bar.h>

#import <megacorp/Baz.h>

Page 27: Cocoa programming for mac os

27 页

@interface Rex : Bar <Baz> {

...instance variables...

}

...methods...

@end

如果你熟悉 Java,Objective-C 对你来说就很简单了.不过记住一点,Objective-C 不支持多重

继承,也就是说,一个类只能有一个父类.

类型和常量

Objective-C 用到了一小部分 C 语言没有的数据类型

id : 指向一个任何类型的类对象 (有点象 void*)

BOOL: typedef 至 char. 表示一个布尔值

YES 为 1

NO 为 0

IBOutlet: 空的宏.可以忽略 (注意, Interface Builder在读取类声明的.h文件.

可以通过它来得到指示,那些成员变量是 IBOutlet)

IBAction: 与 void和 IBOutlet一样. interface builder得到指示.哪些方法是

IBAction

nil : NULL,对象指针赋值为空时使用

看看头文件

现在打开 Foo.h, 我们声明了类 Foo 的,继承至 NSObject. 如下

#import <Cocoa/Cocoa.h>

@interface Foo : NSObject

{

IBOutlet NSTextField *textField;

}

- (IBAction)generate:(id)sender;

Page 28: Cocoa programming for mac os

28 页

- (IBAction)seed:(id)sender;

@end

#import 和 #include的意义一样. 不过有点不同的是: #import能够保证头文件只包含一

次. 在这里我们包含了<Cocoa/Cocoa.h>. 它声明了 Foo 的父类 NSObject

你可能已经注意到在声明 Foo 时用到了@interface, 在 C 语言中我们没有见过符号@. 对

于 Objective-C 的关键字都是使用@开头的.比如: @end,@implementation,@class,

@selector, @protocol,@property 和@synthesize.

通常你会发现书写代码是很简单的,我们可以开启 XCode syntax-aware 选项.你只有输入

头几个字母,后面的 XCode 会自动提示产生. 在 XCode Preferences 中选择 Indentation 面.

勾选 Syntax-aware indenting.如图 2.20

Figure 2.20. Syntax-Aware Indentation Preference

Page 29: Cocoa programming for mac os

29 页

编辑定义文件

现在可以来看看 Foo.m 文件了. 它定义了类 Foo. 在 C++或是 Java.定义一个方法就像这

public void increment(Object sender) {

count++;

textField.setIntValue(count);

}

用自然语言,我们会说:"increment 是一个 public 成员函数,它接受一个对象参数.该函数没

有返回值.它把成员变量 count 加 1.并把 count 作为参数,给对象 textField 发送 setIntValue

的消息"

而在 Objective-C 中,我们会这样做

- (void)increment:(id)sender

{

count++;

[textField setIntValue:count];

}

Objective-C 是一种非常简单的语言,它没有指定可见性(public,protect,private). 所有的方

法都是 public.所有的成员变量都是 protected(实际上,它也支持.不过我们很少用它们, 默

认为 protected 已经足够了)

在第三章,我们会全面的来认识一些 Objective-C. 现在先走下去再说

#import "Foo.h"

@implementation Foo

- (IBAction)generate:(id)sender

{

// Generate a number between 1 and 100 inclusive

int generated;

generated = (random() % 100) + 1;

NSLog(@"generated = %d", generated);

// Ask the text field to change what it is displaying

[textField setIntValue:generated];

Page 30: Cocoa programming for mac os

30 页

}

- (IBAction)seed:(id)sender

{

// Seed the random number generator with the time

srandom(time(NULL));

[textField setStringValue:@"Generator seeded"];

}

@end

(注意 IBAction 等价 void.什么都没有返回)

因为 Objective-C 是 C 的扩展,所以我们可以调用 C 和 Unix 库提供的函数,如 random(),

srandom()

在你编译执行之前,你可能需要修改一下 Xcode 的预设置. 首先是 console,它可以记录程

序的错误.你可能想在每次运行前清除上次的 log. 其次,你可能经常在.h和它对应的.m之

间切换.快捷键是 Command-Option-UpArrow,你想将两个文件显示在相同的窗口中。如

图 2.21

Figure 2.21. Counterparts in Same Editor + Log Clearing

编译运行

我们完成了第一个程序,现在点击 Build and Go.(如果这个程序在之前已经运行.

Build and Go 是灰掉的,请先退出之前的程序)

如果你的程序有错误,会在右上有编译提示. 当点击这些提示时,出错的代码行会显示在

Page 31: Cocoa programming for mac os

31 页

下面.[编译器都大同小异,自己都用,摸索一下就会了] 如图 2.22

Figure 2.22. Compiling

运行你的程序,试试它的功能

恭喜!你完成了自己第一个 Cocoa 程序

你有看到 console 中的 log 信息吗?console 是我们应该经常关注的地方,因为当 cocoa 有

一些异常时,它会在 console 中记录一些信息. 在 XCode 的 Preference 设置当程序启动是

显示 console.如图 2.23

Figure 2.23. Show Console Preference

Page 32: Cocoa programming for mac os

32 页

awakeFromNib

你可能注意到程序的一点瑕疵: 当程序启动后, 我们可以让 Label 显示更有意思的东西.

比如说当前的时间.

前面有提到过,对象被冷冻在 nib 文件里面.在程序启动到接收用户事件前,这些对象会解

冻. 这个机制有点不同于编写很多代码来布局用户界面. Interface Builder 让我们编辑这

些界面对象的属性,然后保存到一个 nib 文件中.

当一个 nib 文件中对象要解冻时.它们的 awakeFromNib 方法会自动被调用[想象一下在

main函数中的 NSApplicationMain 做了什么. 加载 nib文件.遍历 nib文件中的所有对象,

调用所以对象的 awakeFromNib 方法. event loop ...] . 所以我们可以给 Foo 添加 awake

FromNib 方法来给文本框设置想要的当前时间

照着添加下面的代码.可能你会有些不解,以后会知道的.不管怎样,你创建了一个

NSCalendarDate 对象,得到当前时间.并把它设给了文本框来显示

- (void)awakeFromNib

{

NSCalendarDate *now;

now = [NSCalendarDate calendarDate];

[textField setObjectValue:now];

}

在 Foo.m 中,方法定义的前后不重要,不过记得在@implementation 和 @end 之间添加代

码中,我们没有调用 awakeFromNib.它是自动被调用的.再次编译运行,你看到你想要得了

吗? 如图 2.24

Page 33: Cocoa programming for mac os

33 页

在 Cocoa 中,有很多东西会被自动调用(eg. awakeFromNib).随着这本书的深入,你会慢慢

知道的更多.我将尽力去为你解决这些疑惑.

文档

在完成这章前.有必要看看我们可以在哪里找到帮助文档了.这也有助于你完成后面的练

习.最简单的途径就是通过 XCode 的 Help 菜单,选择 Documentation 如图 2.25

Figure 2.25. The Documentation

你也可以使用 Option-Double-Click 点击方法,类或是函数.XCode 会自动在帮助文档中查

询它们.

你做了什么?

回忆一下,你按部就班的完成了一个简单的 Cocoa 程序

创建一个新的工程

创建布局了界面

创建自己的类

把界面对象和自己创建的类连接起来

给自己的类添加了代码

编译

测试运用

Page 34: Cocoa programming for mac os

34 页

让我们来简单的讨论一下程序的运行过程: 当进程开始后,调用了 NSApplication 函数.该

函数创建了一个 NSApplication 对象 NSApp. NSApp 读取 nib 文件并把其中的对象解包(解

冻).然后给每个对象发送 awakFromNib 的消息. 然后就开始等待接受事件了. 如图 2.26

当接收到用户的键盘或鼠标事件, window server[还记得它吧]会把接收到的事件放到适

当的应用程序的事件队列中.NSApp从队列中读取事件并转发给界面对象(比如一个按钮),

这时我们自己的代码将被调用.如果我们自己的代码改变了某些 view.这些 view 将重画.

这样一个事件检查和反馈的过程就是 main event loop. 如图 2.27

Page 35: Cocoa programming for mac os

35 页

当从菜单中选择Quit. NSApp的 terminate:方法就被调用.进程结束,所有的对象将被销毁.

困惑吗?兴奋么?欲知后事如何,请听下回分解!

Page 36: Cocoa programming for mac os

36 页

第三章: Objective-C 语言

从前,一个叫 Brad Cox 的人觉得是时候让编程更模块化一些.当时 C 语言是非常流行和强大的

语言,Smalltalk 是一个优雅的面向对象语言.所以,基于C,Brad Cox增加了 Smalltalk的类和消息

发送机制, Objective-C 语言就诞生了.

Objective-C 不是私有语言.相反,它作为一个开放标准被 GNU C(gcc)所包含很多年了. Cocoa是

用 Objective-C 开发的,大部分 Cocoa 开发也是使用 Objective-C 语言.

讲解 c和面向对象基础需要用一整本书.相对于写一本书,这一章会简单介绍Objective-C 语言.

如果你有一定的 C 和面向对象基础,学习起来不会很难.我也建议你学习 apple 的 Objective-C

Language 这本书.

本章内容:

创建,使用对象

使用已有的类

创建自己的类

调试器

你做了什么

思考: 消息机制工作原理是什么?

挑战

Page 37: Cocoa programming for mac os

37 页

创建,使用对象

在第一章有提到:类是用来创建对象的,对象有自己的方法.而你可以给对象发送消息,让它执

行特定的方法.这一节中,我们就来学习怎么创建对象,并给它们发送消息,在不再需要使用它

们时怎么释放它们.

我们以 NSMutableArray 作为例子.你可以创建一个 NSMutableArray 的对象,并发送消息 alloc

给 NSMutableArray 类. 就像这样 [可能你有疑问: 我到底可以给谁发送消息啊? 这里好像是

给类 NSMutableArray 发送 alloc 消息.这是怎么回事?其实你可以把 NSMutableArray 类当作一

个对象看待.Objective-C Language 有比较好得简介.有机会计划把它也给翻译了]

[NSMutableArray alloc];

该方法返回一个指针,指向为一个对象分配好的空间.你可以使用一个变量来表示它,如

NSMutableArray *foo;

foo = [NSMutableArray alloc];

面对 Objective-C 时,记住,foo 只是一个指针变量,在这里它指向一个对象[好好琢磨一下这句

话]。在使用 foo 指向的对象之前,我们需要对它进行初始化. 发送 init 消息来完成

NSMutableArray *foo;

foo = [NSMutableArray alloc];

[foo init];

好好琢磨一下最后一行代码. 发送 init 消息给 foo 所指向的对象.也就是"foo 是消息 init 的接

收者". 我们也可以给一个类发送消息.就像给 NSMutableArray 发送 alloc 一样.

init 发送会返回一个新的初始化对象,我们经常会这要写:

NSMutableArray *foo;

foo = [[NSMutableArray alloc] init];

怎么释放对象呢?在第四章我们会介绍(包含所有和 NSAutoreleasePool 的知识)

有些方法可能接受参数. 这是方法的名字(也叫 selector)将以:号结束. 例如,给 array 的最后添

加一个对象我们使用 addObject: 方法(假定 bar 是一个对象指针)

Page 38: Cocoa programming for mac os

38 页

[foo addObject:bar];

如果接受多个参数,那么 selector 由几个部分组成.例如,在特定的索引处添加对象:

[foo insertObject:bar atIndex:5];

请注意 intsertObject:atIndex 是一个 selector.它会触发有两个输入参数的方法.你可能觉得

这和 C 或是 Java 太不一样了.但是它是 Smalltalk 风格.这样的语法使你更容易理解和阅读.例

如,我们看看一个 C++的调用

if (x.intersectsArc(35.0, 19.0, 23.0, 90.0, 120.0))

相比较而言,Objective-C 更容易猜到函数的意义

if ([x intersectsArcWithRadius:35.0

centeredAtX:19.0

Y:23.0

fromAngle:90.0

toAngle:120.0])

如果你刚开始觉得别扭,不奇怪,先试着用用.大多数的程序员会喜欢上这种语法风格[我也

是]。现在你大概可以理解一些 Objective-C 代码了.让我们来编写一个程序来创建一个

NSMutableArray 对象.并给它添加 10 个 NSNumber 对象吧

Page 39: Cocoa programming for mac os

39 页

使用已有的类

请再次打开 XCode,关掉之前的工程. 在 File 菜单下选择 New Project... 在弹出的面板中选择

Foundation Tool (图 3.1)

Figure 3.1. Choose Project Type

说明一些 Foundation Tool. 这个工程模板创建没有用户界面的程序.在后台无用户交互的运

行.也就是 Command Line 程序.我们通常需要修改它的 main 函数.

把工程命名为 lottery.不像其他应用程序,Command Line Tool 一般以小写字母命名.如图 3.2

Figure 3.2. Name Project

Page 40: Cocoa programming for mac os

40 页

在 Source 下面选择 lottery.m. 如下编辑 lottery.m

#import <Foundation/Foundation.h>

int main (int argc, const char * argv[])

{

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

NSMutableArray *array;

array = [[NSMutableArray alloc] init];

int i;

for (i = 0; i < 10; i++) {

NSNumber *newNumber = [[NSNumber alloc] initWithInt:(i * 3)];

[array addObject:newNumber];

}

for ( i = 0; i < 10; i++) {

NSNumber *numberToPrint = [array objectAtIndex:i];

NSLog(@"The number at index %d is %@", i, numberToPrint);

}

[pool drain];

return 0;

}

让我们逐行逐句解析一下

#import <Foundation/Foundation.h>

包含了整个 Foundation Framework[还记得它吧.Cocoa 3 个 framework 之一.]

int main (int argc, const char *argv[])

象你看到的其他 Unix C 程序一样

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

声明了指针变量 pool.指向一个新创建的 NSAutoreleasePool 对象.我们会在下一章详细讨论

autorelease pool

NSMutableArray *array;

声明了一个指针变量 array. 指向一个 NSmutableArray 对象. 注意这是还没有任何 array 对象

存在.你只是声明了一个将会指向 NSmutableArray 对象的指针

array = [[NSMutableArray alloc] init];

Page 41: Cocoa programming for mac os

41 页

创建了一个 NSmutableArray 对象,并让 array 指向它.

for (i = 0; i < 10; i++) {

NSNumber *newNumber = [[NSNumber alloc] initWithInt:(i*3)];

[array addObject:newNumber];

}

在 for 循环中,创建了临时指针变量 newNumber,并指向新创建的 NSNumber 对象. 同时把

NSNumber 对象添加到 array 中去.

这个 array 并没有拷贝这些 NSNumber 对象.它只是保留了这些 NSNumber 对象的指针.

Objective-C 程序员很少做对象拷贝,因为很少有这种需要. [可以看看 Objective- C 的内存管理

机制,后面有介绍为什么]

for ( i = 0; i < 10; i++) {

NSNumber *numberToPrint = [array objectAtIndex:i];

NSLog(@"The number at index %d is %@", i, numberToPrint);

}

这里会把 array 中的 NSNumber 打印输出到 console. NSLog 类似 C 语言中的 printf()函数.并且

会在开头打印出应用程序的名字和当前时间.

对于 printf,你可能会用%x去让一个整型数打印成 16 进制格式. NSLog 函数同样也支持. 并且

它还可以支持%@来打印一个对象类型. 当使用%@时,它会给对象发送消息 description,

description 会返回一个 string 来代替%@.我们马上就会讨论到 description 方法

表 3.1 是 NSLog()支持的符号(token)

Page 42: Cocoa programming for mac os

42 页

注意: 如果"The number at index %d is %@"之前的@看起来是有点奇怪.请记住,Objective-C

是C语言的扩展.其中一个扩展就是对Objective-C的字符串可以是NSString的对象.对于C, 字

符串就是存储了字符的以 null字符结束的连续内存块. 区分 C字符窜常量和Objective-C 字符

串常量,就是看前面有没有@. 如

// C string

char *foo;

// NSString

NSString *bar;

foo = "this is a C string";

bar = @"this is an NSString";

你应该会经常用到 NSString. 当然如果你有很多 c 函数使用到 c 字符串也是可以的.

你可以把这两种字符串进行转换

const char *foo = "Blah blah";

NSString *bar;

// Create an NSString from a C string

bar = [NSString stringWithUTF8String:foo];

// Create a C string from an NSString

foo = [bar UTF8String];

因为 NSString 可以是 Unicode 的字符串.所以你要正确的处理 C 字符串的多字节的字符,通常

这有些难且废时.(处理多字节字符问题.你可能还会遇到有些语言是从右向左读这样的问题)

不管怎么样,尽量用 NSString 代替 C 字符串

我们继续看代码

[pool drain];

return 0;

}

这里的 autorelease pool,我们将在下一章讨论.

在工具条上,你可以找到一个 pop-up 菜单: Active Build Configuration. 它有两个选项: Debug

和 Release.当你在开发阶段,你应该会使用 Debug. 如果要发布你的程序,你将会有 Release.

它们有什么区别么? Release 会创建 UB,并且去掉了 debug 所用的 symbol.所以,Release 设置大

概需要 2 倍时间来编译,而且不能做 debug 了. [为了完成书中的例子,这里只是对 XCode 做了

Page 43: Cocoa programming for mac os

43 页

一些简单的介绍.读者应该找 XCode 帮助,详细了解 XCode 开发工具]

点击 Build and Go (图 3.3)

Figure 3.3. Completed Execution

(如果你没有看到的 console, 点击 Run->Console 菜单)

给 nil (空对象)发送消息

对于很多的面向对象语言,如果你给 nil 发送消息会导致程序的崩溃.使用那些语言你可能会

用到很多的非空检查. 例如在 Java 中看到

if (foo != null) {

foo.doThatThingYouDo();

}

在Objective-C中.给 nil发送消息不会有什么问题. 这个消息被忽略而已.这样就消除了太多的

非空检查.例如下面的代码可以正常的编译运行

id foo;

foo = nil;

int bar = [foo count];

这虽然和大多数语言不同.不过我们还是会这样使用

NSObject, NSArray, NSMutableArray, NSString

不错,到现在我们用到了标准的 cocoa 对象:NSObject,NSMutableArray,NSString.(Cocoa 所有的

Page 44: Cocoa programming for mac os

44 页

类都有 NS 的前缀,建议你自己定义类时不要使用 NS 的前缀了.)这些类都包含在 Foundation

framework 里面. 图 3.4 展示了它们的继承关系.

现在让我们一起来看看这些类中常用到的方法吧. 你可以在 XCode help 中的在线帮助文档

里找到完整的函数说明

NSObject

NSObject 是所有 Objective-C 类的根类.以下是它的常用方法的说明

- (id)init

在分配内存空间后对 receiver 进行初始化. init 一般和 alloc 方法写在一起. 如:

TheClass *newObject = [[TheClass alloc] init];

- (NSString *)description

返回一个NSString对象,描述 receiver.如果你在debugger上(gdb)上使用"po" (print object),就会

调用这个方法.如果设计好 description 方法,可以更容易的来做 debug. 同样,你使用%@,也会

调用该方法.如之前在 main 函数中看到的:

Page 45: Cocoa programming for mac os

45 页

NSLog(@"The number at index %d is %@", i, numberToPrint);

它等价于:

NSLog(@"The number at index %d is %@", i,

[numberToPrint description]);

- (BOOL)isEqual:(id)anObject

当 receiver 和 anObject 相等时返回 YES.否则返回 NO. 一般你会这样用:

if ([myObject isEqual:anotherObject]) {

NSLog(@"They are equal.");

}

这里有个疑问了?到底什么是相等呢?对于 NSObject 来说, 这个方法定义只在 receiver 和

anObject 为同一个对象时相等.也就是它俩指向同一个内存地址.

当然,并不是所有的类都想定义这样的相等.所以很多的类会重载这个方法来实现自己的相等

策略.比如:NSString 就重载了这个方法,比较 receiver 和 anObject 的字符是否相等. 如果它们

的字符和字符排列顺序都相等时返回 YES.

所以,对于两个 NSString 对象 x 和 y.下面两个表达式是有不同意义的

x == y

[x isEqual:y]

第一个表达式是比较两个指针是否相等.而第二个是比较两个指针所指向的 NSString 对象是

否相等. 如果 x 和 y 都是没有重载 NSObject isEqual:方法类的实例,这两个表达式的结果是一

样的.

NSArray

一个 NSArray 对象包含指向其他对象的指针列表. 所有指针有一个唯一的 index. 例如, 有 n

个对象指针,那么 index 从 0 - (n-1).你不能把 nil 增加到 NSArray 中(这就意味这 NSArray 中没有

"空洞",这和 Java 的 Object[]不一样). NSArray 从 NSObject 继承而来

Page 46: Cocoa programming for mac os

46 页

NSArray 在创建的时候,就包含了所有对象.你不能增加或是删除其中任何一个对象.这种特定

称为: immutable(NSArray 的子类 NSMutableArray 是 mutable 的.我们稍后讨论.)这种不能改变

的对象,在某些情况下是非常有用的: 很多对象可以共享一个 NSArray,而不会担心谁会修改

了这个共享 NSArray 对象. NSString 和 NSNumber 也是这样的 immutable 对象. 如果你想要一

个新的 string 或 number,不用修改原来的 NSString 和 NSNumber.直接重新创建新的 NSString

和 NSNumber 就好了(NSString 有对应的 mutable 类,NSMutableString 来允许修改其中的字符)

看看 NSArray 常用的方法:

- (unsigned)count

得到 array 中的对象个数

-(id)objectAtIndex:(unsigned)i

得到索引为 i 的对象.如果 i 值超过了 array 对象数量,在程序运行到这里会产生错误

-(id)lastObject

得到最后一个对象.如果 NSArray 中没有任何对象存在,返回 nil.

-(BOOL)containsObject:(id)anObject

当 anObject 出现在 NSArray 中,则返回 YES. 对于出现的定义是这样的: NSArray 会调用对象的

isEqual:方法,并把 anObject 当成参数. 如果 isEqual:返回 YES,那么说明 anObject 出现.

-(unsigned)indexOfObject:(id)anObject

查找 NSArray 中是否存在 anObject, 并返回最小的索引值. 同样会调用对象的 isEqual:方法作

为比较函数.如果没有找到则会返回 NSNotFound. [一个 NSArray 可能有 3 个 NSString 对象. 而

所有的 NSString 都是@"hello" , 这是调用 indexOfObject:@"hello" , 那么会找到 3 个,返回的

会是最小的索引 0]

NSMutableArray

NSMutableArray继承NSArray,扩展了增加,删除对象的功能. 可以使用NSArray的mutableCopy

方法来复制得到一个可修改的 NSMutableArray 对象.

看看它常用的方法

- (void)addObject:(id)anObject

在 reciever 最后添加 anObject. 添加 nil 是非法的

Page 47: Cocoa programming for mac os

47 页

- (void)addObjectsFromArray:(NSArray *)otherArray

在 reciever 最后,把 otherArray 中的对象都依次添加进去.

- (void)insertObject:(id)anObject atIndex:(unsigned)index

在索引 index 处插入对象 anObject. 如果 index 被占用,会把之后的 object 向后移动. index 不

能大约所包含对象个数,并且 anObject 不能为空.

- (void)removeAllObjects

清空 array.

- (void)removeObject:(id)anObject

删除所有和 anObject 相等的对象.同样使用 isEqual:作为相等比较方法.

- (void)removeObjectAtIndex:(unsigned)index

删除索引为 index 的对象.后面的对象依次往前移.如果 index 越界,将会产生错误

如上所说,我们不能把 nil 加到 array 中. 但是有的时候我们真的想给 array 加一个空的对象.

这时可以使用 NSNull 来做这件事.如:

[myArray addObject:[NSNull null]];

NSString

一个 NSString 对象可以存储一段 Unicode 字符.在 Cocoa 中.所有和字符,字串相关的处理都是

使用 NSString 来完成.而 Objective-C 也支持@"...."这样的格式来定义 7-bit ASCII 编码的字符

串常量

NSString *temp = @"this is a constant string";

NSString 继承至 NSObject.我们来看看它的一些常用方法

-(id)initWithFormat:(NSString *)format, ...

就像 sprintf. format 由很多记号组成.比如%d. 其他参数就是这些记号的替代:

int x = 5;

Page 48: Cocoa programming for mac os

48 页

char *y = "abc";

id z = @"123";

NSString *aString = [[NSString alloc] initWithFormat:

@"The int %d, the C String %s, and the NSString %@", x, y, z];

- (unsigned int)length

返回字符个数

- (NSString *)stringByAppendingString:(NSString *)aString

给一个字符串附加一个字符串 aString.如下面的代码,产生字符串:"Error:unable to read file"

NSString *errorTag = @"Error: ";

NSString *errorString = @"unable to read file.";

NSString *errorMessage;

errorMessage = [errorTag stringByAppendingString:errorString];

继承和组合

新手的 cocoa 程序员经常愿意创建 NSString,NSMutableArray 创建自己的子类.不要这样做!现

在的 Objective-C 程序员基本从来不这样做.他们会让一个较大的对象来包含 NString 或

NSMutableArray,也叫做对象组合. 举个例子: BankAccount可能会声明成NSMutableArray的子

类.毕竟, 银行帐号不就是一些交易处理的集合吗?新手很容易会这样做. 而大牛们通常会让

BankAccount 继承 NSObject, 并且声明一个成员变量 transactions 来指向一个 NSMutableArray

对象.

明白 有一个 和 是一个 两者之间的区别很重要 . 新人经常会说"BankAccount 是继承

NSMutableArray". 而大牛会说"BankAccount 使用到了 NSMutableArray".Objective-C 基本编

程思想是:通常使用 有一个,而不是 是一个

你会发现,使用一个类往往比继承一个类简单的多. 继承类需要编写更多的代码,也要比较深

入了解父类. 但是使用组合,Cocoa 开发者可以应用到很多强大的类,而不需要知道那些类的

实现细节.

对于强制类型语言,比如 C++. 继承是非常重要的. 而对于非强制类型语言,如Objective-C. 继

承就不是那么重要了.在本书中只有两处会出现继承关系.而大部分都是写对象包含组合关系.

认清这点,对于一个 Cocoa 程序员尤其重要.

Page 49: Cocoa programming for mac os

49 页

创建自己的类

在我生活的城市,政府出台了彩票政策,容许大家每周都可以购买彩票.想象一下,我们要编写

一个程序来生成彩票.每张彩票有两个数字,1 到 100 之间. 我们需要为下十个星期制作彩票.

每个 LotteryEntry 对象有时间和两个数字.如图 3.5.

Figure 3.5. Completed Program

创建 LotteryEntry 类

首先创建文件.在File菜单上选择New file... 选择Objective-C class. 文件命名为 LotteryEntry.m

(图 3.6)

Figure 3.6. Name File

Page 50: Cocoa programming for mac os

50 页

注意,LotteryEntry.h 也同时被创建了

LotteryEntry.h

编辑 LotteryEntry.h 如下:

#import <Foundation/Foundation.h>

@interface LotteryEntry : NSObject {

NSCalendarDate *entryDate;

int firstNumber;

int secondNumber;

}

- (void)prepareRandomNumbers;

- (void)setEntryDate:(NSCalendarDate *)date;

- (NSCalendarDate *)entryDate;

- (int)firstNumber;

- (int)secondNumber;

@end

在 LotteryEntry.h 中我们声明了一个新类 LotteryEntry. 它继承至 NSObject. 包含 3 个成员变

量 entryDate, firstNumber,和 secondNumber.

entryDate 的类型是 NSCalendarDate

firstNumber, secondNumber 的类型是 int.

同时我们也定义了 5 个方法

prepareRandomNumbers : 为 firstNumber 和 secondNumber 设置 1 到 100 的随机数.

不接受参数,也没有返回值

entryDate 和 setEntryDate: 容许其他的对象访问变量 entryDate. 方法 entryDate 返

回变量 entryDate 的值. setEntryDate 设置变量 entryDate 的值.象这样的读取和设置

方法我们称之为 accessor methods

你也定义了读取 irstNumber 和 secondNumber 的 accessor method . (在这里你没有

定义设置他们的 accessor method . 我们通过 prepareRandomNumbers 来设置他们)

LotteryEntry.m

编辑 LotteryEntry.m 如下:

#import "LotteryEntry.h"

@implementation LotteryEntry

Page 51: Cocoa programming for mac os

51 页

- (void)prepareRandomNumbers

{

firstNumber = random() % 100 + 1;

secondNumber = random() % 100 + 1;

}

- (void)setEntryDate:(NSCalendarDate *)date

{

entryDate = date;

}

- (NSCalendarDate *)entryDate

{

return entryDate;

}

- (int)firstNumber

{

return firstNumber;

}

- (int)secondNumber

{

return secondNumber;

}

@end

我们详细看看它的方法

prepareRandomNumbers: 使用 random 函数产生 1-100 的随机数.

setEntryDate: 给 entryDate 设置新值

entryDate,firstNumber,secondNumber:返回相应变量的值

修改 lottery.m

现在让我们看看 lottery.m . 我们来做一些小得改变. 最主要的变化就是使用 LotteryEntry 对

象来替换 NSNumber 对象

注意加重字体代码

#import <Foundation/Foundation.h>

#import "LotteryEntry.h"

int main (int argc, const char *argv[]) {

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// Create the date object

Page 52: Cocoa programming for mac os

52 页

NSCalendarDate *now = [[NSCalendarDate alloc] init];

// Seed the random number generator

srandom(time(NULL));

NSMutableArray *array;

array = [[NSMutableArray alloc] init];

int i;

for (i = 0; i < 10; i++){

// Create a date/time object that is 'i' weeks from now

NSCalendarDate *iWeeksFromNow;

iWeeksFromNow = [now dateByAddingYears:0

months:0

days:(i * 7)

hours:0

minutes:0

seconds:0];

// Create a new instance of LotteryEntry

LotteryEntry *newEntry = [[LotteryEntry alloc] init];

[newEntry prepareRandomNumbers];

[newEntry setEntryDate:iWeeksFromNow];

// Add the LotteryEntry object to the array

[array addObject:newEntry];

}

for (LotteryEntry *entryToPrint in array) {

// Display its contents

NSLog(@"%@", entryToPrint);

}

[pool drain];

return 0;

}

注意第二个 for 循环. 我们使用到了 Objective-C 的集合列举机制

这个程序创建了 LotteryEntry 对象的序列. 如图 3.7

Page 53: Cocoa programming for mac os

53 页

实现 description 方法

编译运行我们的程序.你将看到如图 3.8 的信息

Figure 3.8. Completed Execution

阿噢,好像不是我们想要看到的. 它应该展示的是那一天彩票开出了哪两个号码才对.(你看

到的是定义在 NSObject 中默认的 descriptiong 函数的输出). 现在我们来让 LotteryEntry 对象

能更友好的描述自己

给 LotteryEntry.m 添加 description 方法

- (NSString *)description

{

NSString *result;

result = [[NSString alloc] initWithFormat:@"%@ = %d and %d",

Page 54: Cocoa programming for mac os

54 页

[entryDate descriptionWithCalendarFormat:@"%b %d %Y"],

firstNumber, secondNumber];

return result;

}

编译运行程序.你就可以看到如图 3.9

Figure 3.9. Execution with description

NSCalendarDate

在学习其他的之前,让我们来看看 NSCalendarDate. NSCalendarDate 对象包含了日期和时间,

时区以及一个带有格式的字符串. 它从 NSDate 继承而来

NSCalendarDate对象基本上是 immutable的: 一旦一个日历日期对象被创建,你无法修改其中

的时间和日期. 当然你可以修改那个带格式的字符串和时区. 因为这样, NSCalendarDate 对

象经常被多个对象共享. 创建一个 NSCalendarDate 对象复本的情况很少.

以下是它的常用方法:

+ (id)calendarDate

该方法创建了当前日期和时间以及默认格式的 NSCalendarDate 对象.它的时区为机器设置好

的时区.该方法为 class method. 在.h文件, .m文件或是文档中要识别那些方法为 class method,

只有看是不是以+开头的.

class method 是通过给 class 发送消息来调用的.(不是给对象) 如

NSCalendarDate *now;

Page 55: Cocoa programming for mac os

55 页

now = [NSCalendarDate calendarDate];

+ (id)dateWithYear:(int)year

month:(unsigned)month

day:(unsigned)day

hour:(unsigned)hour

minute:(unsigned)minute

second:(unsigned)second

timeZone:(NSTimeZone *)aTimeZone

这个 class method 创建了一个 autorelease 对象. 该对象使用了参数指定的值来初始化. year

使用公元记年比如 2009. month 为 1 到 12. day 为 1 到 31.hour 为 0 到 23.minute 为 0 到

59.second 为 0 到 59. 下面的代码用 2009 年 8 月 3 日, 下午 4 点.太平洋时区来创建

NSCalendarDate 对象

NSTimeZone *pacific = [NSTimeZone timeZoneWithName:@"PST"]

NSCalendarDate *hotTime = [NSCalendarDate dateWithYear:2009

month:8

day:3

hour:16

minute:0

second:0

timeZone:pacific];

- (NSCalendarDate *)dateByAddingYears:(int)year

months:(int)month

days:(int)day

hours:(int)hour

minutes:(int)minute

seconds:(int)second

得到比自己(NSCalendarDate 对象)时间相差"多少"的时间. 多少由参数给定. 正数会得到以

后时间,负数得到过去时间. 我们在 lottery.m 中使用过这个方法. 例如, 得到比 hotTime 晚 6

个月的时间:

NSCalendarDate *coldTime = [hotTime dateByAddingYears:0

months:6

days:0

hours:0

minutes:0

seconds:0];

- (int)dayOfCommonEra

得到从公元 1 年算起,有多少天

Page 56: Cocoa programming for mac os

56 页

- (int)dayOfMonth

返回是月的第几天 (1 - 31)

- (int)dayOfWeek

返回是周的第几天 (0 - 6)

- (int)dayOfYear

返回是年的第几天 (1 - 366)

- (int)hourOfDay

返回是日的第几个小时 (0 - 23)

- (void)setCalendarFormat:(NSString *)format

设置显示日期的格式. 格式是由一些 日期 格式组成的字符串. 如下表 3.2

Page 57: Cocoa programming for mac os

57 页

- (NSDate *)laterDate:(NSDate *)anotherDate

这个方法继承至 NSDate. 把 receiver 与 anotherDate 做比较.返回较晚的日期

- (NSTimeInterval)timeIntervalSinceDate:(NSDate *)anotherDate

计算 receiver 与 anotherDate 之间的时间差, 返回以秒计算. 如果 receiver 早于 anotherDate,

返回为负数.NSTimeInterval 等同 double

编写 Initializers (初始化器)

还记得 main函数中的这两行代码吧

newEntry = [[LotteryEntry alloc] init];

[newEntry prepareRandomNumbers];

你创建了一个新的对象,并且立即调用了 prepareRandomNumbers方法来设置 firstNumber

和 secondNumber的值. 这些可以通过创建 initalizer 来实现. 我们接下来在

LotteryEntry重载 init方法.

在 LotteryEntry.m文件中, 把 prepareRandomNumbers 移动到 init方法中

- (id)init

{

[super init];

firstNumber = random() % 100 + 1;

secondNumber = random() % 100 + 1;

return self;

}

init方法开始调用了父类的 initalizer , 然后初始化了自己的变量, 再返回 self -- 指

向对象自己的指针(相同于 C++ 和 java中的 this 指针)

现在我们在.h和.m文件中可以删除 prepareRandomNumbers了. 编译运行我们的程序,它依

然运行正常了

由于在 Cocoa中存在某些类的 initializer在某些情况下会返回 nil. 所以程序员担心自己

类的父类就是这些会返回 nil的类.他们往往这样写

- (id)init

{

if (![super init])

return nil;

firstNumber = random() % 100 + 1];

Page 58: Cocoa programming for mac os

58 页

secondNumber = random() % 100 + 1];

return self;

}

这样一来就相当安全了.不过,在这本书中,你不会遇到这样的类.所以,书中有些地方都省略

了这样的检查

带参数的 Initializer

再回到 lottery.m文件

LotteryEntry *newEntry = [[LotteryEntry alloc] init];

[newEntry setEntryDate:iWeeksFromNow];

如果你让 LotteryEntry的 initializer能够接受 date作为参数来初始化应该会更好些.

LotterEntry *newEntry = [[LotteryEntry alloc]

initWithEntryDate:iWeeksFromNow];

接下来在 LotteryEntry.h中声明该方法

- (id)initWithEntryDate:(NSCalendarDate *)theDate;

同时在 LotteryEntry.m中修改(重新命名) init 方法:

- (id)initWithEntryDate:(NSCalendarDate *)theDate

{

if (![super init])

return nil;

entryDate = theDate;

firstNumber = random() % 100 + 1;

secondNumber = random() % 100 + 1;

return self;

}

编译运行程序,它仍然工作正常.

Page 59: Cocoa programming for mac os

59 页

不过, 我们的类 LotteryEntry可能产生问题. 当你的同事小明想在他自己的程序中使用

LotteryEntry,不幸的是他没有调用方法 initWithEntryDate:. 如下面代码:

NSCalendarDate *today = [NSCalendarDate calendarDate];

LotteryEntry *bigWin = [[LotteryEntry alloc] init];

[bigWin setEntryDate:today];

上面的代码不会产生错误. 但是在 LotteryEntry类中没有定义 init方法,所以调用会一直

往上查找到 NSObject (父类)的 init来执行. 这样就产生问题: firstNumber 和

secondNumber没有初始化值--都是 0

为了避免这样的问题产生.我们可以重载 init方法,用一个默认的 date来调用

initializer-initWithEntryDate:

- (id)init

{

return [self initWithEntryDate:[NSCalendarDate

calendarDate]];

}

注意, 真正做初始化动作的仍然是 initWithEntryDate: 方法. 一个类可以有多个

initializer[比如 LotteryEntry 有两个: init 和 initWithEntryDate], 我们把那个正在

做初始化动作的方法叫 designated initializer, 如果一个类有几个 initializer. 那么

一般 designated initializer 包含所以的参数. 如 NSOject的 designated initializer

是 init .[对于初始化, 这是 Cocoa建议的规则, Cocoa自己的类都是以这样的方式来编写

initializer方法的]

创建 initializer 的规范

如果父类的 initializer足够使用,不要去创建自己的 initializer了

如果你要创建自己的 initializer. 一定要重载父类的 designated initializer

如果你创建了多个 initializer, 让其中一个做真正的初始化工作 (designated

initializer) . 其他的都是来调用它

你的 designated initializer要调用父类的 designated initializer.

有时候你一定会碰到这种情况. 你的类初始必须输入参数. 那你可以重载父类的

designated initializer来抛出异常:

Page 60: Cocoa programming for mac os

60 页

- (id)init

{

[self dealloc];

@throw [NSException exceptionWithName:@"BNRBadInitCall"

reason:@"Initialize Lawsuit with initWithDefendant:"

userInfo:nil];

return nil;

}

调试器

Apple的开发工具包含了 gcc和 gdb. 并且 Apple 对它们作了一些有用的提升. 这一节我们

来了解怎么设置断点.访问 debugger,查看变量的值...

当我浏览代码时,在编辑窗口的左边有一条灰色的边框,如果在那个边框上点击鼠标,将给这

行代码上设置一个断点. 如图 3.10, 我们在[array addObject:newEntry];行上设置了断点

Figure 3.10. Creating a Breakpoint

这时我编译运行程序, 那么程序会在 debugger中运行. 试试吧. debugger将会运行到你设

置的断点处如图 3.11

Page 61: Cocoa programming for mac os

61 页

Figure 3.11. Stopped at Breakpoint

左边的列表你可以看到 stack信息.应该我们把断点设置在 main函数中,所以 stack不是很

深. 在右边的 outline view 的是变量以及变量的值.这时可以看到 i的值为 0.

在 stack信息列表的上面.我们可以找到 pausing, continuing,stepping over, into,out

等按钮. 点击 continue按钮可以执行到下一个断点, 而点击 step over用来逐行执行代码.

gdb调试器特别设计成 Unix风格. 可以点击 Console 来运行终端形式的 gdb.

在 Console中你可以使用所有 gdb的功能. 一个非常便利的功能是输入 "print object"

(po). 如果一个变量是指向一个对象的指针.使用 po将个这个对象发送 description 消息,

并将结果打印到 console中.我们来试试打印 newEntry 的值

po newEntry

我们可以看到 description 的结果了.如图 3.12

Figure 3.12. Using the gdb Console

Page 62: Cocoa programming for mac os

62 页

当程序出现运行错误是,将有异常抛出. 为了让 debugger在异常抛出时停止.我们可以设置

symbolic breakpoint. symbolic breakpoint没有对应到你自己代码的某行.

在这里,我们需要为 objc_exception_throw设置断点 [objc_exception_throw 由 cocoa定

义.你可以输入 malloc 来给 malloc来设置断点.程序运行到所有的 malloc时都会停下来] .

选择 Run->show->Breakpoints.把其他的断点先都禁用. 双击 Double-click指示,输入

objc_exception_throw.如图 3.13

Figure 3.13. Adding a Symbolic Breakpoint

我们可以通过对 array 做越界操作来测试上面设置的 symbol断点. 如下:

array = [[NSMutableArray alloc] init];

NSLog(@"first item = %@", [array objectAtIndex:0]);

重新编译运行,程序会停在异常抛出的地方.

我们还会经常是用到宏 NSAssert()来让程序抛出.例如在 initWithEntryDate:中,你可能希

望当参数为 nil时程序抛出异常. 我们使用 NSAssert():

- (id)initWithEntryDate:(NSCalendarDate *)theDate

{

if (![super init])

return nil;

NSAssert(theDate != nil, @"Argument must be non-nil");

entryDate = theDate;

firstNumber = random() % 100 + 1;

secondNumber = random() % 100 + 1;

return self;

}

编译运行. 没有问题,也没有异常被抛出. 为了验证,我们来修改一下代码. 非空时抛出异

NSAssert(theDate == nil, @"Argument must be non-nil");

Page 63: Cocoa programming for mac os

63 页

现在编译运行. 我们可以看到带有类,方法名字的异常信息了. 有效的使用 NSAssert(),可

以帮助我们更容易的找到 bug.

你可能在某些情况下不想让 assert 调用执行. 大部分工程都会有两个不同的编译配置:

debug 和 release. 在 debug版本中,你希望所有的 assert都被调用. 而在 release版本中

你不希望被调用. 我们来看看怎么实现. 双击 lottery target 打开编译配置信息. 在

Build 页面上选择 Release 配置. 在 GCC Preprocessing下面,增加 Preprocessor Macros

为 NS_BLOCK_ASSERTIONS.如图 3.14

现在你编译运行 release配置的工程. 我们编写的 assert没有被调用了.(记得把

NSAssert()改成正确的检查,保证参数不为 nil)

NSAssert()是对于 Objective-C而言.如果你要检查 C. 可以使用 NSCAssert().

以上介绍的应该足够让你开始学会使用 debugger了. 想要更深了解,可以访问 gcc ,gdb在

线帮助 www.gnu.org.

你做了什么?

我们用 Objective-C创建了一个简单的程序.在 main 函数中生成了几个对象.创建了一个类

LotteryEntry.并在 console 打印了一些信息.

Page 64: Cocoa programming for mac os

64 页

现在你已经基本了解了 Objective-C. 它不是一个复杂的语言.剩下的章节,我们重点放在

cocoa framework中.你将创建事件驱动程序,而不是命令行程序.

思考: 消息机制工作原理是什么?

[当你调用一个对象的方法时, 程序是怎么去查询并执行的?]

我们在之前有提到,一个类就像一个 C结构.NSObject 声明了一个成员变量: isa. 由于

NSObject是所有类的根类,所以所有的对象都会有一个isa的成员变量[公共继承].而该isa

变量指向该对象的类(图 3.15) [类在 Objective-C中也是一个实体, 由于存在 Objective-C

运行环境所有的类将有自己的存储空间.Objective-C运行环境将为每个类分配空间. 这里

所说的 isa,正是指向这样一个类的空间. 从而建立类和对象之间的对应关系.] 类空间

包含了该类定义的成员变量,以及方法实现, 还包含了指向自己父类空间的指针.

Figure 3.15. Each Object Has a Pointer to Its Class

方法以 selector作为索引. selector的数据类型是 SEL. 虽然 SEL定义成 char*, 我们可

以把它想象成 int. 每个方法的名字对应一个唯一的 int值.比如, 方法 addObject: 可能

对应的是 12. 当寻找该方法是, 使用的是 selector,而不是名字 @"addObject:"

Objective-C数据结构中,存在一个 name - selector 的映射表如图 3.16

Page 65: Cocoa programming for mac os

65 页

在编译的时候, 只要有方法的调用, 编译器都会通过 selector来查找,所以 (假设

addObject的 selector为 12)

[myObject addObject:yourObject];

将会编译变成

objc_msgSend(myObject, 12, yourObject);

这里,objec_msgSend()函数将会使用 myObjec的 isa 指针来找到 myObject的类空间结构并

在类空间结构中查找 selector 12 所对应的方法.如果没有找到,那么将使用指向父类的指

针找到父类空间结构进行 selector 12 的查找. 如果仍然没有找到,就继续往父类的父类一

直找,直到找到为止, 如果到了根类 NSObject中仍然找不到,将会抛出异常.

我们可以看到, 这是一个很动态的查找过程.类的结构可以在运行的时候改变,这样可以很

容易来进行功能扩展[Objective-C语言是动态语言, 支持动态绑定.详细的说明建议通过

学习 apple的 objective-c language来学习]

挑战

修改 LotteryEntry类中日期对象的 format 字符串

Page 66: Cocoa programming for mac os

66 页

第四章: 内存管理

假设有两个 Person对象实例, favoriteColor变量指向一个 color对象.如果两个人有同样

的 favoriteColor.那么它们的 favoriteColor变量可以指向同一个 color对象. 随着年龄

的增长,可能它们的favoriteColor都会改变. 最后原来的那个color对象就没有人使用了.

如图 4.14.

Figure 4.1. The Problem

我们当然希望这个没人使用的 Color对象不要在占用程序内存空间. 我们释放它空间来存

放程序的其他新的对象,但是一定要保证释放的 color 再也没有被其它对象所使用.

这是一个烦人的问题. Apple 提供了两套解决方案:

1. 老的方案 retain 计数: 每个对象都有一个 retain 计数,用来描述有多少对象在使用自

己.比如两个 Person对象的 favoriteColor指向同一个 color对象. 那么,该 color对象的

retain计数为 2. 当对象的 retain计数为 0时,对象将被释放.

2. 10.5提供的新的方案 garbage collector. 它管理整个对象视图.garbage collector

扫描整个视图,如果发现对象没有被任何其他对象使用. 该对象将被释放.

到底该使用哪个方案呢?retain计数有些繁琐,在你需要使用某个对象时,你需要显式调用

retain, 而不再需要使用时要显式调用 release. retain还可能产生一个麻烦:对象

Aretain对象 B. 而同时对象 B retain对象 A. 这是两个对象 A ,B都没有办法得到释放.

这就是所谓的 retain cycle. 如图 4.2 是一个常见的 retain cycle

Page 67: Cocoa programming for mac os

67 页

既然这样,我们不可以都使用 garbage collector吗?如果这样的话,我们的程序就没有办法

在 10.5之前的系统上运行了. 同时, garbage collector会耗费 CPU时间来寻找 garbage,

影响程序性能. 比如你的程序是要处理大量 Audio或 Video, garbage collector 可能导

致处理的不顺畅.

开关 garbage collector

在 XCode 左边的outline view 的Groups and Files中,你可以找到 Targets. 没有 project

至少有一个 target. 一个 target描述了一个产品的编译过程和结果. 所以,你的 project

可能包含两个 target. 一个是应用程序, 另一个是该程序的数据文件的 Spotlight

importer.

回到 lottery工程.双击 lottery target打开 Inspector.找到 Build面板,在这里我们可以

设置所有的控制编译的环境变量.

在 search框内,输入 Garbage. 过滤出来 Objective-C Garbage Collection.你可以设置是

否激活 garbage collector.如果你选择 Unsupported, Garbage Collection将不被激活如

图 4.3

Figure 4.3. Enabling/Disabling the Garbage Collector

剩下的两个选项用来激活 Garbage Collection. 在改变设置后,你必须重新编译.

本书中的代码例子都是 dual-mode代码 [即支持retain计数,也支持 Garbage Collection].

dual-mode代码保证程序不会内存泄露.当你开始计划编写一个程序是,你一定会思考是否

要使用 Garbage Collection. 你不必一直都使用 dual-mode代码.

Page 68: Cocoa programming for mac os

68 页

未来,你可能会觉得"retian 计数没有什么意义, 我打算都使用 Garbage Collection" .对

此,我没有什么意见. 不过现在,还是学习一下 retain 计数的机制吧.当你在阅读老的代码

或是使用低层次 framework 时,你知道它是怎么一回事.

(如果 retain计数真的让你不爽,那就只学习下一节:Garbage Collection.本章其余剩下的

都可以跳过去. 以后看到 retain,release,atuorelease你都可以忽略, 这些方法在

Garbage Collection 激活后什么也不做. 也可以略过所有的 dealloc, 在 Garbage

Collection激活后,它永远不会得到调用.)

使用 Garbage Collection

当我们使用 Garbage Collection,我们的程序有一个 NSGarbageCollector对象. 在

lottery.m中增加一行代码

[pool drain];

NSLog(@"GC = %@", [NSGarbageCollector defaultCollector]);

return 0;

编译运行,怎样?你使用了 Garbage Collection么?

如果使用 Garbage Collection, 记住,当你不再需要某个对象了.就不要再指向它. 在

lottery的 main函数中,在不要使用 now和 array对象时,我们让 now和 array指向 nil. 代

码如下:

} // Done with 'now'

now = nil;

for (LotteryEntry *entryToPrint in array) {

NSLog(@"%@", entryToPrint);

}

// Done with 'array'

array = nil;

[pool drain];

NSLog(@"GC = %@", [NSGarbageCollector defaultCollector]);

return 0;

}

这样,Garbage Collection 就知道原来 now指向的 NSCalendarDate对象和 array 指向的

NSMutableArray对象可以释放了(当然, Garbage Collection不会立即去释放它们, Garbage

Collection会定时扫描对象视图发现它们可以被释放后才会去释放)

Page 69: Cocoa programming for mac os

69 页

总的来说,Garbage Collection是让事情简单了.下一节会介绍 retain计数.

使用 Retain 计数

这一节,我们假定你的程序要支持 10.4. 所以,忘掉容易使用的 Garbage Collection. 我们

使用 retain计数.

每一个对象都有一个整型 retain计数. 当一个对象被创建 alloc 时. 他的 retain计数为

1. 而当 retain计数为 0时.对象将被释放掉. 我们可以使用 retain 方法来是对象的

retain计数增加 1,使用 release 方法是其减少 1.

对象的 retain计数描述了有多少个其他对象在使用它. 如果为 0,那么就表示没有对象使用

它了.对象将被释放.

我们打个比方,想象一下狗和狗绳. 如果一个人想要控制一条狗那么,他就会把自己狗绳套

在狗的脖子上,这样狗就跑不了了. 可能同时有好几个人都会把自己的狗绳套在狗的脖子上,

当然也会有人把自己的狗绳从狗的脖子上收回,不过只要有一个人的狗绳套在狗的脖子上,

那么这条狗都不会跑掉. 只有当没有狗绳套着了,狗才会跑掉. 这里,狗绳就相当于 retain

计数. 如图 4.4

retain计数可以让我们很好的控制对象的释放. 不过你一定要仔细的使用 retain和

release方法.如果 release 过多,那么将导致对象过早被释放,程序崩溃. 而 retain过多,

那么对象将不会被释放,内存泄露.

Page 70: Cocoa programming for mac os

70 页

好了,让我们来修改之前不久写得代码,让它使用 release和 retain. 回到 lottery工程.关

掉 Garbage Collection.我们修养对日期对象和 array 对象做 release

}

// Done with 'now'

[now release];

now = nil;

for (LotteryEntry *entryToPrint in array) {

NSLog(@"%@", entryToPrint);

}

// Done with 'array'

[array release];

array = nil;

[pool drain];

NSLog(@"GC = %@", [NSGarbageCollector defaultCollector]);

return 0;

}

这样它们能被正确的释放了.

array对象不会拷贝加入的对象,它会保存加入的对象的指针,并给其发送 retain 消息. 当

array对象释放时,它先会给自己队列中的对象发送 release消息. 同样的,从 array中移除

对象时,也会给对象发送 release消息.

让我们赶快来看看 LotteryEntry 对象的生命周期:

1. entry对象创建后, retain计数为 1

2. entry对象加入到 array中后,retain计数为 2

3. entry对象从 array中移除后,retain计数为 1

噢.LotteryEntry没有被释放! 在这个程序例子中, 程序马上就会结束,系统会回收所有的

内存,问题不是很大. 不过,如果程序如果运行很长时间, 这样的内存泄露将是很严重的事

情了. 试试去修改一下吧.

在添加到 array后,release 它. 如下:

LotteryEntry *newEntry;

newEntry = [[LotteryEntry alloc] initWithEntryDate:iWeeksFromNow];

[array addObject:newEntry];

[newEntry release];

}

Page 71: Cocoa programming for mac os

71 页

实现 dealloc

当对象的 retain计数为 1时调用 release, 对象的 dealloc方法将被调用. 在 dealloc方

法中,我们必须释放我们自己所使用的对象并且调用父类的 dealloc方法. 给

LotteryEntry.m增加 dealloc方法

- (void)dealloc

{

NSLog(@"deallocing %@", self);

[entryDate release];

[super dealloc];

}

在 initWithEntryDate 中我们需要 retain entryDate

- (id)initWithEntryDate:(NSCalendarDate *)theDate

{

if (![super init])

return nil;

entryDate = [theDate retain];

firstNumber = random() % 100 + 1;

secondNumber = random() % 100 + 1;

return self;

}

关掉 Garbage Collection,重新编译运行我们的程序, 仍然工作正常,并且 entry 对象也正

常释放了如图 4.5

Figure 4.5. Running Without the Garbage Collector

不过,程序仍然有内存泄露.

Page 72: Cocoa programming for mac os

72 页

创建自动释放对象

我们来看看 description方法

- (NSString *)description

{

NSString *result;

result = [[NSString alloc] initWithFormat:@"%@ = %d and %d",

[entryDate descriptionWithCalendarFormat:@"%b %d %Y"],

firstNumber, secondNumber];

return result;

}

这段代码看上去工作正常,不过里面有个讨厌的内存泄露. alloc方法会创建出来一个

string对象,它的 retain计数为 1.因此该 string对象返回时,retain计数为 1. 在其他对

象调用 description方法得到 string对象后,它会 retain 该 string对象.这时,string对

象的 retain计数变成 2. 然后在不再需要 stirng 对象时,将会调用 release.这时 string

对象的 retain计数变成 1. 正如你所想, string对象没有得到释放.

我们试着这样修改代码

- (NSString *)description

{

NSString *result;

result = [[NSString alloc] initWithFormat:@"%@ = %d and %d",

[entryDate descriptionWithCalendarFormat:@"%b %d %Y"],

firstNumber, secondNumber];

[result release];

return result;

}

这段代码将会产生问题. 当我么调用 release方法时,stirng对象的 retain计数变成

0,stirng对象将被释放掉. 返回的将是一个已经释放掉的空间.

问题是我们需要返回一个 string对象,又不想有泄露. 因此来看看在没有启用 Garbage

Collection的时候,怎么使用 NSAutoreleasePool来实现它

当给对象发送 autorelease 消息时,对象将被添加到当前的 autorelease pool中. 当

autorelease pool被排空(释放)时. 首先会给 pool中的对象发送 release消息.

换句话说,当给一个对象发送autorelease消息,就是表明将来会给该对象发送release消息.

通常在 Cocoa程序中, 在接受事件的时候创建 autorelease pool, 而在处理完事件后释放

autorelease pool. 这样,除非在中间有对对象进行 retain.否则,在一个事件处理完,所有

Page 73: Cocoa programming for mac os

73 页

的 autorelease 对象将被释放. [为什么是事件处理后呢? 其实这是因为在 NSApplication

类的事件循环里. 开始会创建 pool, 最后会释放 pool. 我们也可以在自己的代码里面增加

autorelease pool. 比如你创建一个 Foundation tool 模板的工程. 它在 main函数中就有

atuorelease pool的创建.]

正确的代码:

- (NSString *)description

{

NSString *result;

result = [[NSString alloc] initWithFormat:@"%@ = %d and %d",

[entryDate descriptionWithCalendarFormat:@"%b %d %Y"],

firstNumber, secondNumber];

[result autorelease];

return result;

}

release 相关规则:

使用 alloc new, copy或是 mutaleCopy创建的对象,其 retain计数为 1, 并且不

会添加到 atuorelease pool 中

当你使用任何方法得到一个对象是, 假定这个对象的计数为 1,或是已经添加到

了当前的 autorelease pool中,如果不希望它随着当期 pool一起被释放,一定要调

用 retain方法

因为我们经常要使用到不想 retain的对象,所以很多的类都会提供一些 class method来返

回自动释放对象.比如 NSString的 stringWithFormat: 上面代码可以是用这样的方法修改

- (NSString *)description

{

return [NSString stringWithFormat:@"%@ = %d and %d",

[entryDate descriptionWithCalendarFormat:@"%b %d %Y"],

firstNumber, secondNumber];

}

临时对象

注意,在 cocoa程序中, Autorelase对象直到事件处理完成后才被释放,这样在中间一定会

有临时对象参数.例如有一个 NSString对象的 array. 我们需要把其中的字符串对象变成大

写,并连接成一个字符串对象返回.代码如下:

Page 74: Cocoa programming for mac os

74 页

- (NSString *)concatenatedAndAllCaps

{

int i;

NSString *sum = @"";

NSString *upper;

for (i=0; i < [myArray count]; i++) {

upper = [[myArray objectAtIndex:i] uppercaseString];

sum = [NSString stringWithFormat:@"%@%@", sum, upper];

}

return sum;

}

我们假定有 13个字符串,那么我们创建了 26个 aoturelease对象: uppercaseString 创建

13个. stringWithFormat:创建 13个. 当然初始化的常量字符串不算. 除了返回的一个字

符串对象可能被 retain. 剩下的 25个都会在事件处理完后释放.[有 25个临时对象存在噢.

要是 100个或是更多呢.那的需要很多内存的, 所以我们可以在 for 循环中间创建

autorelease pool ,这样每循环一次, 该次产生的临时对象将被释放] (当然你可以使用

append的方法而不是创建新的对象来提供这个例子代码的性能)

Accessor 方法

对象包含有成员变量. 其他对象不能直接访问它们.为了让其他对象能改变或是读取自己的

成员变量.对象一般会有一对 accessor 方法

比如. 类 Rex 有一个成员变量 fido.Rex会定义两个方法 fido和 setFido: , fido 让其他

对象读取 fido的值,而 setFido:设置 fido的值

如果该成员变量不是指针类型,那么 accesor 方法将会很简单. 比如成员变量 foo为 int类

型.它对应的 accessor 方法为:

- (int)foo

{

return foo;

}

- (void)setFoo:(int)x

{

foo = x;

}

Page 75: Cocoa programming for mac os

75 页

这些方法让其他对象读取,设置 foo的值

当 foo是指向其他对象的指针时,情况会复杂一点.在"setter"方法中,你需要对新设置的对

象 retain.同时把原来的对象 release, 如图 4.6. 比如 foo指向 NSCalendarDate对象.

"setter"方法有 3个常用的习惯写法. 3个方法都可以正常使用.你应该会发现一些有经验

的 Cocoa程序员会争论哪个更好些. 我会提出自己的看法

第一个习惯写法: retain 然后 release:

- (void)setFoo:(NSCalendarDate *)x

{

[x retain];

[foo release];

foo = x;

}

Page 76: Cocoa programming for mac os

76 页

retain在 release之前是非常重要的.如果你颠倒了它们的循序, 当 x 和 foo指向同一个对

象, 而该对象的 retain计数为 1时. release将会把这个对象释放了. 评价: 如果是指向

同一个对象, retain和 release是多余的.

第二个: 改变前,先检查

- (void)setFoo:(NSCalendarDate *)x

{

if (foo != x) {

[foo release];

foo = [x retain];

}

}

只有当 foo和 x指向不同对象是,才会去做改变. 评价: 必须执行一次额外的 if语句

最后一个: Autorelease 旧的的对象

- (void)setFoo:(NSCalendarDate *)x

{

[foo autorelease];

foo = [x retain];

}

把 foo添加到 autorelease pool. 评价:如果之前有 retain 计数相关的使用错误,那么只有

当事件结束是才会出现.这样不利用调试查找错误. 前两个方法程序崩溃的根源容易找到.

而且,autorelease会影响一定的性能.

"getter"方法和非指针类型的一样

- (NSCalendarDate *)foo

{

return foo;

}

很多的 Java程序员可能会命名成 getFoo. 不要这样做, Objective-C程序员一定会命名成

foo. 在 Objective-C 的习惯中. 使用 get作为前缀,那个需要拷贝对象内部的数据. 例如,

有个 NSColor对象. 我们需要得到其中的红 , 绿 ,蓝和 alpha的值. 你可能会定义方法名

字为: getRed:green:blue:alpha:

float r, g, b, a;

[myFavoriteColor getRed:&r green:&g blue:&b alpha:&a];

Page 77: Cocoa programming for mac os

77 页

如果我们使用 accessor方法得到变量我们的 description方法应该这些写

- (NSString *)description

{

return [NSString stringWithFormat:@"%@ = %d and %d",

[self entryDate], [self firstNumber], [self secondNumber]];

}

对于纯粹的 OO编程观点,这是最好的实现[通过方法来访问成员变量,封装阿]

修改 setEntryDate:方法

- (void)setEntryDate:(NSCalendarDate *)date

{

[date retain];

[entryDate release];

entryDate = date;

}

Page 78: Cocoa programming for mac os

78 页

第五章: Target/Action

很久以前,apple 和 IBM 公司成立了一个公司 Taligent,开发一些类似 Cocoa 的工具和库,在

Taligent 已经比较成熟的时候,在一个商业交流会议上,我遇到了该公司的一个工程师.我请他

开发一个简单的程序: 在一个 window 上创建一个 button. 当点击这个 button, 在 textfield

上面显示 "hello world". 这个工程师创建了一个工程,并且开始疯狂的开始了类继承: 从

window和button以及eventhandler等基本类继承自己的子类. 然后写了n多的代码让button

和 text field 显示在window 上. 45 分钟后,我必须得离开了,可以他仍然没有完成. 在那时候我

就断定,这个公司没有前途. 果然,若干年后,他们关门大吉了.

很多的 C++和 Java 工具库和 Taligent 工具库思想差不多.开发者需要从标准类中继承,编写很

多的代码来控制 window 上显示的控件. 很多类似的工具库确实在使用.

如果使用 AppKit framework. 我们很少去继承类去处理 windows , buttons 或是 events. 相反,

我们会使用现有的类. 同样你也不需要编写代码去控制 window 中的控件. nib 文件能包含所

有的信息. 整个程序只会包含少数几行重要的代码. 开始,你会决定很惊讶.但是,随着时间,你

会发现它的优雅和美妙.

通过学习 NSControl 可以很好的理解 Appkit framework. NSButton, NSSlider,NSTextView 和

NSColorWell 都是 NSControl 的子类. 每个 control 都包含 target 和 action. target 是一个指向

其它对象的指针. action 是会发给 target 的 message(selector). 回忆一下我们在第二章给两个

按钮设置的 target 和 action: 我们把 foo 对象设置成两个按钮的 target. 一个按钮的 action 设

置成 seed:, 另一个设置成 generate: 如图 5.1

当用户和控件交互是. action 消息就会发送给它们的 target. 例如当点击一个 button, 它的

action 消息将发送给它的 target. 如图 5.2

action 方法接受一个参数: sender. 该参数可以让接收者(target)知道是哪一个控件发送的这

Page 79: Cocoa programming for mac os

79 页

个 action 消息. 通常,我们会访问 sender 来获得更多的信息. 比如,一个 check box 在勾选的

时候发送 action 消息. 而接受者接受到消息后会访问 check box,得到它是否是选中的.

- (IBAction)toggleFoo:(id)sender

{

BOOL isOn = [sender state];

...

}

要更好得理解 NSControl, 我们要进一步了解它的父类继承关系: NSControl 继承 NSView. 而

NSView又继承NSResponder. NSResponder的父类就是NSObject. 这个继承关系中的每个节点

类都增加了一些实现去做某些功能.如图 5.3

在继承关系根部是NSObject. 所有的类都从它继承, 同时继承了NSObject实现了一些基本方

法:retain, release, dealloc,和 init. NSResponder 是 NSObject 的子类. 它实现了一些事件处理的

方法,象 mouseDown: keyDown:等等. NSView 是 NSResponder 的子类. NSView 描述了 window

Page 80: Cocoa programming for mac os

80 页

上的一块区域,来显示自己. 我们可以创建 NSView 的子类来画图,或是让用户拖拽数据. 而

NSControl 继承 NSView,并增加了 target 和 action.

一些常用的 NSControl 子类

在使用控件前,我们简要的来学习下 3 个常用控件类:NSButton, NSSlider, NSTextFeild.

NSButton

NSButton 实例对象可以有几个不同的外表: 椭圆形,方形,复选框. 在点击它们时,它们有不同

的行为. 还可以给按钮设置图标和声音. 在 Interface Builder 中选择 NSButton 的 Attributes

Inspector 如图 5.4

Figure 5.4. Button Inspector

我们可能经常给按钮发送 3 个消息:

- (void)setEnabled:(BOOL)yn

用户可以点击 enabled 的按钮, disabled 的按钮会是灰色的.

- (int)state

一般对于复选框而言,如果按钮是勾选的, 返回 NSOnState(1) , 没有勾选则为 NSOffState (0).

- (void)setState:(int)aState

该方法可以使复选框勾选或不勾选.

NSSlider

NSSlider 实例-slider 可以是横向或是纵向. 它可以设置成当拖动时连续不断的发送消息,或是

Page 81: Cocoa programming for mac os

81 页

只有当拖动结束(用户 mouse up)才发送消息. slider 还可以设置标尺,把拖动的改变值限制在

一个刻度 图 5.5.同时我们还可以创建圆形的 slider

Figure 5.5. Slider Inspector

两个常用方法

- (void)setFloatValue:(float)x

移动 slide 到 x 位置

- (float)floatValue

得到当前值(位置)

NSTextField

NSTextField 实例对象文本框,能让用户输入单行文本. 文本框可以是可编辑(容许输入),也可

以是不可编辑. 通常不可编辑的文本框就是窗体上的文本标签.相对于button和 slider.文本框

相对复杂一些,以后我们还会讨论到其中奥秘. 图 5.6 是在 Interface Builder 里面 NSTextField

的属性

Figure 5.6. Text Field Inspector

Page 82: Cocoa programming for mac os

82 页

我们看到如果当文本框为空的时候,会自动包含了灰色的站位文本

NSSecureTextField是NSTextField的子类,处理类似密码文本. 用户的输入会由*号代替.我们也

不能拷贝,剪切其中的文本.

NSTextField 常用方法:

- (NSString *)stringValue

- (void)setStringValue:(NSString *)aString

得到和设置文本框中的文本

- (NSObject *)objectValue

- (void)setObjectValue:(NSObject *)obj

这些方法得到和设置文本框中任意对象类型数据

当你需要使用 formatter时,这会很有帮助. NSFormatter负责把字符串转换为另外的类型.如果

没有相关的 NSFormatter 指定, 这些方法会使用对象 obj 的 descripte 方法返回的字符串.

举个例子: 我们需要一个文本框来让用户输入日期. 我们不希望用户之间输入文本.而是一

个 NSCalendarDate 对象.通过绑定一个 NSDateFormatter,可以保证文本框的 objectVaule 方法

返回一个 NSCalendarDate 对象 ,而 setObjectValue:可以接受一个 NSCalendarDate 对象 ,

NSDateFormatter 会把 NSCalendarDate 对象转换成我们相应的文本(在 23 章,我们会创建自己

的 formatter)

图 5.7 显示了其他我们可能用到的控件. 试着吧它们拖放到你的 window 上,看看它们有什么

属性. 编译运行程序看看它们是怎么响应的吧.

Figure 5.7. Some Other Controls

Page 83: Cocoa programming for mac os

83 页

开始 SpeakLine 例子

我们来创建一个简单的例子来试着使用控件. 这个例子容许用户在文本框中输入一行文本,

然后使用 Mac OSX 的 speech synthesizer 来朗读这行文本. 当我们完成它后,样子如图 5.8

图5.9是我们将要创建的对象,以及它们的关系图.其中所有以NS为前缀的是cocoa framework

中已经有的类.我们创建了 AppController 类

使用 XCode,创建一个 Cocoa Applictiaon. 并命名为 SpeakLine.

Page 84: Cocoa programming for mac os

84 页

布局界面 (nib file)

双击 MainMenu.nib,打开 Interface Builder. 从 Library Window 中拖处一个文本框和两个按钮.

双击文本框,修改文本为"Peter Piper picked a peck of pickled peppers",(或是你希望朗读的文

字.) 把按钮的标题改为 Speak 和 Stop. 如图 5.8

回到 XCode, 我们创建一个类: AppController. AppController 将是两个按钮的 target.每个按钮

都会触发一个不同的 action 方法. 编写 AppController.h

#import <Cocoa/Cocoa.h>

@interface AppController : NSObject

{

IBOutlet NSTextField *textField;

}

- (IBAction)sayIt:(id)sender;

- (IBAction)stopIt:(id)sender;

@end

在 nib 文件中创建一个 AppController 对象: 拖一个蓝色的 NSObject 正反体到 doc window. 在

Identity Inspector 中,把他的 class 设置成 AppController. 如图 5.10

Figure 5.10. Setting the Identity

使用 Interface Builder 连接

对象连接就像我们做人员介绍: "小明,这是小强". 如果你认为小强也有必要知道小明.你会说

"小强,这位就是小明." 在 Interface Builder 中,我们从个某对象拖动到它想知道的那个对象,从

Page 85: Cocoa programming for mac os

85 页

而建立连接.

比如.当用户点击 Stop 按钮, 按钮发送一个消息给 AppController. 那么,按钮对象就要"知道

"AppController 对象. 这里,我们从按钮对象 Control-Drap 到 AppController 对象.这时会弹出一

个面板,我们可以利用它来知道 action 为 stopIt: 如图 5.11

Figure 5.11. Set action for Stop Button

同样的,我们 Conrtrol-drap Speak 按钮,设置 action 为 sayIt:

为了能够朗读文本框中的文字 , AppController 对象需要得到文本框中的文本 .因此 ,

AppController 对象有一个指向文本框的指针. Control-Click(按住 Control 点击鼠标). 当 outlets

列表出现后,从 textField 拖动到文本框上.如图 5.12

Figure 5.12. Connect AppController to the Text Field

到现在,我们设置了几乎所有对象关系图 5.9 中的对象连接. 除了 speechSynth.它将通过代

码而不是 Interface Builder 来连接.

Page 86: Cocoa programming for mac os

86 页

NSWindow 的 initialFirstResponder outlet

当我们的程序运行,窗口出现后, 用户如果没有点击文本框,他没有办法输入文本. 我们可以

设置:当窗口弹出,哪一个 view 可以接受用户的键盘输入. Control-click window 图标.在弹出面

板上拖拽 initialFirstResponder 到文本框

实现 AppController 类

现在我们来编写一些代码.回到 XCode.打开 AppController.h 文件. 给 AppController 添加一个

NSSpeechSynthesizer 类型的成员变量:speechSynth

#import <Cocoa/Cocoa.h>

@interface AppController : NSObject

{

IBOutlet NSTextField *textField;

NSSpeechSynthesizer *speechSynth;

}

- (IBAction)sayIt:(id)sender;

- (IBAction)stopIt:(id)sender;

@end

打开 AppController.m 文件.在这里我们要让程序动起来

#import "AppController.h"

@implementation AppController

- (id)init

{

[super init];

// Logs can help the beginner understand what

// is happening and hunt down bugs.

NSLog(@"init");

// Create a new instance of NSSpeechSynthesizer

// with the default voice.

speechSynth = [[NSSpeechSynthesizer alloc] initWithVoice:nil];

return self;

}

Page 87: Cocoa programming for mac os

87 页

- (IBAction)sayIt:(id)sender

{

NSString *string = [textField stringValue];

// Is the string zero-length?

if ([string length] == 0) {

NSLog(@"string from %@ is of zero-length", textField);

return;

}

[speechSynth startSpeakingString:string];

NSLog(@"Have started to say: %@", string);

}

- (IBAction)stopIt:(id)sender

{

NSLog(@"stopping");

[speechSynth stopSpeaking];

}

@end

好了,编译运行我们的程序.现在我们可以开始朗读文本,并随时停止朗读

注意: 一个菜单项(NSMenuItem 对象)同样有 target 和 action. 我们在这章所讨论的东西一样

适合于菜单项.

思考: 通过代码来设置 target

我们注意到,控件的 action 是一个 selector. NSControl 有一个方法:

- (void)setAction:(SEL)aSelector

那,我们怎么获取一个 selector 呢? 我们可以使用 Objective-C 编译指令 @selector 来查找

selector[在那里查找呢.在 self 中,也就是当前类里面].比如,要设置一个按钮的 action 为

drawMickey:.我们可以这样做

SEL mySelector;

mySelector = @selector(drawMickey:);

[myButton setAction:mySelector];

在编译的时候, @selector(drawMickey:)将会被 selector drawMickey: 来代替.

Page 88: Cocoa programming for mac os

88 页

如果你要在运行时查找 selector.可以使用 NSSelectorFromString()

SEL mySelector;

mySelector = NSSelectorFromString(@"drawMickey:");

[myButton setTarget:someObjectWithADrawMickeyMethod];

[myButton setAction:mySelector];

挑战

这个练习一定对你意义非凡. 虽然前面在我的指示下能训练完成朗读的例子.这个练习毕竟

是你自己完成的. 多参考一些前面的例子.你一定行

创建一个只有一个窗口的程序(不是 document-base). 图 5.13 显示的是程序启动后,你没有任

何输入时的样子.图 5.14 显示了,用户作了一些输入后的样子.

Page 89: Cocoa programming for mac os

89 页

当用户输入一些文本,点击按钮. 下面的文本标签将显示输入的文本,并计算出文本的字符数.

怎么使用 Cocoa 现有的类去实现这些功能对你很重要. 在这个练习中你会认识 NSTextField

类的两个方法

- (NSString *)stringValue;

- (void)setStringValue:(NSString *)aString;

同时你也会发现 NSString 的两个方法很有帮助

- (int)length;

+ (NSString *)stringWithFormat:(NSString *),...;

你还会创建一个 controller 对象, 并包含 2 个 outlet 和一个 action. (恩, 虽然有点难,但你一定

能完成它. 加油!!)

调试建议

现在你不再是简单的从书中拷贝代码,而是动手自己编写一些代码了.我想是时候给你一些代

码调试的建议了

时刻注意 console: 一旦 Cocoa 对象抛出异常,它会在 console 中记录相关信息. 如果你没注意

console.你就不能发现这些错误.

开发过程中使用 debug 编译设定: release 编译设定移除了一个 debugging symbol. 因此调试

器会没有办法正常调试

以下是一些常见问题及其解决方法

没有任何响应: 你可能忘记了在 Interface Builder 做对象连接. 因此,指针为 nil.还记得给

nil 发消息什么都不会做

做了连接,还是没有响应: 方法名字是否有拼错. Objective-C 是大小写敏感的. 所以

setFoo: 和 setfoo:是不同的方法. 设置一个断点,看看方法得到调用了么?

程序崩溃: 给一个已经释放调的对象发送消息,将会导致你的程序崩溃.(如果你使用了

garbage collector,问题将很难解决). 解决这类问题可能比较难-毕竟,产生问题的对象已

经释放掉了. 一个方法是设置对象僵化来替代释放.当我们给一个僵化的对象发送消息

是,debugger 会抛出异常显示一些描述, 比如"You tried to send the message -count to a

freed instance of class Fido". 并且 debugger 会停止在发送消息那行.

在 XCode 中双击 executable . 在 Info 面板上添加两个环境变量: NSZombiesEnabled 为 YES.

Page 90: Cocoa programming for mac os

90 页

CFZombieLevel 为 16. 如图 5.15

Figure 5.15. Turning on Zombies

对象没有释放,照样崩溃: 检查你的参数类型. 例如下面就会崩溃

int x = 5;

NSLog(@"x is %@", x);

没法通过 Interface Builder做对象连接: 是不是.h 搞错了,是不是忘掉分号: . 还是变量定义出

错, 比如 NSTabView 写成 NSTableView. 仔细找找看吧.

Page 91: Cocoa programming for mac os

91 页

第六章: 辅助(helper)对象

很久以前(在<<海岸救援队>>之前), 有一种没有名字的机器人. 有人决定给机器人提供枪和

火箭炮的功能,还的有轮子.让它成为一种完美的反恐战士. 开始他们认为"先做一个机器人

子类,然后重载方法,添加枪,火箭炮以及轮子的功能." 但是问题是,他发现,如果要添加枪以

及火箭炮功能,必需要完成了解原有机器人的内部构造. 最后,觉得给原有机器人添加辅助对

象- KITT

注意前后的不同. 整个计划并没有重新创建新的机器人.而是对原有的机器人进行了扩展.让

他成为战争机器. 机器人大脑指挥:"KITT, 我要到这面墙的那一边去." KITT 会把墙炸出一个

大洞,然后回报机器人大脑. 这是机器人就可以大摇大摆的过去了. [这段翻译的不是很准,大

概就是这个意思吧]

在Cocoa库中,很多对象通过同样的方法可以进行扩展.也就是说,cocoa中存在一些类,我们可

以根据我们的需要对它进行扩展.比如,我们不会去创将 table view 的子类.而是简单提供 table

view 辅助对象.当 table view 要显示的时候,它会向辅助对象问:"我需要显示多少行?" "第一行

我要显示什么数据, 第二行呢?"

所以,为了扩展现有的 cocoa 类功能,我们会经常编写辅助对象. 在这一章中,我们就来看看怎

么编写辅助对象,以及把它们和 cocoa 已有的类连接起来.

代理 - Delegates

对于第五章的 SpeakLine 程序, 我们做如下修改后,用户使用起来就会更方便:只有在朗读过

程中 stop 按钮才是可点击的 ,而朗读过程中 speak 按钮是不能点击的 . 为了实现这

些.AppController 需要在朗读开始是 enable 按钮,而在朗读结束时去 disable 按钮

在 cocoa 库中,很多类都声明了一个成员变量:delegate. 我们可以把它设置成指向一个辅助

对象. 在类的帮助文档中,清楚的描述了那些 delegate 方法. 比如,对于 NSSpeechSynthesizer

类,它的 delegate 方法有:

- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender

didFinishSpeaking:(BOOL)finishedSpeaking;

- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender

willSpeakWord:(NSRange)characterRange

Page 92: Cocoa programming for mac os

92 页

ofString:(NSString *)string;

- (void )speechSynthesizer:(NSSpeechSynthesizer *)sender

willSpeakPhoneme:(short)phonemeOpcode;

编写 NSSpeechSynthesizer 类的那些 apple 工程师把准备好了那些钩子, 他们就像是机器人.

而我们就是 KITT.

上 面 那 3 个 会 发 送 给 delegate 的 消 息 中 , 我 们 会 使 用 到 第 一 个

speechSynthesizer:didFinishSpeaking:

在我们的程序中 ,我们让 AppController 成为 speech sythesizer 的 delegate,并实现方法

speechSynthesizer:didFinishSpeaking:. 该 方 法 会 在 朗 读 结 束 时 自 动 调 用

[ NSSpeechSynthesizer 类中在朗读结束后调用 delegate - appController 的这个方法]. 图 6.1

为新的对象关系图.

Page 93: Cocoa programming for mac os

93 页

注意到,我们没有实现其他的 delegate 方法. 实现的那个会调用,而没有实现的将被忽略. 同

时,第一参数是发送这个消息的对象-在这里,就是 speech synthesizer

在 AppCotroller.m 中, 设置 speech synthesizer 的 delegate

- (id)init

{

[super init];

NSLog(@"init");

speechSynth = [[NSSpeechSynthesizer alloc] initWithVoice:nil];

[speechSynth setDelegate:self];

return self;

}

接着,添加 delegate 方法, 先简单的打印一行话

- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender

didFinishSpeaking:(BOOL)complete

{

NSLog(@"complete = %d", complete);

}

编译运行程序. 结果,当我们点击 stop 按钮,或朗读所有文本后,delegate 方法将被调用. 如果

朗读了所有的文本. complete 为 YES.

为了能够 disbale Stop 和 Speak 按钮.我们得创建 outlet 来指向它们. 修改 AppController.h,并

保存

IBOutlet NSButton *stopButton;

IBOutlet NSButton *startButton;

回到 Interface Builder. control-click AppController 从 stopButton outlet 拖拽到 Stop 按钮.如图

6.2.建立连接.同样从 startButton outlet 拖拽到 Speak 按钮.

Figure 6.2. Set stopButton startButton Outlets

程序刚启动的时候, Stop 按钮应该是 disable 的. 所以设置按钮的属性为 disable 如图 6.3.保存

nib 文件

Page 94: Cocoa programming for mac os

94 页

Figure 6.3. Disable Stop Button

回到 XCode,编辑 AppController.m. enable, disable 按钮. 在 sayIt: 中

- (IBAction)sayIt:(id)sender

{

NSString *string = [textField stringValue];

if ([string length] == 0) {

return;

}

[speechSynth startSpeakingString:string];

NSLog(@"Have started to say: %@", string);

[stopButton setEnabled:YES];

[startButton setEnabled:NO];

}

在 speechSynthesizer:didFinishSpeaking:中,重置按钮的状态

- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender

didFinishSpeaking:(BOOL)complete

{

NSLog(@"complete = %d", complete);

[stopButton setEnabled:NO];

[startButton setEnabled:YES];

}

编译运行程序. 我们看到了我们想要的结果.

Page 95: Cocoa programming for mac os

95 页

NSTableView 和它的 dataSource

下面,我们来添加一个 table view. 可以使用户来选择不同的声音来朗读.如图 6.4

table view 用来显示多栏的数据. NSTableView 包含一个辅助对象:dataSource,如图 6.5 . table

view 要求它的 dataSource 能够响应一些消息 . 我们把它叫做" data source 必须遵循

NSTableDataSource 非正式协议" [对于协议,我想你可能需要看看 Objective-C 语言来理解. 它

就是定义了一组方法,来规定:遵循该协议的类都有实现这些方法.有点象 C++的纯虚类概念.

而非正式协议是通过科目来实现的. 科目是什么呢? 后面会讲到,也是 Objective-C 的概念.

在这里你只有知道非正式就是我们不需要都实现这些方法] . 更直观的说法就是: 需要实现

两个方法:

- (int)numberOfRowsInTableView:(NSTableView *)aTableView;

dataSource 需要反馈有多少行数据要显示

- (id)tableView:(NSTableView *)aTableView

objectValueForTableColumn:(NSTableColumn *)aTableColumn

row:(int)rowIndex;

dataSource 需要反馈在 rowIndex 行,aTableColumn 列中要显示的数据是什么.

Page 96: Cocoa programming for mac os

96 页

如果我们的 cell [某行某列单元]需要支持可编辑.那么我们需要实现方法:

- (void)tableView:(NSTableView *)aTableView

setObjectValue:(id)anObject

forTableColumn:(NSTableColumn *)aTableColumn

row:(int)rowIndex;

dataSource 得到用户在 rowIndex 行,aTableConlumn 列的输入 abObject.在这个方法中,需要把

输入保存起来. 如果不可编辑,那就不需要实现这个方法

注意,我们是被动的提供数据去显示. 只有当 table view 需要数据的时候,我们的 dataSource

才会提供. 对于很多的程序员,他们想主宰 table view:"去,给我在第 3 行,第 5 列显示数字 7".

table view 不会遵命的.只有在它要显示第 3 行,第 5 列时,它才会向 dataSource 要要显示的数

据. 我们的 dataSource 其实是一个服务的公仆

我们可以使用 reloadDate 消息来让 table view 做刷新. table view 会重新显示所有用户可以看

到的 cell.

现在,我们来让 AppController 对象成为 table view 的 dataSource. 2 个步骤: 实现前面提到的

两个方法;设在 table view 的 dataSource outlet 指向 AppController 对象. 图 6.6 是我们将要创

建的对象关系图

Figure 6.1. New SpeakLine Object Diagram

Page 97: Cocoa programming for mac os

97 页

首先,在 AppController.h 中声明两个成员变量:

#import <Cocoa/Cocoa.h>

@interface AppController : NSObject

{

IBOutlet NSTextField *textField;

IBOutlet NSButton *startButton;

IBOutlet NSButton *stopButton;

IBOutlet NSTableView *tableView;

NSArray *voiceList;

NSSpeechSynthesizer *speechSynth;

}

保存.h 文件, 修改 init 方法初始化 voiceList:

- (id)init

{

[super init];

speechSynth = [[NSSpeechSynthesizer alloc] initWithVoice:nil];

[speechSynth setDelegate:self];

voiceList = [[NSSpeechSynthesizer availableVoices] retain];

return self;

}

布局用户界面

打开 MainMenu.nib 文件. 将窗口要编辑成如图 6.7

Page 98: Cocoa programming for mac os

98 页

拖动一个 NSTableView 对象到窗口 图 6.8

Figure 6.8. Drop a Table View on the Window

选择到 table view,以便我们可以在 Inspector 中查看它的属性(图 6.9). 这可能有点麻烦. 因为

table view是放在一个 scroll view里面,而在 table view 里面有 table view column. 多尝试几回.

当你看到 Inspector 窗口的标题变成 Table View Attributes 时,说明你选定它了. 修改属性,使

table view 只有一列.并且让 column 不可选

Figure 6.9. Inspect the Table View

双击 column 的顶部,修改标题为 Voices

连接

首先,设置 NSTableView 的 dataSource outlet 为 AppController 对象. 选中 NSTableView.

Control-click,显示出面板. 从 dateSource outlet 拖拽到 AppController.

如果你没有找到 dataSource,你选中的应该是 NSScrollView, 而不是里面的 NSTableView. scroll

view 是处理滚动条事件的. 我们会在第 17 章再详细讨论它.现在我们只关心它里面的 tabel

view 吧. 如图 6.10

Page 99: Cocoa programming for mac os

99 页

Figure 6.10. Set the tableView's dataSource Outlet

同时让 AppController 也成为 table view 的 delegate.

接下来 ,我们把 AppController 的 outlet:tableView 指向那个 table view. Control-click

AppController. 连接 tableView outlet 到 table view 如图 6.11

Figure 6.11. Set the AppController's Object's tableView Outlet

Page 100: Cocoa programming for mac os

100 页

保存 nib 文件,并关闭

编辑 AppController.m

在 AppController.m 中实现 data source 方法

- (int)numberOfRowsInTableView:(NSTableView *)tv

{

return [voiceList count];

}

- (id)tableView:(NSTableView *)tv

objectValueForTableColumn:(NSTableColumn *)tableColumn

row:(int)row

{

NSString *v = [voiceList objectAtIndex:row];

return v;

}

声音的标识是一个长字符串 .例如:com.apple.speech.synthesis.voice.Fred.如果你想让 table

view 中只显示 Fred. 修改后面那个方法

- (id)tableView:(NSTableView *)tv

objectValueForTableColumn:(NSTableColumn *)tableColumn

row:(int)row

{

NSString *v = [voiceList objectAtIndex:row];

NSDictionary *dict = [NSSpeechSynthesizer attributesForVoice:v];

return [dict objectForKey:NSVoiceName];

}

编译运行程序. 到现在,我们有了一个声音列表.不过还不能选择不同的声音效果.

table view 除了有一个 dataSource outlet. 它还有一个 delegate outlet.当改变 table view 中的

选 定 后 , delegate 方 法 将 被 调 用 . 在 AppController.m 中 . 实 现 方 法

tableViewSelectionDidChange: (类 NSNotification 会在本书的后面接受. 现在,我们只要知道.

一个通知对象作为 delegate 方法的参数)

- (void)tableViewSelectionDidChange:(NSNotification *)notification

{

int row = [tableView selectedRow];

if (row == -1) {

Page 101: Cocoa programming for mac os

101 页

return;

}

NSString *selectedVoice = [voiceList objectAtIndex:row];

[speechSynth setVoice:selectedVoice];

NSLog(@"new voice = %@", selectedVoice);

}

因为 speech synthesizer 在朗读的过程中,不能修改它的声音. 所以我们需要让 table view 在朗

读时不能做选择.

- (IBAction)sayIt:(id)sender

{

NSString *string = [textField stringValue];

if ([string length] == 0) {

return;

}

[speechSynth startSpeakingString:string];

NSLog(@"Have started to say: %@", string);

[stopButton setEnabled:YES];

[startButton setEnabled:NO];

[tableView setEnabled:NO];

}

- (void)speechSynthesizer:(NSSpeechSynthesizer *)sender

didFinishSpeaking:(BOOL)complete

{

NSLog(@"complete = %d", complete);

[stopButton setEnabled:NO];

[startButton setEnabled:YES];

[tableView setEnabled:YES];

}

同时,程序用户应该知道程序启动后默认的朗读声音是什么. 在 awakeFromNib 方法中,

让 table view滚动并选择到默认声音行

- (void)awakeFromNib

{

// When the table view appears on screen, the default voice

// should be selected

NSString *defaultVoice = [NSSpeechSynthesizer defaultVoice];

int defaultRow = [voiceList indexOfObject:defaultVoice];

[tableView selectRow:defaultRow byExtendingSelection:NO];

[tableView scrollRowToVisible:defaultRow];

Page 102: Cocoa programming for mac os

102 页

}

编译运行程序. 当正在朗读时.如果尝试改变声音,会有系统提示音. 没有朗读时.我们可

以选择不同的声音.

实现 delegate 方法时常犯的错误

拼错方法名字: 方法不会被调用. 而在编译时不会有错误和警告.避免这个问题

最好的方法是从声明方法的头文件或帮助文档中复制该方法名.再拷贝过来

忘记连接 delegate outlet. 同样在编译时不会有任何的错误和警告.

对象代理

代理是一种设计模式, 我们可以在 cocoa 库的很多地方看到他的影子. 下面列举了在

Appkit 库中支持 delegate的类:

NSAlert

NSAnimation

NSApplication

NSBrowser

NSDatePicker

NSDrawer

NSFontManager

NSImage

NSLayoutManager

NSMatrix

NSMenu

NSPathControl

NSRuleEditor

NSSavePanel

NSSound

NSSpeechRecognizer

NSSpeechSynthesizer

NSSplitView

NSTabView

NSTableView

NSText

NSTextField

NSTextStorage

NSTextView

NSTokenField

NSToolbar

NSWindow

Page 103: Cocoa programming for mac os

103 页

思考:代理是怎么工作的?

代理对象不需要实现所有的 delegate 方法.不过,一旦实现了某个代理方法,它将得到调用.这

在其他很多语言中是不可能实现的. Objective-C 是怎么实现的呢?

NSObject 有一个方法:

- (BOOL)respondsToSelector:(SEL)aSelector

因为所有的对象都是从 NSObject 继承而来(直接或是间接). 所以所有的对象都有这个方法.

如果对象有 aSelector 描述的方法,那么返回 YES. 注意 aSelector 是一个 SEL. 而不是 NSString

对象

想象一下,你是编写 NSTableView 类的工程师. 你要为改变 table view 选择行编写代码. 你会

这样想:"恩.我应该检查一下我的代理对象". 于是,你应该添加类似代码:

// About to change to row "rowIndex"

// Set the default behavior

BOOL ok = YES;

// Check whether the delegate implements the method

if ([delegate respondsToSelector:

@selector(tableView:shouldSelectRow:)]

)

{

// Execute the method

ok = [delegate tableView:self shouldSelectRow:rowIndex];

}

// Use the return value

if (ok)

{

...actually change the selection...

}

我们注意到,只有当代理对象实现了代理方法时.才会调用这个代理方法. 如果代理对象没有

实现相应的代理方法 .那么将采用默认方法来处理 .(实际上 , 为了提高性能 ,会把

Page 104: Cocoa programming for mac os

104 页

respondsToSelector:的结果缓存起来.)

我们可以在代理对象里面重载 respondsToSelector:方法.来看看这种检查机制:

- (BOOL)respondsToSelector:(SEL)aSelector

{

NSString *methodName = NSStringFromSelector(aSelector);

NSLog(@"respondsToSelector:%@", methodName);

return [super respondsToSelector:aSelector];

}

你可以试着把这个方法添加到 AppController.m 中去.

挑战: 生成一个 Delegate

创建一个只有一个窗体的程序. 生成一个对象做为这个窗体的 delegate. 当用户改变窗体大

小时,让窗体改变成它原来的两倍.

这是你要实现的 delegate 方法

- (NSSize)windowWillResize:(NSWindow *)sender

toSize:(NSSize)frameSize;

第一个参数是被改变大小的窗体. 第二个参数是一个 C 结构体. 描述了用户要改变的大小.

typedef struct _NSSize {

float width;

float height;

} NSSize;

以下是使用 NSSize 的例子: 200*100 像素大小

NSSize mySize;

mySize.width = 200.0;

mySize.height = 100.0;

NSLog(@"mySize is %f wide and %f tall", mySize.width,

mySize.height);

你可以通过 Interface Buiilder-窗体的 Size Inspector 属性来设置窗体初始大小.

Page 105: Cocoa programming for mac os

105 页

挑战: 生成一个 Data Source

创建一个 to-do 列表的程序. 用户可以在文本框中输入一个一个任务. 当点击 Add 按钮后,把

任务添加到一个Mutable Array中. 同时新的任务添加显示到下面的 table view的后一行.如图

6.12

你 还 可 以 使 table view 可 以 编 辑 .( 提 示 : NSMutableArray 有 一 个 方 法

replaceObjectAtIndex:withObject:)

Page 106: Cocoa programming for mac os

106 页

第七章: Key-Value Coding. Key-Vaule Observing

Key-value coding (KVC) 是一种机制. 容许我们通过变量的名字来获取和设置变量的值. 变量

的名字是简单的字符串, 不过我们称它为"key" . 比如. 我们有个类叫 Student. 它有个类型

为 NSString 的成员变量 firstName.

@interface Student : NSObject

{

NSString *firstName;

}

...

@ends

如果我们有个 Student 对象,我们可以这样设置它的 firstName

Student *s = [[Student alloc] init];

[s setValue:@"Larry" forKey:@"firstName"];

同时也可以这样获取 firstName:

NSString *x = [s valueForKey:@"firstName"];

方法 setValue:forKey: 和 valueForKey 定义在 NSObject 中[又是定义在 NSObject 中.这表示所

有的对象都有这连个方法咯.] 虽然这不象飞机火箭那么复杂.不过这个机制是非常强大有用

的. 这一章我们会用一个简单的例子来展示它的威力.

Key-Value Coding

使用 XCdoe,新建一个 Cocoa Application 工程. 命名为 KVCFun, 新建一个 Objectvie-class 文件.

命名为 AppController.在 Interface Builder, 拖拽一个自定义对象,设置它的类为 Appcontroller

如图 7.1

Figure 7.1. Create AppController

Page 107: Cocoa programming for mac os

107 页

保存 nib 文件

回到 XCode.打开 AppController.h, 添加一个 int 类型的成员变量 fido.

@interface AppController : NSObject

{

int fido;

}

@end

编辑 AppController.m, 我们打算创建 init 方法,通过 key-value coding 去读取设置变量 fido. 当

然这看上去有点傻:用复杂的方法去实现一个简单的功能.不过,我们这样做是为了说明问题,

而不是实际编程.

这就是那个使用 key-value coding 的复杂方法. 我们用 NSNumber 对象来代替 int 类型.

- (id)init

{

[super init];

[self setValue:[NSNumber numberWithInt:5]

forKey:@"fido"];

NSNumber *n = [self valueForKey:@"fido"];

NSLog(@"fido = %@", n);

return self;

}

key-value 机制会自动把NSNumber 转换成 int类型去设置 fido的值. 编译运行程序,没有什么

特别的.我们看到一个空白窗口, 看到"fido=5"打印在 cosole 里面

假如我们定义了 accessor 方法去读取,设置 fido.那么它们将被调用. 当然你必须正确的知道

accessor 方法的名字. getter 必须是 fido. 而 setter 必须是 setFido: . 注意,这样做不仅仅是因

为编码规范. 如果我们的 accessor 方法名不标准,当使用 key-value coding 方法时.它们就得不

到调用.[很显然这是 apple的规定啊.在 key-value方法中就是以这样 fido 和 setFido的方法名

来调用的.如果我们名字取的和它们不一样.当然没有办法调用到了] . 添加 fido 和 setFido:方

- (int)fido

{

NSLog(@"-fido is returning %d", fido);

return fido;

}

- (void)setFido:(int)x

{

NSLog(@"-setFido: is called with %d", x);

fido = x;

Page 108: Cocoa programming for mac os

108 页

}

同时在 AppController.h 中声明它们.

- (int)fido;

- (void)setFido:(int)x;

编译运行程序.我们可以看到 accessor 方法别调用了

绑定 (Binding)

Cocoa 中很多图形对象都支持绑定. 如果我们把一个图形对象的属性(比如颜色, 或是值)和

一个 key,比如 fido,绑定起来.那么,图形对象就可以自动和那个 key 的值同步. 我们添加一个

slider,把它到值和 fido 绑定起来. 看看它们是怎么同步的. 打开 MainMenu.nib. 拖拽一个

slider 到窗口上. 在 Attributes Inspector 设置 slider 为 Continuous 如图 7.2

Figure 7.2. Make Slider Continuous

在 Bindings Inspector 中, 把 slider 的 value 和 AppController 对象的 fido key 绑定一起.图 7.3

Figure 7.3. Bind Value of Slider to fido

Page 109: Cocoa programming for mac os

109 页

编译运行程序. 注意到,slider 调用 valueForKey:方法来获取它的初始值, 这个方法将调用我们

的 fido 方法. 当我们拖动 slider, 会调用 setValue:forKey: 去更新 fido 的值,这会调用 setFido:

方法

[当 slider 做 init 时, 它发现自己和 AppController 的对象绑定了,那么它肯定就保留了

AppController 对象的指针 . 然后调用了 appController 的 valueForKey:方法 .并且参数为

key-fido. 从而 fido 方法得到调用. 而在更新的时候,同样因为绑定.会调用 appController 对象

的 setValue:forKey:, 参数为 key-fido. 这样 setFido 也被调用了]

Key-Value Observing

如果我们通过其他途径来改变 fido 值,又会发送什么呢?slider 会跟着改变么?它是怎么知道

fido 的值改变了呢?

当 slider 创建的时候, 它告诉了 AppController: 我会一直关注这 key-fido[用程序语言来讲,应

该是 slider 注册了一个通知,当 fido 改变时,会通知 sldier. 通知会在后面的章节讲到.它和观察

者模式一样 ]. 任何时候通过 accesor 方法或是 key-value coding 方法改变了 fido 的

值,AppController 都会发送一个消息通知 slider:fido 改变了

再次打开 MainMenu.nib, 添加一个文本框.把它的值和 AppController 的 key fido 绑定.如图

7.4

Figure 7.4. Bind Value of Text Field to fido

编译运行程序, 我们拖动 slider, setFido:方法被调用. 同时会给绑定 fido 的文本框也发送一个

通知告知 fido 被改变了. 文本框会使用 valueForKey:来得到 fido 的新值. 因此,fido 的方法被

Page 110: Cocoa programming for mac os

110 页

调用 [建议大家可以把断点设置在 accessor 方法里面,然后看看调用时的运行堆栈,能看到什

么呢?]

观察 key

前一节,当使用 accessor 方法或 key-value coding 方法去改变 key 的值,观察者可以自动得到改

变的通知. 那假如我们直接修改变量的值又会如何?

打开 AppController.h, 声明一个新的 action 方法

- (IBAction)incrementFido:(id)sender;

打开 AppController.m,实现该方法

- (IBAction)incrementFido:(id)sender

{

fido++;

NSLog(@"fido is now %d", fido);

}

打开 MainMenu.nib, 给窗口添加一个按钮 .命名为 Increment Fido. Control-drag 按钮到

AppController 对象建立连接. 这个按钮将会触发 incrementFido action 如图 7.5

Figure 7.5. Set target/action of Button

我们希望当点击这个按钮是,slider 会跟着移动,而文本框也会刷新. 不幸的是.现在还不行.

试试编译运行看看吧

如果我们要支持给变量赋值,我们必须给这个 key 的观察者发送通知. 修改 incrementFido:方

- (IBAction)incrementFido:(id)sender

Page 111: Cocoa programming for mac os

111 页

{

[self willChangeValueForKey:@"fido"];

fido++;

NSLog(@"fido is now %d", fido);

[self didChangeValueForKey:@"fido"];

}

编译运行.OK, Increment 按钮可以正常工作了.

还有两个实现方法.第一个,使用 key-value coding

- (IBAction)incrementFido:(id)sender

{

NSNumber *n = [self valueForKey:@"fido"];

NSNumber *npp = [NSNumber numberWithInt:[n intValue] + 1];

[self setValue:npp forKey:@"fido"];

}

或者使用 accessor 方法改变 fido

- (IBAction)incrementFido:(id)sender

{

[self setFido:[self fido] + 1];

}

试试修改,然后编译运行看看

图 7.6 是我们实现的对象关系图. 注意绑定是使用半个箭头来表示

Figure 7.6. Object Diagram

Properties 和它们的属性

你可能猜到,我们大量的使用着 accessor方法. 从Objective-2.0,apple增加一个新的方法,我们

Page 112: Cocoa programming for mac os

112 页

可以通过点号来调用 accessor 方法. 比如我们有一个对象指针 rover,该对象有个 getter 方法

rex. 那么我们可以这样使用

NSLog(@"Rover's rex is %@", rover.rex);

而要调用 setter 方法,可以这样

rover.rex = [NSDate date];

在我看来,在已经有发送消息的语法情况下,这是一个愚笨的新特性.所以我不会在这本书中

使用这种方法.

编写 accessor 方法又怎么样?如我对象有 12 个成员变量,我们需要 12 个 getter 和 12setter 方

法么?

@property 和 @synthesize

同样在 Objective-C2.0 中,apple 提供了一条非常优雅的方法来节省代码量. 在 AppController.h

文件. 使用声明一个 property 来替换 fido 和 setFido:

@interface AppController : NSObject {

int fido;

}

@property(readwrite, assign) int fido;

@end

这行代码和声明 setFido:和 fido 相同

在 AppController.m 中, 我们使用@synthesize 来实现 accessor 方法. 删除方法 fido 和 setFido:

用下面的代码代替

@synthesize fido;

恩.所有的都能正常工作(当然,我们看不到那些 log 了)

Property 的属性

一般,我们这样来声明一个 property

Page 113: Cocoa programming for mac os

113 页

@property (attributes) type name;

attributes 可以是 readwrite (默认的) 或是 readonly. 如果使用 readonly,那么就只有 getter

方法.

我们也可以通过:assign, retain,copy 属性来指定 setter 方法的工作方式

assign(默认) 简单的赋值, assign 不会对新的值做 retain. 如果使用对象类型的参数,

同时没有启用 garbage collector.我们不要使用 assign

retain: release 旧的值,同时 retain 新值. 这个属性用在参数为 Objective-C 对象类型

时. 如果启用了 garbage collector, 它和 assign 作用一样. [启用了 garbage collector.

release 和 retain 什么也不做.]

copy: 对新值做拷贝,把拷贝赋值给变量. 变量为 string 时,常用该属性.

最后,我们还可以使用 nonatomic 属性. 如果我们的程序是多线程的. 那么让 setter 方法成为

atomic 是非常重要的. 也就是说, 不同的线程访问同一个 setter 方法时,不对产生冲突[线程

重入]. 如果没有启用 garbage collector. 默认的会使用锁机制来保证在同一个时间点,只能由

一个线程来调用一个 setter 方法. 因为创建和使用锁会产生一些资源消耗.所以如果我们能

够保证 accessor 方法不需要 atomic. 我们可以使用 nonatomic 属性来减少这种消耗.

思考: Key Path

通常很多对象会组成对象网络. 比如, 一个人有一个配偶, 而配偶可能有一辆脚踏车, 脚踏

车有型号. 如图 7.7

我们可以通过 key path 来得到这个人配偶的脚踏车的型号:

NSString *mn;

mn = [selectedPerson valueForKeyPath:@"spouse.scooter.modelName"];

Page 114: Cocoa programming for mac os

114 页

我们可以说: spouse 和 scooter 是 Person 的关联对象.而 modelName 是 Scooter 类的属性.

key path支持很多的操作.例如,有一个 person序列,我们可以通过 key path来求得他们的平均

expectedRaise:

NSNumber *theAverage;

theAverage = [employees valueForKeyPath:@"@avg.expectedRaise"];

以下是一些常用的操作

@avg

@count

@max

@min

@sum

现在,我们大概了解了 key path. 我们可以通过程序来进行绑定. 例如, 我们想把一个对象

-employeeController 中所有的人-arrangedOnjects 的期望加薪的平均值显示到一个文本框中.

我们可以使用绑定:

[textField bind:@"value"

toObject:employeeController

withKeyPath:@"[email protected]"

options:nil];

当然,使用 Interface Builder 来创建绑定会更简单

使用 unbind:来解除绑定:

[textField unbind:@"value"];

思考: Key-Value Observing

文本框是怎样成为 AppConroller 对象 fido key 的观察者呢? 当它从 nib 文件加载时, 文本框

把它自己添加成为一个观察者. 如果你想成为一个观察者.代码大概就像这样:[想象一下.可

能在 NSTextField 的 awakFromNib 中的代码就是这样写的]

[theAppController addObserver:self

forKeyPath:@"fido"

options:NSKeyValueObservingOld

context:somePointer];

这个方法定义在 NSObject 中. 意思为:"喂,当 fido 改变了,通知我一声阿." option 和 context 是

随着这个通知 fido 改变消息一起传送的额外数据. 触发的消息如下[当得到通知,会调用]

- (void)observeValueForKeyPath:(NSString *)keyPath

Page 115: Cocoa programming for mac os

115 页

ofObject:(id)object

change:(NSDictionary *)change

context:(void *)context

{.

……..

}

在这里.keyPaht 为@"fido". object 则为 AppController. context 则为 addObserver 时给定的

somePointer. change 为一个 NSDictionary 对象,保存了原来和新改变的值

Page 116: Cocoa programming for mac os

116 页

第八章:NSArrayController

在面向对象编程世界里,有个很通用的设计模式: Model-View-Controller [cocoa 可以说把这个

模式使用到了极致,我们在编写任何 cocoa程序时,首先得考虑到MVC]. 在这个模式思想里面,

我们创建的任何一个类都应该归纳下面 3 个组的一组内:

Model : Model 类描述了我们的数据结构.比如,我们要创建一个银行系统, 可能会生

成一个 SavingsAccount 类,来保存交易和结算列表.最完美的 Model 类不会包含任何

用户界面相关的信息.并可以使用在多个程序中.

View : View 类是 GUI 的一部分. 例如. NSSlider 是一个 View 类.最完美的 View 类都

是通用类.可以使用在多个程序中

Controller: 程序特定的Controller类负责控制程序流程的[业务逻辑类]. 用户需要浏

览数据-controller 对象读取从文件或数据库读取 Model 数据,并通过 view 对象来显

示. 当用户对数据做些改变时. View对象会通知 controller.随后 controller更新model

对象. 同时可能会把数据保存到文件或是数据库

在 Mac OS 10.3 前,cocoa 程序员往往创建 controller 对象,编写大量代码在 Mode 对象和 View

对象之间传递数据.为了使编写常用 controller对象更简单.Apple提供了NSControler类和绑定

NSController 是一个抽象类(图 8.1). NSObjectController 是 NSController 的子类,内部包含一个

对象. NSArrayController 内部包含数据对象列表.在这个练习中,我们使用 NSArrayController

Page 117: Cocoa programming for mac os

117 页

开始 RaiseMan 程序

在接下来的几个章节,我们会创建一个丰富功能的程序. 该程序记录了雇员以及他们每个人

在一年中的薪资增长比例. 所在本书的进度,我们会增加文件存储, undo, 用户偏好,打印等

功能. 而这一章,程序会是这样 图 8.2

(恩.有经验的 Cocoa 程序员,会使用 CoreData 来创建类似程序.不过我想让大家看看怎么使用

手动的方法实现. 让大家看到 CoreData 的实现也不是多么的神奇.)

XCode

使用 XCode 创建一个新的工程. 选择 Cocoa Document-based Application. 并命名为 RaiseMan

什么是 document-based 程序.这种程序可以同时打开多个窗口 .比如 TextEdit 就是一个

document-based 程序. 而 System Preferences则不是一个 document-based程序.我们会在下一

节学习更多的相关知识.

程序的对象关系图如图 8.3, talbe columns 和 NSArrayController 的连接是通过绑定而不是

outlet 来建立的.

Page 118: Cocoa programming for mac os

118 页

Figure 8.3. Object Diagram

注意,程序模板已经创建好了 MyDocument 类. MyDocument 是 NSDocument 的子类.负责文件

的读写.在这个练习中,我们会使用 NSArrayController 和绑定来构建我们的简单程序界面.所以,

我们不会去给 MyDocument 添加任何代码.选择 File->New File...,创建一个 Person 类.选择

Objective-C 类,命名为 Person.m. 同时勾选创建 Person.h 选项. 如图 8.4

Page 119: Cocoa programming for mac os

119 页

在 Person.h 中声明两个 property

#import <Foundation/Foundation.h>

@interface Person : NSObject {

NSString *personName;

float expectedRaise;

}

@property (readwrite, copy) NSString *personName;

@property (readwrite) float expectedRaise;

@end

编辑 Person.m 文件. 实现 init 和 dealloc 方法来重载父类的

#import "Person.h"

@implementation Person

- (id)init

{

[super init];

expectedRaise = 5.0;

personName = @"New Person";

return self;

}

- (void)dealloc

{

[personName release];

[super dealloc];

}

@synthesize personName;

@synthesize expectedRaise;

@end

我们知道 Person 类是一个 Model 类-它没有包含任何的界面信息. 所以,不需要知道所有的

Cocoa frameworks, 我们使用 Foundation/Foundation.h 来替换 Cocoa/Cocoa.h. 包含较小的

framework 会更优雅. 同时,Person 类也可以在命令行程序中重用.

在 MyDocument.h 中声明 employees array. 该 array 中保存的是 Person 对象.

@interface MyDocument : NSDocument

{

NSMutableArray *employees;

}

- (void)setEmployees:(NSMutableArray *)a;

在 MyDocument.m 中,创建 setEmployees:方法. 在 dealloc 中,释放该 array

- (id)init

Page 120: Cocoa programming for mac os

120 页

{

[super init];

employees = [[NSMutableArray alloc] init];

return self;

}

- (void)dealloc

{

[self setEmployees:nil];

[super dealloc];

}

- (void)setEmployees:(NSMutableArray *)a

{

// This is an unusual setter method. We are going to add a lot

// of smarts to it in the next chapter.

if (a == employees)

return;

[a retain];

[employees release];

employees = a;

}

InterFace Builder

双击打开 MyDocument.nib.删除文本框 Your document here. 给 window 添加一个 talbe view

和两个 button. 组织布局如图 8.5

从 cocoa->Objects&Controller->Controllers. 拖动 NSArrayontroller 到 doc window. 在他的

Inspector 中, 设置 Object Class Name 为 Person, 并添加 personName 和 expectedRainse key.

Page 121: Cocoa programming for mac os

121 页

如图 8.6

Figure 8.6. Controller Classes

将 array controller 的 Content Array 绑定至 File' Owner(也就是 MyDocument 对象)的

employees array. 如图 8.7

Figure 8.7. Bind Content Array

Page 122: Cocoa programming for mac os

122 页

table view 的第一列将会显示每个 employee 的名字.点击,再双击第一列.(注意不要绑定的是

scroll view,table view,或是 cell. 所以请时刻关注 Inspector 窗口的标题). 在 Bindings Inspector

中,设置 value 显示 NSArraycontroller 的 arrangedObjects 的 personName. 如图 8.8

Figure 8.8. Binding the personName Column

table view 的第二列显示每个 employee 的预期的提升值. 拖动一个 number formatter(在

Library->Cocoa->Views&Cells -> Formatters)到这列的 cell 中. 在 Inspector 中[选择这个 number

formatter,然后设置佛 formatter 的属性], 设置 formatter 显示成百分比.如图 8.9

Figure 8.9. Adding a Number Formatter

再次选定第二列 , 在 Bindings Inspector 中 , 设置 vaule 显示为 NSArrayController 的

arrangedObjects 的 expectedRaise.如图 8.10

Page 123: Cocoa programming for mac os

123 页

Figure 8.10. Bind Second Column to expectedRaise of arrangedObjects

Control-Drag 使这个 array controller 成为 Add New Employee button 的 target. 设置 action 为

add: [想象一下 NSArrayController 是我们自己写的类,实现了 add: action]

同样Control-Drag使 array controller成为 Delete button的 target.设置其 action为 remove: . 并

在不 Bindings Inspector 中,将 button的 enabled绑定到 NSArrayController 的 canRemove属性.

如图 8.11

Figure 8.11. Binding the enabled Attribute of the Delete Button

用户也许需要使用 Delete 键来删除所选择的employees. 在Attributes Inspector中,甚至键盘

快捷键为 Delete 如图 8.12

Page 124: Cocoa programming for mac os

124 页

Figure 8.12. Make Delete Key Trigger Delete Button

编译运行我们的程序. 我们可以创建和删除 Person 对象. 也可以在 table view 中编辑 Person

对象的属性 . 我们还可以同时打开多个 document.(当然 ,现在我们还没有办法将这些

document 保存成文件.)

Key-Vaule Coding 和 nil

到现在,我们只编写了少量的代码. 我们通过 Interface Builder 来指定每一列显示什么样的信

息,但是没有代码来调用的 Person 类的 accessor 方法,程序是怎么实现的呢? 那是因为有

Key-Value Coding. Key-Vaule Coding 使一般,复用类成为可能,比如 NSArrayController.

Key-vaule coding 方法会自动强制转换类型 .比如 ,当用户输入一个新的期望提升值时 ,

formatter 类会生成一个 NSNumber 对象. Key-Vaule coding 方法 setValue:forKey:会在调用

setExpectedRaise:前自动将 NSNumber 对象转换成 float.

不过这里可能会产生问题 . 指针可以为 nil.但是 float 类型不行 . 如果把 nil 作为

setValue:forKey:参数,转换时会调用类[setValue:forKey: 所属的类,在例子中为 Person]的方法

- (void)setNilValueForKey:(NSString *)s

该方法在 NSObject 定义.默认实现为抛出异常.所以,当用户使 Expected Raise 栏位为空时. 对

象[Person 对象]会抛出异常. 我们可以重载 setNilValueForKey: 将成员变量设置为默认值. 在

这个例子中,我们在 Person 类中重载这个方法,把 expectedRaise 设置为 0.0 . 在 Person.m 中添

加这个方法

- (void)setNilValueForKey:(NSString *)key

Page 125: Cocoa programming for mac os

125 页

{

if ([key isEqual:@"expectedRaise"]) {

[self setExpectedRaise:0.0];

} else {

[super setNilValueForKey:key];

}

}

当然,在我们设置一个 formatter 的时候,不是都需要重载 setNilVauleForKey:方法. 我们可以配

置让 formatter 不接受 nil 的输入.

增加排序

当程序运行后,我们可以点击一列的头部来进行排序,但是排序处理不太好. 默认的使用了大

小写敏感的 compare:方法来对 name 进行排序. 比如"Z" 会在 "a"之前. 让我们来更改排序

的方法

打开 MyDocument.nib , 我们可以通过 Attributes Inspector 来设置每一列的排序条件. 选择显

示personName这一列. 将Sort Key设为personName , 将 slector设为 caseInsensitiveCompare:

如图 8.13

Figure 8.13. Sorting on personName

caseInsensitiveCompare:是 NSString 的方法. 你可能会这样使用它:

NSString *x = @"Piaggio";

NSString *y = @"Italjet"

NSComparisonResult result = [x caseInsensitiveCompare:y];

// Would x come first in the dictionary?

if (result == NSOrderedAscending) {

...

}

Page 126: Cocoa programming for mac os

126 页

NSComparisonResult 是 整 型 . NSOrderedAscending 为 -1. NSOrderedSame 为 0.

NSOrderedDescending 为 1.

编译运行程序. 点击列的头部来进行排序. 是不是不一样了.

思考: 不使用 NSArrayController 来进行排序

在第 6 章中,我们实现了一个 tabel view 以及它的一些 dataSource 方法.你可能想知道怎么来

实现排序功能.

所有添加到每一列里的信息都会放到一个 NSSortDescriptor 对象中[table view 有一个 array

来保存 NSSortDescriptor 对象数组,每一列对应一个 NSSortDescriptor 对象]. NSSortDescriptor

对象包含了 key,一个 selector,和一个向上还是向下排序的指示量. 对于 NSMutableArray 对象,

我们可以使用下面的方法来对它进行排序

- (void)sortUsingDescriptors:(NSArray *)sortDescriptors

而当点击列头的时候,一个可选[你可以实现该方法,也可以不实现-使用默认的]的 dataSource

方法将会触发调用:

- (void)tableView:(NSTableView *)tableView

sortDescriptorsDidChange:(NSArray *)oldDescriptors

所以,如果我们使用一个 mutable array 来保存 table view 的数据,那么我们可以这样来进行排

- (void)tableView:(NSTableView *)tableView

sortDescriptorsDidChange:(NSArray *)oldDescriptors

{

NSArray *newDescriptors = [tableView sortDescriptors];

[myArray sortUsingDescriptors:newDescriptors];

[tableView reloadData];

}

噢! 程序可以支持排序功能了

挑战 1

让程序可以通过 people 名字的长度来进行排序. 你可以使用 Interface Builder 来完成这个功

Page 127: Cocoa programming for mac os

127 页

能. 诀窍是使用 key path(提示: NSString 有 length 方法)

挑战 2

这本书的第一版,我们没有使用NSArrayController和绑定来实现RaiseMan程序(这些特性是在

Mac OS 10.3 才添加的). 我们使用的是前面章节介绍的方法来实现的. 所以,这个挑战就是,

不使用 NSArrayController 和绑定来重写 RainMan 程序. 绑定机制看上去是具有神奇魔力的.

不过我们最好来看看这些魔力是怎么产生.请一定创建一个新的工程来完成这个练习.在下一

章,我们将要在你创建的工程上进行.Person 类不需要做修改. 对于 MyDocument.nib,你需要

设置每一列的 identifier 为将有显示的变量名.所以,MyDocument 类将会是 table view 的

dataSource. 同时也是 Create New 和 Delete 按钮的 target. MyDocument 会有一个 array 来保

存 Person 对象. 开始,你会修改 MyDocument.h

#import <Cocoa/Cocoa.h>

@class Person;

@interface MyDocument : NSDocument

{

NSMutableArray *employees;

IBOutlet NSTableView *tableView;

}

- (IBAction)createEmployee:(id)sender;

- (IBAction)deleteSelectedEmployees:(id)sender;

@end

修改 Mydocument.m

- (id)init

{

[super init];

employees = [[NSMutableArray alloc] init];

return self;

}

- (void)dealloc

{

[employees release];

[super dealloc];

}

#pragma mark Action methods

- (IBAction)deleteSelectedEmployees:(id)sender

{

// Which row is selected?

NSIndexSet *rows = [tableView selectedRowIndexes];

// Is the selection empty?

if ([rows count] == 0) {

NSBeep();

return;

Page 128: Cocoa programming for mac os

128 页

}

[employees removeObjectsAtIndexes:rows];

[tableView reloadData];

}

- (IBAction)createEmployee:(id)sender

{

Person *newEmployee = [[Person alloc] init];

[employees addObject:newEmployee];

[newEmployee release];

[tableView reloadData];

}

#pragma mark Table view dataSource methods

- (int)numberOfRowsInTableView:(NSTableView *)aTableView

{

return [employees count];

}

- (id)tableView:(NSTableView *)aTableView

objectValueForTableColumn:(NSTableColumn *)aTableColumn

row:(int)rowIndex

{

// What is the identifier for the column?

NSString *identifier = [aTableColumn identifier];

// What person?

Person *person = [employees objectAtIndex:rowIndex];

// What is the value of the attribute named identifier?

return [person valueForKey:identifier];

}

- (void)tableView:(NSTableView *)aTableView

setObjectValue:(id)anObject

forTableColumn:(NSTableColumn *)aTableColumn

row:(int)rowIndex

{

NSString *identifier = [aTableColumn identifier];

Person *person = [employees objectAtIndex:rowIndex];

// Set the value for the attribute named identifier

[person setValue:anObject forKey:identifier];

}

如果上面的程序可以正常工作了.不要忘了添加排序

Page 129: Cocoa programming for mac os

129 页

第九章:NSUndoManager

使用NSUndoManaer, 我们可以给程序以一种优雅的风格添加 undo功能. undo管理器跟踪管

理一个对象的添加,编辑和删除.这些消息将会发送给 undo 管理器去做 undo. 而当我们请求

做 undo 操作时, undo 管理器也会跟踪这些消息,这些消息会被记录用来做 redo. 该机制使用

两个 NSInvocation 对像堆栈来实现.

在这么早就讨论这个主题是相当沉重的.(有时候一说起 undo.我的头就有点大.),不过因为

undo 和 document 架构关联,所以我们先来学习 undo 是怎么工作的.这样在下一章能更好理

解 document 架构的工作流程.

NSInvocation

正如你所想, 应该有个对象能方便的封装一个消息[就是一个操作] - 包含 selector, 接受对象,

以及所有的参数 . NSInvocation 对象就是这样的对象.

invocation 一个非常方便的用途就是转发消息. 当一个对象接受到一个它没法响应的消息[没

有实现该方法].message-sending 系统不会马上抛出一个异常,它会先检查该对象是否实现了

这个方法

- (void)forwardInvocation:(NSInvocation *)x

如果对象实现了该方法 . 那么这个消息就会被封包成对象 NSInvocation-x. 来调用

forwardInvocation:方法

NSUndoManager 是怎样工作的

假定一下用户打开一个新的 RaiseMan document,并且做了 3 个编辑动作

添加一条记录

将记录的名字"New Employee" 修改为"Rex Fido"

将 raise 改成 20

当实现每一次修改时,controller将把一个要做undo的 invocation添加到undo栈中.简单的说:"

该修改的反向动作添加到 undo 栈中". 图 9.1 是在作了上面 3 个修改后的 undo 栈

Page 130: Cocoa programming for mac os

130 页

如果这时候用户点击 Undo 菜单,那么第一个 invocation 将会抛出并调用.person 的 raise 会设

置成 0.如果用户再次点击 Undo 菜单,那么 person 的 name 将会修改回"New Employee"

每一次从Undo栈弹出执行一项时,反向操作将会压入到 redo栈中.所以,当执行了上面说的两

个 undo 动作后,undo 和 redo 栈将会是这样的如图 9.2

undo manager 是非常智能的,当用户做编辑动作时,undo invocation 将加入到 undo 栈中,当用

户 undo 编辑时,undo invocation 将加入到 redo 栈. 而当用户 redo 编辑时, undo invocation 又

加入到 undo 栈中. 这样操作都是自动完成的. 我们的任务仅仅是提供给 undo manager 需要

做反向操作的 invocation.

Page 131: Cocoa programming for mac os

131 页

现在假设我们编写一个方法 makeItHotter, 它的反向操作方法为 makeItColder. 看看是如何

实现 undo 的

- (void)makeItHotter

{

temperature = temperature + 10;

[[undoManager prepareWithInvocationTarget:self] makeItColder];

[self showTheChangesToTheTemperature];

}

你可能猜到了, prepareWithInvocationTarget: 记录了 target [self].并且返回 undo manager 它

自己. undo manager重载了 forwardInvocation: 把 invocation-makeItColder: 加入到 undo栈中

所以,我们还有实现方法 makeItColder

- (void)makeItColder

{

temperature = temperature - 10;

[[undoManager prepareWithInvocationTarget:self] makeItHotter];

[self showTheChangesToTheTemperature];

}

我们在 undo manager中注册了反向操作. 在执行 undo时,makeItColder将被执行,而它的反向

makeItHotter 将会添加到 redo 栈中

每个栈中的 invocation 会是聚合的. 默认的,当单一事件[做了一个操作]发生时加入到栈中的

所有 invocation 将会是聚合在一起 [这里要理解什么是 invocation, 简单来讲,它就是某个对

象的某个方法. 所以当你做某个单一操作时,可能会涉及到多个对象,多个方法. 也就是多个

invocation]. 所以,当用户的一个操作改变了多个对象时, 如果点击 undo 菜单,那么所有的改

变都会一次 undo 完成.

我们也可以来修改菜单 Undo 和 Redo 标题. 比如使用 Undo Insert 来代替简单的 Undo. 可

以使用如下代码

[undoManager setActionName:@"Insert"];

那么,怎么得到一个 undo manager 呢?你可以直接创建. 不过注意,NSDocument 对象已经有一

个自己的 undo manager [它也是自己创建的哈]

Page 132: Cocoa programming for mac os

132 页

为 RaiseMan 添加 Undo 功能

为了使用户可以使用 undo 功能: undo 点击 Add New Employess 和 Delete.以及 undo 对

person 对象的修改. 我们必须给 MyDocument 添加代码

当我设计类时, 我会考虑为什么要定义一个成员变量? 一定是下面的 4 个目的之一

1. 简单的属性: 比如学生的名字. 它们一般会是数字或 NSString,NSNumber,NSDate,NSData

对象

2. 单一关系: 比如一个学生一定会有一个学校和他相关. 这和 1 比较像,只是它的类型是一

个复杂对象.单一关系使用指针来实现: 学生对象有一个指向学校对象的指针

3. 有序的多元关系: 比如,每个播放列表会有一系列歌曲和它关联. 这些歌曲有特定的顺序.

这样的关系一般使用 NSMutableArray 来实现

4. 无序的多元关系: 比如,每个部门会有一些雇员,我们可以对雇员按某个方式来排序(比如

按照姓氏),不过这样的顺序都不是本质上的顺序. 一般使用 NSMutalbeSet 来实现.

早些时候,我们讨论了怎样使用 key-vaule coding 来设置简单属性和单一关系

.当 setting 或是 getting fido 的值时, key-value coding 使用 accessor 方法.同样的我们可以为

有序的多元关系和无序的多元关系创建 accessor 方法.

来看看,对象 playlist 有一个 NSMutabelArray 变量来存放 Song 对象.如果你使用 key-value

coding 来操作这个 array 对象,你将调用 mutableArrayVauleForKey: . 得到一个代理对象,这个

代理对象表示那个 array.

id arrayProxy = [playlist mutableArrayValueForKey:@"songs"];

int songCount = [arrayProxy count];

这个例子中.当调用 count 方法时.代理对象将先看 playlist 对象有没有实现 countOfSongs 方

法.如果有,那么就会调用该方法并返回结果.如果没有, 那么会调用保存 song的 array的 count

方法如图 9.3. 注意.方法 countOfSongs 的命名不仅仅是因为编码习惯: key-vaule coding 机制

使用这样的名字来查找

Page 133: Cocoa programming for mac os

133 页

下面是几个例子

id arrayProxy = [playlist mutableArrayValueForKey:@"songs"];

int x = [arrayProxy count]; // is the same as

int x = [playlist countOfSongs]; // if countOfSongs exists

id y = [arrayProxy objectAtIndex:5] // is the same as

id y = [playlist objectInSongsAtIndex:5]; // if the method exists

[arrayProxy insertObject:p atIndex:4] // is the same as

[playlist insertObject:p inSongsAtIndex:4]; // if the method exists

[arrayProxy removeObjectAtIndex:3] // is the same as

[playlist removeObjectFromSongsAtIndex:3] // if the method exists

对于无序多元关系也是一样的如图 9.4

Page 134: Cocoa programming for mac os

134 页

id setProxy = [teacher mutableSetValueForKey:@"students"];

int x = [setProxy count]; // is the same as

int x = [teacher countOfStudents]; // if countOfStudents exists

[setProxy addObject:newStudent]; // is the same as

[teacher addStudentsObject:newStudent]; // if the method exists

[setProxy removeObject:expelledStudent]; // is the same as

[teacher removeStudentsObject:expelledStudent]; // if the method exists

因为我们绑定了 array controller的 contentArray和Mydocument对象的 employees. 所以 array

controller 将会使用 key-vaule coding 来添加和删除 person 对象. 我们可以使用这个机制来实

现当添加 person 对象时添加 unod invocation 到 undo 栈. 给 MyDocument,m 添加如下方法

- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index

{

NSLog(@"adding %@ to %@", p, employees);

// Add the inverse of this operation to the undo stack

NSUndoManager *undo = [self undoManager];

[[undo prepareWithInvocationTarget:self]

removeObjectFromEmployeesAtIndex:index];

if (![undo isUndoing]) {

[undo setActionName:@"Insert Person"];

}

// Add the Person to the array

[employees insertObject:p atIndex:index];

}

- (void)removeObjectFromEmployeesAtIndex:(int)index

{

Person *p = [employees objectAtIndex:index];

NSLog(@"removing %@ from %@", p, employees);

// Add the inverse of this operation to the undo stack

NSUndoManager *undo = [self undoManager];

[[undo prepareWithInvocationTarget:self] insertObject:p

inEmployeesAtIndex:index];

if (![undo isUndoing]) {

[undo setActionName:@"Delete Person"];

}

[employees removeObjectAtIndex:index];

}

当给 NSArrayController 添加或是删除 Person 对象时,这些方法会自动调用:例如, 当 Create

New 和 Delete 按钮发送 insert: 和 remove: 消息的时候

在 MyDocument.h 中声明

Page 135: Cocoa programming for mac os

135 页

- (void)removeObjectFromEmployeesAtIndex:(int)index;

- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index;

由于使用了 Person 类.所以我们需要告知编译器. 在 MyDocument.h 中添加

#import <Cocoa/Cocoa.h>

@class Person;

同样,在 MyDocument.m 中导入 Person.h

#import "Person.h"

好了,我们已经可以 undo 添加和删除了. 对于 undo 编辑会有点复杂. 在搞定它前,先编译运

行我们的程序.试试 undo 功能. 注意,redo 功能也是可用的

Key-Vaule Observing

在第 7 章,我们讨论了 key-vaule coding. 回忆一下,key-vaule coding 是一种通过变量名字来读

取和修改变量值的方法. 而 key-vaule observing 是当这些改变发生时我们能得到通知.

为了实现 undo 编辑,我们需要让 document 对象能够得到改变了 Person 对象 expectedRaise

和 personName 的通知. NSObject 的一个方法可以用来注册这样的通知

- (void)addObserver:(NSObject *)observer

forKeyPath:(NSString *)keyPath

options:(NSKeyValueObservingOptions)options

context:(void *)context;

对象 observer 为要通知的对象, keyPath 标识激活通知的改变. options 定义了通知包含的内容

选项,例如,是否包含改变前的值,是否包含改变后的值. context 是一个随着通知一起发送的对

象,可以包含任何信息.一般为 NULL.

当一个改变发生, observer 对象将收到下面的消息

- (void)observeValueForKeyPath:(NSString *)keyPath

ofObject:(id)object

change:(NSDictionary *)change

context:(void *)context;

observer会知道那个对象的那个 key path改变了. change是一个 dictionary,其中的内容会依据

在注册是 option指定的值. 可能包含改变前的值和(或)改变后的值. 而 context指针就是注册

是的 context 指针,通常情况下,忽略它.

Page 136: Cocoa programming for mac os

136 页

Undo 编辑

第一步是将 document 对象注册观察它自己的 person 对象改变.在 MyDocument.m 中添加如

下方法

- (void)startObservingPerson:(Person *)person

{

[person addObserver:self

forKeyPath:@"personName"

options:NSKeyValueObservingOptionOld

context:NULL];

[person addObserver:self

forKeyPath:@"expectedRaise"

options:NSKeyValueObservingOptionOld

context:NULL];

}

- (void)stopObservingPerson:(Person *)person

{

[person removeObserver:self forKeyPath:@"personName"];

[person removeObserver:self forKeyPath:@"expectedRaise"];

}

在添加或删除 Person 对象是调用上面的方法

- (void)insertObject:(Person *)p inEmployeesAtIndex:(int)index

{

// Add the inverse of this operation to the undo stack

NSUndoManager *undo = [self undoManager];

[[undo prepareWithInvocationTarget:self]

removeObjectFromEmployeesAtIndex:index];

if (![undo isUndoing]) {

[undo setActionName:@"Insert Person"];

}

// Add the Person to the array

[self startObservingPerson:p];

[employees insertObject:p atIndex:index];

}

- (void)removeObjectFromEmployeesAtIndex:(int)index

{

Person *p = [employees objectAtIndex:index];

// Add the inverse of this operation to the undo stack

NSUndoManager *undo = [self undoManager];

[[undo prepareWithInvocationTarget:self] insertObject:p

inEmployeesAtIndex:index];

Page 137: Cocoa programming for mac os

137 页

if (![undo isUndoing]) {

[undo setActionName:@"Delete Person"];

}

[self stopObservingPerson:p];

[employees removeObjectAtIndex:index];

}

- (void)setEmployees:(NSMutableArray *)a

{

if (a == employees)

return;

for (Person *person in employees) {

[self stopObservingPerson:person];

}

[a retain];

[employees release];

employees = a;

for (Person *person in employees) {

[self startObservingPerson:person];

}

}

实现编辑修改方法

- (void)changeKeyPath:(NSString *)keyPath

ofObject:(id)obj

toValue:(id)newValue

{

// setValue:forKeyPath: will cause the key-value observing method

// to be called, which takes care of the undo stuff

[obj setValue:newValue forKeyPath:keyPath];

}

实现当 Person 对象编辑通知响应方法,

- (void)observeValueForKeyPath:(NSString *)keyPath

ofObject:(id)object

change:(NSDictionary *)change

context:(void *)context

{

NSUndoManager *undo = [self undoManager];

id oldValue = [change objectForKey:NSKeyValueChangeOldKey];

// NSNull objects are used to represent nil in a dictionary

if (oldValue == [NSNull null]) {

oldValue = nil;

}

Page 138: Cocoa programming for mac os

138 页

NSLog(@"oldValue = %@", oldValue);

[[undo prepareWithInvocationTarget:self] changeKeyPath:keyPath

ofObject:object

toValue:oldValue];

[undo setActionName:@"Edit"];

}

好了,现在编译运行程序, undo 和 redo 功能完全可以工作了.

注意到了吗? 一旦我们修改了 document, 窗口标题栏上的红色关闭按钮会出现一个黑点来

提示我们,这些改变没有被保存. 在下一个章节,我们来学习把它们保存为文件

插入后开始编辑

我们的程序看上去运行的很好,不过有些用户可能会抱怨"当我插入一条记录后,为什么我必

须双击才能开始编辑?很明显的我一定会修改新增 person 的名字啊."

这会有些复杂,我打算提供所需的代码片段,首先,MyDocument.h 中添加一个 acton 和两个成

员变量

@interface MyDocument : NSDocument

{

NSMutableArray *employees;

IBOutlet NSTableView *tableView;

IBOutlet NSArrayController *employeeController;

}

- (IBAction)createEmployee:(id)sender;

保存文件,(我们记住一定要保存.h 文件.这样新加的 action 和 outlet 才能在 Interface Builder

中 找 到 ) 在 Interface Builder 中 Control-drag Add New Employee 按 钮 到 File's

Owner(MyDocument 对象). 设置 action 为 createEmployee: 如图 9.5

Figure 9.5. Set target/action of Add Button

Page 139: Cocoa programming for mac os

139 页

Control-click file's Owner,设置好 outlet tableView 和 employeeController 如图 9.6

Figure 9.6. Set Outlets

在 MyDocument.m 中添加 createEmployee:方法

- (IBAction)createEmployee:(id)sender

{

NSWindow *w = [tableView window];

// Try to end any editing that is taking place

BOOL editingEnded = [w makeFirstResponder:w];

if (!editingEnded) {

NSLog(@"Unable to end editing");

return;

}

NSUndoManager *undo = [self undoManager];

// Has an edit occurred already in this event?

if ([undo groupingLevel]) {

// Close the last group

[undo endUndoGrouping];

// Open a new group

[undo beginUndoGrouping];

}

// Create the object

Person *p = [employeeController newObject];

// Add it to the content array of 'employeeController'

[employeeController addObject:p];

[p release];

// Re-sort (in case the user has sorted a column)

[employeeController rearrangeObjects];

// Get the sorted array

NSArray *a = [employeeController arrangedObjects];

// Find the object just added

int row = [a indexOfObjectIdenticalTo:p];

Page 140: Cocoa programming for mac os

140 页

NSLog(@"starting edit of %@ in row %d", p, row);

// Begin the edit in the first column

[tableView editColumn:0

row:row

withEvent:nil

select:YES];

}

不能期望你能理解没一行代码.不过试着浏览这些方法,立即它们的基本原理. 编译运行程序

吧.

思考: Windows 和 Undo Manager

可以把 view 编辑动作加入到 undo manager.例如, NSTextView,可以把文字输入的动作加入到

undo manager.使用 Interface Builder 来激活 如图 9.7

Figure 9.7. NSTextView Inspector

那么 text view 是怎么知道使用哪一个 undo manager 呢?首先,它会询问 delegate. NSTextView

的 delegate 可以实现这个方法

- (NSUndoManager *)undoManagerForTextView:(NSTextView *)tv;

接下来,它会询问他的 window. NSWindow 有一个方法

Page 141: Cocoa programming for mac os

141 页

- (NSUndoManager *)undoManager;

window 的 delegate 可以实现一个方法来说明是否 window 可以提供 undo manager

- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;

Undo/redo 菜单项反应了当前 key window 的 undo manager 状态(key window 也就是大家说

的 active window. Cocoa 开发者叫它 key 是因为用户的键盘输入事件由它接受)

Page 142: Cocoa programming for mac os

142 页

第十章:Archiving

一个面向对象程序在运行的时候, 一般都创建了一个复杂的对象关系图. 经常需要把这样一

个复杂的对象关系图表示成字节流.这样的过程我们叫做 arching 如图 10.1,这个字节流可以

在网络中传送,也可以写入到文件中. 例如,我们创建保存一个 nib 文件,Interface Builder 把对

象写入到 nib 文件就是这样的 arching 过程(对于 Java,这个过程叫 serialization)

Figure 10.1. Archiving

而当从字节流中重新恢复对象关系图的过程叫做 unarchive. 例如,当程序启动是,将会从 nib

文件中 unarchive 对象

虽然对象包含成员变量和方法.但是只有成员变量和类名会被 archive. 换句话说,data 会被

archive,而 code 不会. 所以,如果程序 A archive 对象,而程序 B unarchive 对象.那么程序 A 和 B

都要保证包含了 class 所连接的 code. 举个例子,在 nib 文件中,你使用到了 Appkit framework

的 NSWindow 和 NSButton 对象.那么如果我们的程序没有连接 Appkit framework,那么我们就

没有办法生成 NSWindow 和 NSButton 对象,因为 archive 中只包含了 data,而没有 code

有一个洗发水的广告是这样说得:"我告诉了我的两个朋友,而他们各自又告诉了自己的两个

朋友,这样一传十,十传百.."寓意就是,你告诉了你的朋友,最后所有的人都开始使用这个洗发

水了. 对象 archiving 的工作方式和这差不多. 你 archiving 一个 root 对象. 它 archiving 自己相

关联的对象,那些相关联的对象也会 archiving 自己相关联的对象,依次类推,所有相关的对象

都被 archiving 了

archiving 由 2 步来完成. 1,我们需要告知我们的对象要怎么样来 archive. 2. 我们需要激发

archiving 动作发生

Objective-C 语言有一个机制叫 protocol, 就像 java 中的 interface 一样. 一个 protocol 声明了

Page 143: Cocoa programming for mac os

143 页

一系列方法.但你的类实现一个 protocol,那么就预定了,你的类需要实现 protocol 中声明的所

有方法

NSCoder 和 NSCoding

NSCoding 是一个 protocol. 如果你的类实现了 NSCoding.那么就要实现这些方法

- (id)initWithCoder:(NSCoder *)coder;

- (void)encodeWithCoder:(NSCoder *)coder;

NSCoder 是 archivie 字节流的抽象类.我们可以实现把数据写入一个 coder,也可以从 coder 中

读取我们写入的数据. 我们对象的方法 initWithCoder:就是从一个 coder 从读取数据,然后把

数据赋给成员变量. 方法 encodeWithCoder: 则是把成员变量的值写入到 coder 中. 在这一章

中,我们会在 Person 类中实现这两个方法

NSCoder 是一个抽象类,我们不会直接使用它来创建对象. 相反,我们会使用从它继承来的子

类. 也就是我们使用 NSKeyedUnarchiver 类来从字节流中读取数据,而使用 NSKeyedArchiver

类来把对象写入到字节流

Encoding

NSCoder 包含了很多方法, 不过大部分人会发现只会使用到其中很少的一部分. 下面是当要

archivie 数据时用到的一些常用方法

- (void)encodeObject:(id)anObject forKey:(NSString *)aKey

这个方法把 anObject 对象写入到 coder 中,并把它和 aKey 关联起来[下次使用 aKey 从 coder

中可以再把 anObject 读取出来] 这会是 anObject 的方法 encodeWithCodr 得到调用(还记得上

面那个洗发水广告把.就是这样传下去的)

对于 C 的基本类型(如 int float).NSCoder 使用下面方法

- (void)encodeBool:(BOOL)boolv forKey:(NSString *)key

- (void)encodeDouble:(double)realv forKey:(NSString *)key

- (void)encodeFloat:(float)realv forKey:(NSString *)key

- (void)encodeInt:(int)intv forKey:(NSString *)key

添加 encoing 方法到 Person 类中.

- (void)encodeWithCoder:(NSCoder *)coder

{

[super encodeWithCoder:coder];

Page 144: Cocoa programming for mac os

144 页

[coder encodeObject:personName forKey:@"personName"];

[coder encodeFloat:expectedRaise forKey:@"expectedRaise"];

}

这里调用了父类的 encodeWithCoder,使得父类有机会把自己的变量写入到 coder 中. 因此,类

继承树中的类只会把自己的成员变量写入到 coder-不会包含父类的成员变量

Decoding

从 coder 中 decoding 数据,我们使用这些方法

- (id)decodeObjectForKey:(NSString *)aKey

- (BOOL)decodeBoolForKey:(NSString *)key

- (double)decodeDoubleForKey:(NSString *)key

- (float)decodeFloatForKey:(NSString *)key

- (int)decodeIntForKey:(NSString *)key

如果因为某些原因, 字节流中没有和 aKey 关联的数据,那么我们会得到 0 值. 例如,对象没有

把 key foo 关联一个 float 数据写入 coder,那么在使用 foo key 来读取这个 float 数据,coder 会

返回0.0 . 如果 key foo关联的是一个对象数据[使用方法encodeWithCoder 写入],那么读取时

coder 返回 nil

添加 decoding 到 Person 类中

- (id)initWithCoder:(NSCoder *)coder

{

[super init];

personName = [[coder decodeObjectForKey:@"personName"] retain];

expectedRaise = [coder decodeFloatForKey:@"expectedRaise"];

return self;

}

我们没有调用父类的 initWithCoder, 那是因为NSObject没有实现它. 如过 Person类的父类实

现了 NSCoding 协议,那么这个方法应该这样写

- (id)initWithCoder:(NSCoder *)coder

{

[super initWithCoder:coder];

personName = [[coder decodeObjectForKey:@"personName"] retain];

expectedRaise = [coder decodeFloatForKey:@"expectedRaise"];

return self;

}

Page 145: Cocoa programming for mac os

145 页

你可以会说"在第 3 章中, designated initializer 会完成所有的 init 工作然后在调用父类的

designated initializer, 也就是说类的其他 initializer 方法都会调用 designated initializer,Person

类有 designated initializer- init. 可以这个新加入的 initializer 方法并没有调用 init 方法啊?" 不

错, 你是对的, initWithCoer: 是这个规则的一个特例.

好了.我们实现了 NSCoding 协议的方法.现在让 Person 类实现 NSCoding protocol. 我们来编辑

Person.h 文件.

@interface Person : NSObject <NSCoding> {

现在编译我们的工程. 你也可以运行程序看看.虽然Person类可以encode自己了.不过我们没

有地方让它这么做.所以程序看上去没什么变化.

Document Architecture

多文档程序有很多的共同性. 比如都可以创建新的 document, 打开 document,保存或打印打

开的document, 当关闭document窗口或退出程序时提醒用户保存编辑好得document. Apple

提供 3 个类- NSDocumentController,NSDocument,NSWindowController-来完成这些工作. 它们

一起组成了 document architecture

创建 document architecture 的意图是和我们第 8 章讨论的 Model-View-Controller 设计模式相

关的. 在 RaiseMan 工程中. 我们的 NSDocument 子类-使用了 NSArrayController 类-就是其中

的Controller. 它包含了指向model对象的指针. 负责下面所列的职责 [这里的model 数据就

是值 employyess-person 对象]

将 model 数据保存为一个文件

从一个文件中加载 model 数据

在 view 中显示 model 数据

响应用户通过 view 的输入,并更新 model

Info.plist 和 NSDocumentController

XCode 在编译创建一个程序时会使用到一个文件 Info.plist(本章后面,我们会修改这个文件).

当程序启动时,它会读取 Info.plisst 的信息. 告知工作的文件类型是什么. 如果它发现是一个

document-base 程序. 那么会创建一个NSDocumentController对象(图10.2). 我们很少去直接

Page 146: Cocoa programming for mac os

146 页

使用这个 document controller. NSDocumentController 对象在后面会为我们做一些工作.例如,

当选择 New 或是 Save All 菜单时, document controller 会处理这些请求. 如果你有给

document controller 发送消息,你可以这样做

NSDocumentController *dc;

dc = [NSDocumentController sharedDocumentController];

document controller 保存了一个 document 对象的 array - 每一个 document 对象就是一个打

开的 document.

NSDocument

document 对象是 NSDocument 子类的一个实例 . 在我们的 RaiseMan 程序中 ,它就是

MyDocument 的实例. 对于大部分程序,一般我们只有简单的扩展 NSDocument 来完成想要的

功能而不需要过多关系 NSDocumentcontroller 或是 NSWindowController

saving

菜单项 Save,Save As...,Save All,和 Close 虽然不相同.但是它们都面向同一个问题:把 mdoel 保

存为一个文件或是文件包(文件包是一个文件目录,不过对于用户就象是一个文件一样). 对

于这些菜单项. 我们的 NSDocument 子类需要实现下面 3 个方法中的一个

- (NSData *)dataOfType:(NSString *)aType

error:(NSError *)e

Page 147: Cocoa programming for mac os

147 页

你的 document 对象将 model 生成一个 NSData 写入文件.[这个方法中,我们只有把 model 压

成一个 NSData 返回,然后 Cocoa 会把 NSData 在写入文件了] NSData 就是字节 buffer. 是简单

也是通用的实现 saving 的方法.如果不能生成一个NSData 对象,那么就返回 nil,而用户会得到

一个 alert 提示 save 失败. 注意到参数 aType, 它可以容许你将 document 保存为一个或多个

类型格式. 例如,你编写了一个图像程序,你可能容许用户将图像保存为 gif 或是 jpg 格式.所以

当你生成 data 对象时, aType 就指定了用户请求保存的格式.如果你的程序只处理单一类型,

那么可以忽略 aType. 为了说明你不能保存,可以返回 nil 并创建一个 NSError 对象来说明出来

什么样得错误

- (NSFileWrapper *)fileWrapperOfType:(NSString *)aType

error:(NSError *)e

你的 document 对象生成一个文件包返回. 文件包将被创建在用户指定的位置

- (BOOL)writeToURL:(NSURL *)absoluteURL

ofType:(NSString *)typeName

error:(NSError **)outError;

你的 docuemnt对象以指定的 type 把 model数据保存在指定的 URL(URL 就是文件系统上的文

件路径)[这个方法应该在 NSDocument 类中实现了,里面估计就是调用了 dataOfType:error: .

得到NSData后将其写入指定 URL. 当然你也可以从中这个方法] 如果能够保存成功返回 YES,

否则返回 NO. 如果返回 NO,那么你你应该生成一个 NSError 对象来描述错误是什么

来解释下 NSError.它的观念是,因为某些原因,某个方法没有办法完成这个功能.那么它就会生

成一个 NSError 对象,并把 NSError 对象的指针放到指定的位置. 例如,如果我希望从一个文件

中读取到一个 NSData,那么我会提供一个地址,当出错时,我可以从这个地址中得到错误信息

NSError *e;

NSData *d = [NSData dataWithContentsOfFile:@"/tmp/x.txt"

options:0

error:&error];

// Did the read fail?

if (d == nil) {

NSLog(@"Read failed: %@", [error localizedDescription];

}

所以 NSData 类即会返回一个 data 对象,同时可能会创建一个 error 对象

在 save 和 load 方法中,我们将有负责在失败的时候创建 NSError 对象

Page 148: Cocoa programming for mac os

148 页

Loading

Open...,Open Recent,和 Revert To Saved 菜单项也是一样,它们都面向同一个问题:从一个文件

或是文件包中得到model. 为了响应它们,NSDocuement子类需要实现下面3个方法中的一个

- (BOOL)readFromData:(NSData *)data

ofType:(NSString *)typeName

error:(NSError **)outError

包含了用户要打开的文件内容的 NSData 对象被传进来. 如果能够从这个 NSData 对象中生成

model 那么就返回 YES. 如果返回 NO,那么用户会得到一个 Alert 提示为什么个不能成功打开

文件. Alert 的内容由这个方法生成的 NSError 对象来指定

- (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper

ofType:(NSString *)typeName

error:(NSError **)outError;

从一个 NSFileWrapper 对象读取 model 数据

- (BOOL)readFromURL:(NSURL *)absoluteURL

ofType:(NSString *)typeName

error:(NSError **)outError;

从指定文件中读取 model 数据

在实现了一个 save 和一个 load 方法后,我们的程序就知道怎么样读写文件了.在打开一个文

件时, document 对象会在读取 nib 文件前读取 document 文件[你需要读取 nib 文件来显示一

个 document 阿] . 这样的结果是,我们不能在 loading 一个 document 文件后马上去给用户界

面发送消息(它们还不存在)[注意 load nib文件是 document 架构为我们做的,这里说的立马调

用是指在 load 方法中-这个是我们在 NSDocument 子类中实现的调用-给 UI 发送消息]. [那如

果我们想要马上给 UI 发送消息怎么办?]-为了解决这个问题,我们可以实现一个方法-它会在

nib 文件被调用 UI 创建好了后发送

- (void)windowControllerDidLoadNib:(NSWindowController *)x;

[想想 当点击 Open 菜单,代码执行的过程是怎么样 - 有些代码是 cocoa 里面实现的,有些是

我们自己实现的]

在我们的 NSDocuemnt 子类中,实现这个方法刷新 UI

Page 149: Cocoa programming for mac os

149 页

NSWindowController

在 document architecture 中最后要介绍的一个类是 NSWindowcontroller .每打开一个

Document 都会产生一个窗口-生成一个 NSWindowController 实例. 对于大部分程序,每一个

document 对于一个 window, window controller 的默认实现已经够用了.所以一般我们只有在

下面几种情况下才会生成一个 NSWindowController 的子类

. 对于同一个 document,需要使用多个 window. 例如,CAD 程序, 你可能需要一个 text 窗口来

描述一个立体,而另外一个窗口来显示这个立体

你需要把 UI controller 和 model controller 放到不同的类中

你需要创建不和 NSDocument 对象对应的窗口.我们会在 12 章来做这样的事

Saving 和 NSKeyedArchiver

现在我们知道了怎样 encode 和 decode 我们自己的类,现在开始给我们的程序添加 saving 和

loading 功能了. 当我们要保存 person 到一个文件,MyDocument 类会被请求生成一个 NSData

实例. 一旦创建了 NSData 实例并返回,它会自动保存到文件中

为了生成一个 NSData 实例[encode 了 model 数据] , 我们使用 NSKeyedArchiver 类. 它有这样

一个方法

+ (NSData *)archivedDataWithRootObject:(id)rootObject

这个方法将对象 archive 成 NSData 对象的字节 buffer [字节 buffe-看看 NSData 的说明吧]

再一次回到那个广告"我告诉了两朋友,他们也告诉了自己的朋友...."当你encode一个对象是,

这个对象会 encode 它自己连接的对象,那些对象也会 encode 它们连接的对象..等等. 这里我

们要encode那个对象呢?就是array employees了. 它又会encode所有包含的Person对象. 而

我们在 Peron类中实现了 encodeWithCoder:,所以每个 Perosn对象开始 encode自己了-encode

personName 字串和 expectedRaise float

编辑方法 dataOfType:error:. 添加 saving 功能

- (NSData *)dataOfType:(NSString *)aType

error:(NSError **)outError

{

// End editing

[[tableView window] endEditingFor:nil];

Page 150: Cocoa programming for mac os

150 页

// Create an NSData object from the employees array

return [NSKeyedArchiver archivedDataWithRootObject:employees];

}

这里我们忽略了 error 参数.将没有 error 产生

Loading 和 NSKeyedUnarchiver

现在开始添加 load 文件功能, 再一次说明,NSDocument 已经大部分细节

我们会使用到 NSKeyedUnarchiver 类方法

+ (id)unarchiveObjectWithData:(NSData *)data

编辑 MyDocument 类的 readFromData:ofType:error:方法

ofType:(NSString *)typeName

error:(NSError **)outError

{

NSLog(@"About to read data of type %@", typeName);

NSMutableArray *newArray = nil;

@try {

newArray = [NSKeyedUnarchiver unarchiveObjectWithData:data];

}

@catch (NSException *e) {

if (outError) {

NSDictionary *d = [NSDictionary

dictionaryWithObject:@"The data is corrupted."

forKey:NSLocalizedFailureReasonErrorKey];

*outError = [NSError errorWithDomain:NSOSStatusErrorDomain

code:unimpErr

userInfo:d];

}

return NO;

}

[self setEmployees:newArray];

return YES;

}

在 nib 文件加载后,你需要刷新 UI.不过 NSArrayController 为你完成了这个功能.我们不需要在

windowControllerDidLoadNib:方法中多做什么. 我们在 13 章将会修改这个方法

- (void)windowControllerDidLoadNib:(NSWindowController *)aController

{

Page 151: Cocoa programming for mac os

151 页

[super windowControllerDidLoadNib:aController];

}

注意,在打开或创建一个 document 时,会询问我们的 document 类:需要 load 那个 nib 文件.现

在我们也不需要修改这个方法

- (NSString *)windowNibName

{

return @"MyDocument";

}

因为我们激活了 undo 机制,所以在编辑了 document 后, window 会自动标注为编辑过.

现在,我们的程序能够读写文件了.编译运行程序,试试看吧,看上去都能工作正常. 不过我们

保存的文件的后缀名为.???? ,我们需要在 Info.plist 中给它定义一个后缀名

设置后缀名和图标

我们将为 RaiseMan 文件添加后缀.rsmn 和一个图标. 首先找到一个.icns 文件并拷贝到我们

的工程中. 就使用/Developer/Examples/Appkit/CompositeLab/BBall.icns 吧.把他从 Finder 中拖

到 XCode 的 Resources 组中.如图 10.3

Figure 10.3. Drag Icon into Project

XCode 会弹出一个页面,确保勾选 Copy items into destination group's folder 如图 10.4.这样将

会包 icon 文件拷贝到我们的工程目录中

Page 152: Cocoa programming for mac os

152 页

Figure 10.4. Make It a Copy

在XCode中选定RaiseMan Target, 从File菜单中选择Get Info, 来设置document-type属性. 在

Properties 页中,设置 identifier 为 com.bignerdranch.RaiseMan. 设置 Icon file 为 BBall. 在

document-types 中,设置 name 为 RaiseMan Doc. Extensions 为 rsmn. icon file 为 BBall.参考图

10.5

Figure 10.5. Specify Icon and Document Types

Page 153: Cocoa programming for mac os

153 页

编译运行程序.我们再次试试保存和打开功能. 在 Finder 中, 我们的.rsmn 文件的图标变成了

BBall.icns

一个程序其实是一个目录 . 包含了程序用到的 nib 文件 , 图像,声音和可执行代码 . 在

Terminal,试试输入

> cd /Applications/TextEdit.app/Contents

> ls

可以看到 3 个有趣的东西

Info.plist 文件. 包含了该程序的信息, 文件类型和相关的图标. Finder 会使用这些信息

MacOS/目录. 这里包含了可执行代码

Resources/目录. 这里包含了程序用到的图像,声音和 nib 文件,你还可以看到不同语言的

本地化资源

思考:避免死循环

聪明的读者可能会怀疑:""如果对象 A 使对象 B 进行 encode,对象 B 使对象 C 进行 encode,而

对象 C 又使得对象 A 进行 encode. 这样不是会产生无穷循环吗?"" 没错,确实会发生这种情

况,好在 NSKeyedArchiver 类设计好了避免这种情况发送.

当 encode一个对象的时候,会将一个唯一标识同时放到流中.并建立一个表,一旦 archive对象,

就会把该对象和它的唯一标识联系起来. 如果下次又要 encode 同一个对象,NSKeyedArchiver

会先浏览这个表,看是否已经 encode 过,并只会把唯一标识放置到流中.

当从流中 decode出对象时, NSKeyedUnarchiver同样会生成一个表,把 encode对象和唯一标识

关联起来.如果发现流中只有唯一标识[说明之前有 encode 这个对象],unarchiver 就会在表中

来查找这个对象,而不是再生成一个新的对象.

NSCoder 有一个方法容易使读者和上面的思想产生混淆

- (void)encodeConditionalObject:(id)anObject forKey:(NSString *)aKey

当对象A有一个指针指向对象B, 但是对象A不需要知道对象B是否被 archive[是否存在]. 不

过如果另外一个对象已经 archive 了 B,对象 A 又希望将对象 B 的唯一标识在 encode 的时候

能够放置到流中. [也就是说对象 A 不会主动 encode B, 如果存在对象 B ,那么就指向它,否则

就指向空]

举个例子,我们需要给 Engine 对象编写它的 encodeWithCoder:方法. 它有一个成员变量为 car,

是一个指向 Car 对象的指针(发动机是汽车的一部分). 我们在 archiving Engine 对象时,不希望

整个 Car 对象被 archived. 不过如果该 Car 对象之前在其他地方 archived 过, 我们又希望

Engine 对象的 car 指针指向它. 在这种情况下,我们就要要求 Engine 对象有条件的来 encode

car 指针指向的对象了. 如图 10.6

Page 154: Cocoa programming for mac os

154 页

思考: 创建 Protocol

创建自己的 Protocol 非常简单.下面的 Protocol 有两个方法,它可能在 Foo.h 文件中

@protocol Foo

- (void)fido:(int)x;

- (float)rex;

@end

在 Objective-2.0 中,新增了语法@optional. 可以用来指定那些方法是必须那些方法是可选的

@protocol Foo

- (void)fido:(int)x;

- (float)rex;

@optional

- (int)rover;

- (void)spot:(int)x;

@end

在这个例子中 fido: 和 rex 方法是必须的,而 rover 和 spot:方法是可选的

如果你有一个类要实现 Foo protocol 和 NSCoding protocol. 应该这样做

#import "Spunky.h"

#import "Foo.h"

@interface ZsaZsa:Spunky <Foo, NSCoding>

...etc...

@end

Page 155: Cocoa programming for mac os

155 页

我们不需要重新声明父类和 protocol中声明过的方法.所以,在本例中, ZsaZsa类接口文件中不

需要再次声明 Spunky 和 Foo,NSCoding 中声明过的方法

通用类型描述[UTI]

在使用计算机时,一直有这样一个问题:"数据是怎么样展现出来的". 对于 Mac, 这个问题在

不同的几个地方都会遇到:当从 Finder 打开一个文件时.当通过剪贴板拷贝数据时,当通过

Spotlight 索引文件时,当使用 Quicklook 预览文件时.这个问题有一些答案: 文件扩展名,

creator codes,和 MIME 类型

Apple选择的长期解决途径是通用类型描述(UTIs). 一个UTI是一个描述了文件类型的字符串.

UTIs 按一定层次关系组织.

我们在 Info.plist 文件中定义程序可以读写的 UTIs-包括新建的和自定义的 UTIs. Info.plist 文件

是 XML 格式,包含了目录以及 key-value. 可以使用一个新 key UTExporterTypeDeclarations 来

export 新的 UTIs. 例如,如果你想给 RaiseMain Document 添加一个 UTI. 可以在 Info.plist 文件

中添加如下描述:

<array>

<dict>

<key>UTTypeIdentifier</key>

<string>com.bignerdranch.raiseman-doc</string>

<key>UTTypeDescription</key>

<string>RaiseMan Document</string>

<key>UTTypeConformsTo</key>

<array>

<string>public.data</string>

</array>

<key>UTTypeTagSpecification</key>

<dict>

<key>com.apple.ostype</key>

<string>rsmn</string>

<key>public.filename-extension</key>

Page 156: Cocoa programming for mac os

156 页

<array>

<string>rsmn</string>

</array>

</dict>

</dict>

</array>

当然,我们也通过 properties inspector 来使用 UTI.如图 10.7

Figure 10.7. Setting the UTI

你可以在 Apple 的文档中找到所有的系统定义的 UTIs

Page 157: Cocoa programming for mac os

157 页

第十一章: Core Data 基本原理

到现在,我们实现的程序可以维护一个对象链,实现 Undo 功能,还可以 save 和 load 文件. 你可

以想象一下,应该有很多的程序需要完成类似的功能

Apple 希望通过一些方法可以让这类程序更容易开发

NSArrayController 保持维护一个对象链

绑定机制避免了编写很多 model 对象和 view 对象同步更新的所需代码

NSManagedObjectContext 类将会监测 model 对象的成员变量,处理 undo,save 和

load 功能

使用 Core Data 和绑定, 我们可以不编写任何代码来创建类似 RaiseMan 这样到程序. 在这章

中,我们就会来创建一个简单的 Core Data 程序, 和 RaiseMan 不一样,我们不会编写任何代码

NSManagedObjectModel

系统为了知道怎么来 save 和 load 对象中的数据,它需要知道这些数据是什么样的:对象的属

性名字是什么?它们的类型是什么?为了提供这样的信息,我们需要生成 model. XCode 有一个

编辑器让我们可以很简单的来生成这样的model. 在运行的时候,程序会读取model文件来生

成 NSManagedObjectModel 对象

model使用了一些不太一样的名字术语. 对于 class,model称之为 entity; 对于 instant variable,

model 称之为 property

model包含两种property: attributes 和 relationships. attribute为简单数据类型,如一个字符串,

日期,数字. relationship 我们会在后面介绍

在 RaiseMan中,我们使用了NSDocument的子类MyDocument. 而在这个程序中,MyDocument

的父类是 NSPersistentDocument. NSPersistentDocument 类会自动读取 model 文件并生成一个

NSManagedObjectContext 对象. NSPersistentDocument 类内部包含了大量我们所需的代码[不

要自己编写咯,甚至不需要自己直接去调用,系统帮你做了]

打开 XCode,创建一个新的工程-Core Data Document-based Application . 命名为 CarLot. 假设

你有一个汽车销售店, 这个程序就是用来帮助你管理你要出售的汽车. 它会是这样的:如

Page 158: Cocoa programming for mac os

158 页

图 11.1

回到 XCode, 在工程的 Models 下打开 MyDocument.xcdatamodel . 找到 Entity table view. 点

击+ 来添加一个新的 entity, 并命名为 Car.

选中 entity Car, 在 Properties table view 下面有一个弹出按钮,点击并选择 Add Attribute,依照

下表添加六个 attributy

图 11.2 展示了添加后的样子

Page 159: Cocoa programming for mac os

159 页

Figure 11.2. Completed Model

虽然我们还可以添加更多的其他东西到 model 中, 不过对于这个练习现在已经够用了

Interface

打开 Mydocuent.nib , 从 Window 中删除 your document contents here.拖动一个 array

controller 到 doc 窗口. 这个 array controller 会使用 document 对象的 NSManaedObjectContext

对象来获取和保存数据. 使用 Bindings Inspector 将 array controller 的 managedObjectContext

和 File's Owner 的 managedObjectContext 绑定如图 11.3

Figure 11.3. Give the Array Controller a Managed Object Context

Page 160: Cocoa programming for mac os

160 页

在 Attributes Inspector 中,设置让 array controller 从 Car entity 中获取数据,如图 11.4 . 同时勾

选 Prepares Content, 使的 array controller 在创建后立马从 model 中获取数据(doc window 中

的对象下面的标签可以设置为任何名字, 在这个例子中我将 array controller 标签改成 Cars.

这样当有多个 array controller 存在时不会搞混了)

Figure 11.4. Inspect the Attributes of the Cars Array Controller

创建,配置 View

拖拽一个 table view 到窗口(从 Cocoa->Views & Cells->Data Views). 在 Attributes Inspector 中

设置为 3 列,列名为 Make/Model , Price,和 Special, 再拖拽一个 number formatter(from

Cocoa->Views & Cells-> Formatters)到 Price 列.选中这个 fommatter(就在这一列的边上一个小

圆形), 设置它显示货币. 使用 10.4+格式,设置风格为 Currency. 同时勾选 Generate Decimal

Numbers 和 Always Shows Decimal 如图 11.5

Figure 11.5. Configure Formatter

Page 161: Cocoa programming for mac os

161 页

第 3 列将显示为一个 check box. 拖拽一个 check box(从 Cocoa->Views & Cells -> Cells)到第 3

列. 选中该 cell,清空它的标题 如图 11.6

Figure 11.6. Drop Check Box Cell

在 table view 的下面,我们还需要一个 NSDataPicker,两个按钮,一个 NSImageView,和一个

NSLevelIndicator. 给 NSDataPicker 和 NSLevelIndicator 添加两个 text field. 按钮的标题修改为

New 和 Delete. text field 标题为 Data Purchased: 和 Condition:. 在 NSLevelIndicator 的

Attributes Inspector 中,设置 min 为 0,max 为 5 ,并设置风格为 Rating 模式(会显示为五角星).

并勾选 editable 如图 11.7

Figure 11.7. Attributes of the NSLevelIndicator

设值 NSImageView 为 editable. 同时选择 date picker , image view 和连个 labels , level

indicator. 点击 Layout->Embed Objects In -> Box 菜单.把它们都放置到一个 box 中去 如图

Page 162: Cocoa programming for mac os

162 页

11.8

连接和绑定

接下来我们要完成一系列的绑定, 我们会一步一步来. 图 11.9 是我们将要完成的连接 view

和 array controller 的绑定

Figure 11.9. Summary of Bindings

Page 163: Cocoa programming for mac os

163 页

有一点要注意, 我们从来不会绑定到 scroll view, table view 或是 cell. 我们只会绑定到 table

view 的某列. 列包含了 cell,属于 table view. 而 table view 是在 scroll view 里面

对每一列的 value (Cars 就是 NSArrayController)

使 New 按钮激活 arrayController 的方法 add: 图 11.10

Figure 11.10. Set target/action of New Button

同样使 Delete 按钮激活 array controller 的 remove:方法

绑定下面控件的 value 到 array controller

Page 164: Cocoa programming for mac os

164 页

绑定 image view 的 Data(不是 Value 噢)到 Cars. controller key 为 selection,keypath 为 photo. 同

时勾选 Conditionally sets Editable 如图 11.11

Figure 11.11. image View Binding

绑定box的Title With Pattern到Cars, Controller Key为 selection. Model Key Path为makeModel .

设置 Display Pattern 为 Details for %{title}@ . 设置 No Selection Placeholder 为<No selection>.

设置 Null Placeholder 为<no Make/Model>. 如图 11.12 [想一下这些设定的会导致的 cocoa 内

部工作流程是怎么样的]

Figure 11.12. Box Binding

让我们再来是实现如果 car 是 special[check box 为勾选]时,前两列的文字为粗体. 绑定 Font

Page 165: Cocoa programming for mac os

165 页

Bold 到 Cars's arrangedObjects onSpecial 如图 11.13

Figure 11.13. Specials Appear in Boldface

完成了, 编译运行程序. Save 和 Load 功能有了, Undo 功能也有了.是不是很神奇呢?

Core Data 是怎么工作的

虽然我们没有编写任何代码, 不过 Cocoa 内部还是自动为我们创建了很多的对象来完成程

序的功能. 图 11.14 为对象关系图

NSPersistentDocument 读取创建好得 data model 来生成一个 NSManagedObjectModel 对象.

Page 166: Cocoa programming for mac os

166 页

在我们的例子里, managed object model 有一个 NSEntityDescription 来描述我们的 Car entity.

entity description 包含了多个 NSAttributeDescription 对象.接下来,NSPersistentDocument 生成

一 个 NSPersistentStoreCoordinator 对 象 和 一 个 NSManagedObjectContext 对 象 .

NSManagedObjectContext 对象会从 data model 中取得 NSManagedObject 对象. 当这些

managed objected 在内存中的时候, managed object context 就会监测这些对象.如果其中的数

据改变了,managed object context 就会注册 undo 动作到 document 的 NSUnodManager.

managed object context 知道那个对象被改变了,而需要保存因此,在 Core Data 的类当中, 你

会发现你会经常和 NSManaedObjectContext 进行交互. 使用它来获取对象,保存对象的改变

等等我们可以在程序中添加卖掉的 car, 如果使得添加 car 的 datePurchased 属性为当天就更

好了. 为了实现这个功能,一个好得方法就是继承 NSArrayController 类, 重载 newObject 方法

回到 XCode, 创建一个 Objective-C 类文件. 命名为 CarArrayController. 在.h 中修改:

#import <Cocoa/Cocoa.h>

@interface CarArrayController : NSArrayController

{}

@end

在.m 中,重载方法 newObject

- (id)newObject

{

id newObj = [super newObject];

NSDate *now = [NSDate date];

[newObj setValue:now forKey:@"datePurchased"];

return newObj;

}

然后,在 Interface Builder中, 将 array controller的 class属性修改为CarArrayCtroller 如图 11.15

Figure 11.15. Change Class of Array Controller

编译运行程序, 当添加一个 Car 时, datePurchased 属性会初始为当天了.

Page 167: Cocoa programming for mac os

167 页

第十二章: Nib 文件和 NSWindowController

在 RaiseMan 程序中,我们已经有使用到两个 nib 文件: MainMenu.nib 和 MyDocument.nib.

NSApplication 在一启动时会自动将 MainMenu.nib 加载 . 而 MyDocument.nib 是在

MyDocument 对象创建的时候自动加载进来的 . 在本章中 , 我们来学习怎样使用

NSWindowController 来加载 nib 文件

可是既然系统可以自动加载,那为什么我们需要自己来加载一个 nib文件呢?大多情况下,程序

可能有几个窗口. 比如有查找的 panel,或是 Preferences panel,这些窗口只是偶尔用到. 只有

当用到这些窗口的时候才将 nib 文件加载,这样程序可以启动的更快.而且,如果用户从来不打

开这写窗口,程序将使用更少的内存

NSPanel

在这一章,我们将创建一个 Preferences panel. panel 是 NSPanel 对象,而 NSPanel 继承至

NSWindow. panel 和通常的 window 之间并没有太多的不同. 不过 panel 作为一种辅助

window(相对于通常的 document window),还是有一些不一样

panel可以成为 key window 但是不能成为main window.[cocoa 架构里面的 window系统

有介绍什么 key ,main window] NSApplication 有两个 outlet, mainWindow 和 keyWindow.

一般这连个 oulet 指向同一个 window. 而当 panel 生成后,keyWindow 就可能指向 panel

了.

如果 panel 有关闭按钮, 可以通过 Escape'键来关闭它

Panel 不会出现在 Window 菜单的列表中. 基本上,用户不会从这里来查看一个 panel.

所有的 window 都又一个布尔型变量:hidesOnDeactivate . 如果这个变量为 YES,那么当程序不

是激活状态的时候,window 将会被隐藏. 一般 document window 会设置为 NO. 而辅助的

panel 设置为 YES, 这个机制可以是屏幕不会混乱. 我们可以通过 Interface Builder->Attributes

Inspector 来设置 hidesOnDeacivate.

Page 168: Cocoa programming for mac os

168 页

给程序添加一个 Panel

在本章中我们要添加的 Preferences Panel 只是会显示出来,不会完成什么功能. 在下一章中,

我们会学习 user default 来给 Preferences Panel 添加一些功能

这个 Preferences panel 将会存放在自己的 nib 文件中, 我们会创建 NSWindowController 的子

类PreferenceController. 一个PreferenceController对象就是这个Preferences panel的 controller.

当我们要创建一个辅助的 panel 时,我们应该要意识到这个 panel 我们可能会在下个程序中重

用到. 让一个类只做 controller 工作,同时让 nib 文件只包含 panel.这样会更容易重用. 内行的

程序员会说"尽量使程序模块化,这样就能最大程度的重用". 模块化同时可以更好的为多个

程序员分配工作, Team leader 可以这样指示:"小明, 你就复制 Preferences panel,只有你可以

修改 nib 文件和 preference controller 类"

Preferences panel 中的对象会和 preference controller 连接. 这个 preference controller 将是一

个 color well和 check box 的 target. 当用户点击 Preferences...菜单时,preferences panel 将显示

出来 如图 12.1

Figure 12.1. Completed Application

Page 169: Cocoa programming for mac os

169 页

图 12.2 展现了我们将要创建的 nib 文件中包含的对象关系图

Figure 12.2. Object/Nib Diagram

打开 RaiseMan 工程, 创建新的 Objective-C 类,命名为 AppController. 编辑 AppController.h 如

#import <Cocoa/Cocoa.h>

@class PreferenceController;

@interface AppController : NSObject {

PreferenceController *preferenceController;

}

- (IBAction)showPreferencePanel:(id)sender;

@end

注意到@class PreferenceController;

这是告诉编译器,有一个类PreferenceController. 然后我们就可以不需要 import头文件就可以

声明

PreferenceController *preferenceController;

我们也可以使用#import "PreferenceController.h"来代替@class PreferenceController;这就话会

import 头文件,编译器就可以知道 PreferenceController 是一个类. import 会让编译器解析更

多的文件,所以@class 可以提供编译速度

不过注意,我们一定要 import 父类的头文件,因为编译器必须知道父类定义了哪些成员变量.

这里, 通过 import <Cocoa/Cocoa.h>来 import 了父类.h NSObject.h

设置菜单项

打开 MainMenu.nib. 从 Library(在 Objects & Controllers 下面)拖拽一个 NSObject. 在 Identity

Page 170: Cocoa programming for mac os

170 页

Inspector 中,将 class 设置为 AppController 如图 12.3

Figure 12.3. Create an Instance of AppController

从 菜 单 项 Preferences... control-drag 到 AppController. 设 置 成 target, action 为

showPreferencePanel: 如图 12.4

Figure 12.4. Set the target of the Menu Item

关闭 nib 文件

AppController.m

现在给 AppController 添加代码如下

Page 171: Cocoa programming for mac os

171 页

#import "AppController.h"

#import "PreferenceController.h"

@implementation AppController

- (IBAction)showPreferencePanel:(id)sender

{

// Is preferenceController nil?

if (!preferenceController) {

preferenceController = [[PreferenceController alloc] init];

}

NSLog(@"showing %@", preferenceController);

[preferenceController showWindow:self];

}

@end

我们只会创建一次 PreferenceController 对象,如果不等于 nil. 将会给 preferenceController 发

送消息 showWindow:

注意我们需要 import PreferenceController.h. 在 XCode 中,从 File 菜单选择New File..,创建一个

新恶 Objective-C NSWindowController subclass. 命名为 PreferenceController 如图 12.5

编辑 PreferenceController.h 如下

#import <Cocoa/Cocoa.h>

Page 172: Cocoa programming for mac os

172 页

@interface PreferenceController : NSWindowController {

IBOutlet NSColorWell *colorWell;

IBOutlet NSButton *checkbox;

}

- (IBAction)changeBackgroundColor:(id)sender;

- (IBAction)changeNewEmptyDoc:(id)sender;

@end

Preferences.nib

使用 XCode 创建新的空 nib 文件,命名为 Preferences.nib 如图 12.6

Figure 12.6. Create an Empty Nib

你将看到有 XIB 和 NIB 文件,两者的功能作用一样,不过 NIB 是二进制格式,而 XIB 是 XML 格

式. 在编译的时候 XIB 文件会编译成 NIB 文件. 既然这样,为什么要使用 XIB 文件呢?那是因为

XIB 有利于版本控制系统的应用.比如 Subversion

双击 Preferences.nib 打开. 选择 File's Owner, 在 identity Inspector 中设置他的 class 为

PreferenceController 如图 12.7

Figure 12.7. Set File's Owner to be a PreferenceController

Page 173: Cocoa programming for mac os

173 页

File's Owner

如果一个程序已经运行了一段时间,这时候我们需要加载一个新的nib文件, 那么之前已经存

在的对象需要一些连接可以访问到这个新加载的 nib 文件中的对象. File's Owner 提供了这样

的连接. 在 nib 文件中,File's Owner 其实就是一个站位符[在 nib 文件没有加载前,File's Owner

就是一个表示将来加载后要连接这个 nib 中的对象的某个对象,一旦加载后,File's Owner 就会

成为一个正在存在的对象,从而成为 nib 文件和外面对象通讯的接口]. 一个对象加载一个 nib

文件时,会提供该 nib文件的 Owner[也就是给 File's Owner 占位符赋值为一个对象]. 在我们的

程序中, owner 将会是由 AppController 创建的 PreferenceController 对象

File's Owner 迷惑了很多的人, 我们不需要在 nib 文件中实例化 PreferenceController(真正的

对象会在 nib 加载的时候提供),相反地,我们只会告知 nib 文件,它的 owner 将会是一个

PreferenceController 对象.[还记得,我们把 file's Owner 的 class 设置为 PreferenceController,对

吧. 那再详细为什么要告知 class 是什么呢??--为了 target 和 action 等连接萨]

界面布局

从 Library(Application->Windows)拖拽一个 panel 来创建一个新的 panel. 如图 12.8

Figure 12.8. Create an Instance of NSPanel

缩小到合适大小,添加一个 color well 和一个 check box 上去. 并添加一些 Label 如图

12.9(check box 自己就含有 label, 不过 color well 需要我们添加一个 text field 来作为 label)

Page 174: Cocoa programming for mac os

174 页

将 Color Well 的 target 为 File's Owner(也就是我们的 preferenceController 对象),action 为

changeBackgroundColor: 如图 12.10

Figure 12.10. Set the target of the Color Well

同样,设置 check box 的 target 为 file's owner. action 为 changeNewEmptyDoc:

Control-Click 在 File's Owner 上,出现连接面板. 将 File's Owner 的 outlet colorWell 设为这个

color well 对象. outlet checkbox 这位 check box 对象如图 12.11

Figure 12.11. Set the colorWell and checkbox Outlets

Page 175: Cocoa programming for mac os

175 页

Control-click 在 File's Owner 上,出现连接面板. 连接 outlet window 到 panel 上如图 12.12

打开 panel 的 Attributes Inspector, 禁止 Resize. 将标题修改为 Preferences. 保存 nib 文件,如

图 12.13

Figure 12.13. The New Window's Attributes

PreferenceController.m

在 XCode 中, 编辑 PreferenceController.m 如下

#import "PreferenceController.h"

@implementation PreferenceController

- (id)init

Page 176: Cocoa programming for mac os

176 页

{

if (![super initWithWindowNibName:@"Preferences"])

return nil;

return self;

}

- (void)windowDidLoad

{

NSLog(@"Nib file is loaded");

}

- (IBAction)changeBackgroundColor:(id)sender

{

NSColor *color = [colorWell color];

NSLog(@"Color changed: %@", color);

}

- (IBAction)changeNewEmptyDoc:(id)sender

{

int state = [checkbox state];

NSLog(@"Checkbox changed %d", state);

}

@end

注意到我们在 init 方法中设定了要加载的 nib 文件的文件名. nib 文件将会在需要的时候自动

加载进来.同时 PreferenceController 对象将取代该 nib 文件的 File's Owner 占位符

当 nib文件加载后,PreferenceController会收到windowDidLoad消息. 这样我们就有机会在 nib

文 件 一 加 载 后 马 上 来 设 置 nib 文 件 中 对 象 的 属 性 ( 和 awakeFromNib,

windowControllerDidLoadNib: 一样).第一次调用 showWindow:时, NSWindowController会自动

加载 nib 文件[nib 文件名在 controller 创建时指定了哈],并将窗口移动到屏幕并置前. nib 文件

只会加载一次,如果你关闭了 preference panel,并不会释放,只是会从屏幕上移走. 下次在打开

时, 只是简单的重新移回屏幕.

目前 changeBackgroundColor:和 checkboxChanged:方法还没做什么有意思的事-只是打印了一

些信息.在下一章,我们会使用它们来更新 user's defaults 数据库编译运行程序,新的 panel 可

以使用了, 操作 color well 和 check box,就会在 console 上打印出一些信息如图 12.14

Figure 12.14. Completed Application

Page 177: Cocoa programming for mac os

177 页

思考: NSBundle

bundle 是一个目录,其中包含了程序会使用到的资源. 这些资源包含了如图像,声音,编译好的

代码,nib 文件(用户也会把 bundle 称为 plug-in). 对应 bundle,cocoa 提供了类 NSBundle.

我们的程序是一个bundle. 在Finder中,一个应用程序看上去和其他文件没有什么区别. 但是

实际上它是一个包含了 nib 文件,编译代码,以及其他资源的目录. 我们把这个目录叫做程序

的 main bundle

bundle中的有些资源可以本地化.例如,对于 foo.nib,我们可以有两个版本: 一个针对英语用户,

一个针对法语用户. 在 bundle 中就会有两个子目录:English.lproj 和 French.lproj,我们把各自

版本的 foo.nib 文件放到其中. 当程序需要加载 foo.nib 文件时,bundle 会自动根据所设置的语

言来加载. 我们会在 16 章再详细讨论本地化

通过使用下面的方法得到程序的 main bundle

NSBundle *myBundle = [NSBundle mainBundle];

一般我们通过这种方法来得到 bundle.如果你需要其他目录的资源,可以指定路径来取得

bundle

NSBundle *goodBundle;

goodBundle = [NSBundle bundleWithPath:@"~/.myApp/Good.bundle"];

一旦我们有了 NSBundle 对象,那么就可以访问其中的资源了

// Extension is optional

NSString *path = [goodBundle pathForImageResource:@"Mom"];

NSImage *momPhoto = [[NSImage alloc] initWithContentsOfFile:path];

bundle 中可以包含一个库. 如果我们从库得到一个 class, bundle 会连接库,并查找该类:

Class newClass = [goodBundle classNamed:@"Rover"];

id newInstance = [[newClass alloc] init];

如果不知到 class 名,也可以通过查找主要类来取得

Class aClass = [goodBundle principalClass];

id anInstance = [[aClass alloc] init];

可以看到, NSBundle 有很多的用途.在这章中, NSBundle 负责(在后台)加载 nib 文件. 我们也可

以不通过 NSWindowController 来加载 nib 文件, 直接使用 NSBundle:

BOOL successful = [NSBundle loadNibNamed:@"About" owner:someObject];

注意噢, 我们指定了一个对象 someObject 作为 nib 的 File's Owner

挑战

创建一个新的 nib 文件,包含一个 About panel. 给 AppController 添加一个 outlet 指向它. 同时

添加 showAboutPanel:方法. 使用 NSBundle 来加载这个 nib 文件,让 AppController 成为它的

File's Owner

Page 178: Cocoa programming for mac os

178 页

第十三章: User Defaut

大部分的程序都会有 Preferences Panel 来让用户设置偏好的外观和功能.用户选择的偏好设

置会保存 user default 数据库中,在用户 Home 目录中: ~/Library/Preferences.找到数据库文件,

一般为 property list 格式 我们可以使用程序 Property List Editor 来浏览这些文件

我们通过 NSUserDefaults 类来注册程序的出厂设置,保存用户偏好设置,以及读取之前保存过

得用户偏好设置

在 12 章中我们创建了一个 color well 控件,我们将用它来设定 table view 的背景颜色.当用户

修改了他的 preference,程序将会把新的 preference 写入数据库. 当创建新的 document 窗口

时. 程序会从 user default 数据库中读取 preference. 只有当重新创建一个 window,改变才生

效,如图 13.1

Figure 13.1. Completed Application

你有注意到每次运行程序时 , 一个 untitled document 同时出现么 ? 这个 check

box:Automatically Open new document 将用来控制程序启动时是否要同时打开一个 untitled

document

NSDictionary 和 NSMutableDictionary

在关注 user defaults 之前,我们需要对类 NSDictonary (图 13.2)和 NSMutableDictionary 有所了

解. 一个 dictionary 就是 key-vaule 对的集合. key 是字符串, vuale 是对象指针

Page 179: Cocoa programming for mac os

179 页

dictionary 中描述 key 的字符串应该是唯一的.我们可以通过方法 objectForKey:来取得对应于

某个 key 的 value

anObject = [myDictionary objectForKey:@"foo"];

如果 dictionary 中没有对应的 key. 方法返回 nil

NSMutableDictionary 是 NSDictionary 的子类. NSDictionary 在创建的时候其中所有的 key 和对

象的 value 都存在了, 你可以访问其中的内容,而不能修改. 而 NSMutableDictionary 可以让你

来添加和删除 key 和 vaule

NSDictionary

dictionary 使用 hash 表来实现,所以查找的速度很快. 以下是类 NSDictionary 的一些常用方法

- (NSArray *)allKeys

返回一个包含所有 key 的 array

- (unsigned)count

返回有多少对 key-value

- (id)objectForKey:(NSString *)aKey

返回和 aKey 相关联的 value,如果没有和 aKey 相关联的 value,则返回 nil

- (NSEnumerator *)keyEnumerator

Page 180: Cocoa programming for mac os

180 页

Enumerators 也就是 iterators 或 enumerations.我们可以使用它来一步一步迭代出集合中的所

有成员. 这个方法是从一个dictionary中得到一个key的迭代器. 下面是我们可能使用它来列

举所有的 key-vaule 对

NSEnumerator *e = [myDict keyEnumerator];

for (NSString *s in e) {

NSLog(@"key is %@, value is %@", s, [myDict objectForKey:s]);

}

- (NSEnumerator *)objectEnumerator

和前一个一样,这个得到 value 的迭代器.(NSArray 也有一个类似的方法得到 array 的成员迭代

器 : objectEnumerator)

NSMutableDictionary

+ (id)dictionary

创建一个空的 dictionary

- (void)removeObjectForKey:(NSString *)aKey

从 dicionary 中删除一条记录,aKey 以及和它对应的 value

- (void)setObject:(id)anObject forKey:(NSString *)aKey

使用 aKey 和 anObject 组成一条记录添加到 dictionary 中. 在添加之间, 将会发送 retain 消息

给 anObject. 如果 aKey 已经存在于 dictionary 中,那么会移除原来对应的 value object,使用新

的 anObject 代替.同时发送 release 消息给原来的 vaule object

NSUserDefaults

每个程序有会有一些出厂默认设置. 当用户修改他的 defaults 时, 只有和出厂默认设置不同

的 user defaults 会存储在 user default 数据库. 所以当程序启动时,我们需要首先使用出厂默

认设置. 这个过程叫: registering defaults

当 registering defaults 完成后, 我们将使用 user defaults 配置用户所需. 这个过程叫做:

reading and using the defaults. user defaults database 中的数据将会自动从文件系统中读取.

你也有可能创建一个Preferences panel来让用户设置defaults, 对于defaults 对象的改变会自

动写入文件系统中.这个过程叫: setting the defaults 图 13.3

Page 181: Cocoa programming for mac os

181 页

以下是 NSUserDefaults 类的一些常用方法

+ (NSUserDefaults *)standardUserDefaults

返回全局标准 defaults 对象

- (void)registerDefaults:(NSDictionary *)dictionary

注册出厂默认设置

- (void)setBool:(BOOL)value forKey:(NSString *)defaultName

- (void)setFloat:(float)value forKey:(NSString *)defaultName

- (void)setInteger:(int)value forKey:(NSString *)defaultName

- (void)setObject:(id)value forKey:(NSString *)defaultName

修改 defaults 方法

- (BOOL)boolForKey:(NSString *)defaultName

- (float)floatForKey:(NSString *)defaultName

- (int)integerForKey:(NSString *)defaultName

- (id)objectForKey:(NSString *)defaultName

对其 defaults 方法.如果用户没有修改过,返回出厂默认设置

- (void)removeObjectForKey:(NSString *)defaultName

删除用户设置,程序将使用出厂默认设置

Page 182: Cocoa programming for mac os

182 页

不同类型的 defaults 的优先级

到目前,我们讨论了两种类型的 defaults. 用户所希望的 user default 和出厂默认设置 default.

其中前者优先级高于后者.[这里的优先级理解是这样的:假如存在多种 defauts, 我们使用

NSUserDefaults 类去读写 default 是,会先去看那个类型的 default].实际上,还有几种优先级存

在. 这些优先级 level 我们称之为 domains. 下面列举了程序可以用地的节 domain. 优先级从

高到低

arguments: 通过 command line 传递. 一般我们都是通过双击程序图标来运行程序,

而不是使用 command line. 所以我们很少会用到

Application: 来自于 user defaults database

Global: 用户对于整个提供的设定

Language: 基于用户所选语言

Registered defaults: 程序的出厂默认设置

设置程序的 Identifier

在~/Library/Preferences 中为程序创建的 property list 文件叫什么名字呢?默认的,以程序的

identifier.我们在第 10 章设置了程序的 identifier 为 com.bignerdranch.RaiseMan. 所以文件名

将为 com.bignerdranch.RaiseMan.plist

给 Defaults Key 命名

我们可能在不同的几个类中 register,read,write defaults. 为了保证我们能使用一样的 key名字,

我们将名字字符串定义在一个文件中,然后在使用的地方使用#import 来包含它

我们可以使用 C 的预编译#define, 不过 Cocoa 程序员一般选择使用全局变量来实现. 在

PreferenceController.h #import 只后添加

extern NSString * const BNRTableBgColorKey;

extern NSString * const BNREmptyDocKey;

在 PreferenceController.m 中定义这些变量,(在#import 后, @implementation 前)

NSString * const BNRTableBgColorKey = @"TableBackgroundColor";

NSString * const BNREmptyDocKey = @"EmptyDocumentFlag";

那我们为什么要声明全局变量来包含一个常量字符串? 我们可以直接使用这个字符串就好

Page 183: Cocoa programming for mac os

183 页

了阿. 这里有个问题,当你直接使用字符串时,假如发生拼写错误的时候,编译器不会报错,而

是使用错误的字符串. 相反,如果我们使用全局变量,如果拼写错了,编译器会报错的.

为了使全局变量能和其他公司组织的全局变量区别开来,我们使用前缀 BNR (Big Nerd Ranch).

Cocoa 的全局变量前缀是 NS. 当我们需要使用第三方的组件来开发时,这样的前缀规则是十

分重要的[名字空间概念] (类名也是全局的. 我们自己的类应该有 BNR 的前缀,这样来区别于

其他组织的类)

Registering Defaults

在接受其他消息前 ,每个类首先都会接受的 initialize 消息 . 我们重载 Appcontroller 的

initialize 方法,来区别先 register defaults

+ (void)initialize

{

// Create a dictionary

NSMutableDictionary *defaultValues = [NSMutableDictionary dictionary];

// Archive the color object

NSData *colorAsData = [NSKeyedArchiver archivedDataWithRootObject:

[NSColor yellowColor]];

// Put defaults in the dictionary

[defaultValues setObject:colorAsData forKey:BNRTableBgColorKey];

[defaultValues setObject:[NSNumber numberWithBool:YES]

forKey:BNREmptyDocKey];

// Register the dictionary of defaults

[[NSUserDefaults standardUserDefaults]

registerDefaults: defaultValues];

NSLog(@"registered defaults: %@", defaultValues);

}

这个方法是 class method,所以有+ 号的前缀

注意到我们将 color 对象先储存为一个 data 对象. 这是因为,NSColor 对象不知道怎么样将自

己写入到 property list, 所以我们先将它们打包成一个 data 对象 . property list 类 -

NSString,NSArray,NSDictionary,NSCalendarDate,NSData 和 NSNumber- 知道怎么将自己写入

property list. 一个 property list 任何是这些类对象的组合.例如,一个 dictionary 可以包含一个

date array.[日期对象的 array, 如果 array 中存放的是 NSColor 对象,会怎么样呢.我估计是不能

写入到 property list 中,大家可以试试看]

Page 184: Cocoa programming for mac os

184 页

让用户编辑 defaults

下来,我们要修改 PreferenceController 类使用 defaults database 更新 Preference Panel. 在

PreferenceController.h 中声明以下的方法

- (NSColor *)tableBgColor;

- (BOOL)emptyDoc;

修改 PreferenceController.m 如下

#import "PreferenceController.h"

NSString * const BNRTableBgColorKey = @"TableBackgroundColor";

NSString * const BNREmptyDocKey = @"EmptyDocumentFlag";

@implementation PreferenceController

- (id)init

{

if (![super initWithWindowNibName:@"Preferences"])

return nil;

return self;

}

- (NSColor *)tableBgColor

{

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

NSData *colorAsData = [defaults objectForKey:BNRTableBgColorKey];

return [NSKeyedUnarchiver unarchiveObjectWithData:colorAsData];

}

- (BOOL)emptyDoc

{

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

return [defaults boolForKey:BNREmptyDocKey];

}

- (void)windowDidLoad

{

[colorWell setColor:[self tableBgColor]];

[checkbox setState:[self emptyDoc]];

}

- (IBAction)changeBackgroundColor:(id)sender

{

NSColor *color = [colorWell color];

NSData *colorAsData =

[NSKeyedArchiver archivedDataWithRootObject:color];

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

[defaults setObject:colorAsData forKey:BNRTableBgColorKey];

}

Page 185: Cocoa programming for mac os

185 页

- (IBAction)changeNewEmptyDoc:(id)sender

{

int state = [checkbox state];

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

[defaults setBool:state forKey:BNREmptyDocKey];

}

@end

在 windowDidLoad 方法中, 我们读取了 defualts 中的数据,并用它让 color well 和 check box 更

新当前设定. 在 changeBackgroundColor:和 changeNewEmptyDoc:方法中我们更新了 defaults

database

现在我们编译运行程序, 程序将读写 defaults database. 不过,我们还没有使用这些信息来做

什么, 所以文档的背景颜色还是白色.而 untitled document 还是会在打开程序时弹出来

使用 Defaults

现在我们打算使用这些 defaults. 首先,我们将 AppController 设置成 NSApplication 对象的

delegate,根据 user defaults, 禁止生成一个 untitled document. 然后,设置 table view 中的背景

颜色.

禁止生成 Untitled Documents

之前,我们通过 2 步来生成 delegate: 实现 delegate 方法 和 将 delegate outlet 指向一个对象

如图 13.4

在 自 动 生 成 一 个 新 的 untitled document 前 ,NSApplication 对 象 会 发 送 消 息

applicationShouldOpenUntitledFile:给它的 deleate.在 AppController.m 中添加如下方法

- (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender

{

NSLog(@"applicationShouldOpenUntitledFile:");

Page 186: Cocoa programming for mac os

186 页

return [[NSUserDefaults standardUserDefaults]

boolForKey:BNREmptyDocKey];

}

现在来设置 delegate.打开 MainMenu.nib 文件 , Control-click File's Owner-在这里 ,它是

NSApplication 对象-弹出连接窗口. 从 delegate 拖动到我们的 AppController 如图 13.5

设置 Table View 的背景颜色

当对应于一个新的 document 的 nib 文件成功 unarchived 时. 将会给我们的 Mydocument 对

象发送消息 winowControllerDidLoadNib:. 在这时候 ,我们就可以来更新 table view 的

background color 了.[其实我觉得,我们不需要去记住这些 cocoa 类的 delegate 方法,只有了解

cocoa 类的基本架构,然后查阅.h 或文档就可以了]

修改 MyDocument.m 中的这个方法如下

- (void)windowControllerDidLoadNib:(NSWindowController *)aController

{

[super windowControllerDidLoadNib:aController];

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

NSData *colorAsData;

colorAsData = [defaults objectForKey:BNRTableBgColorKey];

[tableView setBackgroundColor:

[NSKeyedUnarchiver unarchiveObjectWithData:colorAsData]];

}

Page 187: Cocoa programming for mac os

187 页

同时,为了能使用这些全局变量,确保包含了 PreferenceController.h. 编译运行程序.

思考: NSUserDefaultsController

有时,我们希望能够将 NSUserDefaults 对象的值绑定, NSUserDefaultsController 类可以做这样

的事情.程序中的所有 nib 共同使用的一个共享的 NSUserDefaultsController 对象.

例如,对于Preference panel的 check box,我们想要使用绑定来代替 target/action.我们可以将他

绑定到 NSUserDefaultsController 的 value.EmptyDocumentFlag 如图 13.6

Figure 13.6. Binding to the NSUserDefaultsController

思考:使用 Command line 来读写 Defaults

user defaults 存放在~/Library/Preferences 中.我们可以使用 defaults 命令通过命令行来编辑它

们.例如,我们可以查看 XCode 的 defaults, 打开 Terminal 输入以下命令

defaults read com.apple.Xcode

我们就可以看到 XCode 的 defaults 了.下面列出来了看到的开始几行:

{

DocViewerHasSetPrefs = YES;

NSNavBrowserPreferedColumnContentWidth = 155;

NSNavLastCurrentDirectoryForOpen = "~/RaiseMan";

NSNavLastRootDirectoryForOpen = "~";

NSNavPanelExpandedSizeForOpenMode = "{518, 400}";

NSNavPanelFileListModeForOpenMode = 1

Page 188: Cocoa programming for mac os

188 页

同样,我们也可以修改 defaults.输入下面的命令来修改 NSOpenPanel 打开的 XCode 默认的目

defaults write com.apple.Xcode NSNavLastRootDirectoryForOpen /Users

再试试这个

defaults read com.bignerdranch.RaiseMan

要看我们的全局 defaults

defaults read NSGlobalDomain

挑战

给 Preference panel 添加一个 button,可以清除所有的 user defaults. 命名为 Reset Preferences.

不要忘记了使用默认的新 defaults 值来更新 Preference 窗口

Page 189: Cocoa programming for mac os

189 页

第十四章: 使用 Notifications

用户可能使用 RaiseMan 并打开了几个 document, 然后他发现紫色的背景颜色实在是不利于

阅读文档正文. 于是,他打开 Preferences panel 修改背景颜色,不过令人失望的是,已经存在的

文档的背景颜色不会跟着改变. 于是,这个用户可能会写信给你告诉你这些. 你也许会回

复:"defualts 会在 document 创建的时候才读取,保存 document 在打开"实际上,用户想说明的

是他希望程序能立马刷新已经打开的文档. 如果这样,那该怎么做呢?我们需要把所有打开的

document 用一个 list 记录起来么?

什么是 Notification?

这个要求其实也很容易实现. 每个运行中的 application都有一个NSNotificationCenter 的成员

变量,它的功能就类似公共栏. 对象注册关注某个确定的 notification(如果有人捡到一只小狗,

就去告诉我). 我们把这些注册对象叫做 observer. 其它的一些对象会给 center 发送

notifications(我捡到了一只小狗). center 将该 notifications 转发给所有注册对该 notification 感

兴趣的对象. 我们把这些发送 notification 的对象叫做 poster

很多的标准 Cocoa 类会发送 notifications: 在改变 size 的时候,Window 会发送 notification; 选

择 table view 中的一行时,table view 会发送 notification;我们可以在在线帮助文档中查看到标

准 cocoa 对象发送的 notification

在我们的例子中,我们将 MyDocumet 对象注册为 observer. 而 preference controller 在用户改

变 color 时将发送 notification. MyDocument 在接受到该 notification 后改变 background color

在 MyDocument 对象释放前,我们必须从 notification center 移除我们注册的 observer. 一般我

们在 dealloc 方法中做这件事

Notifications 不是什么

当程序员们听到 notification center 的时候, 他们可能会联想到 IPC(进程间通讯).他们认为:"

我在一个程序中创建一个 observer,然后在另外一个程序中发送一个 notification". 这个设计

没有办法工作的, notification center 允许同一个程序中的不同对象通许,它不能跨越不同的程

序 [Notification 就是设计模式中的 观察者模式, cocoa 为我们实现了该模式, 就像 Java 也有

同样的实现一样]

Page 190: Cocoa programming for mac os

190 页

NSNotification 和 NSNotificationCenter

Notification 对象非常简单. 它就是 poster 要提供给 observer 的信息包裹. notification 对象有

两个重要的成员变量: name 和 object. 一般 object 都是指向 poster(为了让 observer 在接受

到 notification 时可以回调到 poster)

所以,notification 有两个方法

- (NSString *)name

- (id)object

NSNotificaitonCernter是架构的大脑了.它允许我们注册 observer对象, 发送 notification, 撤销

observer 对象注册

下面是它的一些常用方法

+ (NSNotificationCenter *)defaultCenter

返回 notification center [类方法,返回全局对象, 单件模式.cocoa 的很多的全局对象都是通过

类似方法实现]

- (void)addObserver:(id)anObserver

selector:(SEL)aSelector

name:(NSString *)notificationName

object:(id)anObject

注册 anObserver 对象:接受名字为 notificationName, 发送者为 anObject 的 notification. 当

anObject 发送名字为 notificationName 的 notification 时, 将会调用 anObserver 的 aSelector

方法,参数为该 notification 如图 14.1

Figure 14.1. Registering for Notifications

Page 191: Cocoa programming for mac os

191 页

如果 notificationName 为 nil. 那么 notification center 将 anObject 发送的所有

notification 转发给 observer

如果 anObject 为 nil.那么 notification center 将所有名字为 notificationName 的

notification 转发给 observer

- (void)postNotification:(NSNotification *)notification

发送 notification 至 notification center 如图 14.2

Figure 14.2. Posting a Notification

- (void)postNotificationName:(NSString *)aName

object:(id)anObject

创建并发送一个 notification

- (void)removeObserver:(id)observer

移除 observer

Page 192: Cocoa programming for mac os

192 页

发送一个 Notification

发送 notification 是其中最简单的步骤了 ,所以我们从它开始实现 .当我们接收到

changeBackgroundColor:消息时, PreferenceController 对象发送一个 notification.

我们将 notification 命名为@"BNRColorChanged" ,我们使用一个全局常量来指定.(有经验的程

序 员 会 使 用 一 个 前 缀 , 这 样 避 免 和 其 他 组 件 定 义 的 notification 混 淆 ) 打 开

PreferenceController.h 添加下面的的外部申明

extern NSString * const BNRColorChangedNotification;

在 PreferenceController.m 中定义常量

NSString * const BNRColorChangedNotification = @"BNRColorChanged";

在 PreferenceController.m 修改 changeBackgroundColor:方法

- (IBAction)changeBackgroundColor:(id)sender

{

NSColor *color = [colorWell color];

NSData *colorAsData =

[NSKeyedArchiver archivedDataWithRootObject:color];

[[NSUserDefaults standardUserDefaults] setObject:colorAsData

forKey:BNRTableBgColorKey];

NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

NSLog(@"Sending notification");

[nc postNotificationName:BNRColorChangedNotification object:self];

}

注册成为 Observer

要注册一个 observer, 我们必须提供几个要数 : 要成为 observer 的对象 ;所感兴趣的

notification 的名字;当 notification 发送时要调用的方法. 我们也可以指定要关注莫个对象的

notification.(比如说,我们需要关注莫个特定的 window 的 resize 的 notification)

编辑 MyDocument 类的 init 方法

- (id)init

{

if (![super init])

return nil;

employees = [[NSMutableArray alloc] init];

Page 193: Cocoa programming for mac os

193 页

NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

[nc addObserver:self

selector:@selector(handleColorChange:)

name:BNRColorChangedNotification

object:nil];

NSLog(@"Registered with notification center");

return self;

}

同时在 dealloc 方法,将 MyDocument 从 notification center 中移除

- (void)dealloc

{

[self setEmployees:nil];

NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

[nc removeObserver:self];

[super dealloc];

}

处理 Notification

当一个 notification 发生时, handleColorChange:方法将被调用. 目前我们在方法中简单的打印

一些 log.

- (void)handleColorChange:(NSNotification *)note

{

NSLog(@"Received notification: %@", note);

}

编译运行程序,看到了我们想要的 log 了吧

userInfo Dictionary

notification 对象的 object 变量是 poster,如果我们想要 notification 对象传递更多的信息,我们

可以使用 user info dictionary. 每个 notification 对象有一个变量叫 userInfo, 它是一个

NSDictionary 对象,用来存放用户希望随着 notification 一起传递到 observer 的其它信息.

MyDocument 将使用它来得到要改变的 color.在 PreferenceController.m 添加 userInfo

- (IBAction)changeBackgroundColor:(id)sender

{

NSColor *color = [sender color];

Page 194: Cocoa programming for mac os

194 页

NSData *colorAsData;

colorAsData = [NSKeyedArchiver archivedDataWithRootObject:color];

[[NSUserDefaults standardUserDefaults] setObject:colorAsData

forKey:BNRTableBgColorKey];

NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];

NSLog(@"Sending notification");

NSDictionary *d = [NSDictionary dictionaryWithObject:color

forKey:@"color"];

[nc postNotificationName:BNRColorChangedNotification

object:self

userInfo:d];

}

在 MyDocument.m,从 userInfo 中读取到 color

- (void)handleColorChange:(NSNotification *)note

{

NSLog(@"Received notification: %@", note);

NSColor *color = [[note userInfo] objectForKey:@"color"];

[tableView setBackgroundColor:color];

}

打开几个窗口,并改变背景颜色,现在,那些打开的窗口的背景颜色立马就变了.

思考

通常当你将自己的一个对象设置为 cocoa 某个标准对象的 delegate 的时候,你同时或许也对

该标准对象的 notification 感兴趣 . 例如 ,我们实现一个 window 的 delegate 来处理

windowShouldClose: , 我们也许会对 NSWindowDidResizeNotification 这样的 notification 感兴

趣.

如果一个 cocoa 标准对象有一个 delegate,同时它也发送 notification 的话, cocoa 对象会自动

将它的 delegate 对象注册成为 observer 来接受接受自己的 notification. 如果我们实现了一个

delegate,那么 delegate[也就是我们的对象]要怎样声明来接受 notification 呢?[方法的名字是

什么?]

方法名字其实很简单: 以 notification 名字为基准, 先将 NS 前缀去掉,接着将第一个字母改为

小写 . 在将后面的 Notification 去掉 ,然后加个冒号 :. 例如 ,为了能接受到 window 的

NSWindowDidResizeNotification, delegate 可以实现方法:

Page 195: Cocoa programming for mac os

195 页

- (void)windowDidResize:(NSNotification *)aNotification

当 window 改变大小时,这个方法将自动调用. 对于 NSWindow,我们可以在.h 或是帮助文档中

找到类似的 notification 来实现 notification 方法.

挑战

当程序不再是 active 状态是 ,让程序发出 beep. 当 unactive 时 ,NSApplication 会发送

NSApplicationDidResignActiveNotification 的 notificaiton. 而 我 们 的 AppController 是

NSApplication 的 delegate. 函数 NSBeep()可以用来发出 beep 声音

Page 196: Cocoa programming for mac os

196 页

第十五章: 使用 Alert Panels

有时候,我们可能想要通过一个 Alert Panel 来给用户一些警告信息. Alert panel 很容易生成,在

cocoa中,大部分的东西都是面向对象的,不过显示一个 alert panel却是通过一个 C函数来实现:

NSRunAlertPanel() . 下面是函数声明:

int NSRunAlertPanel(NSString *title, NSString *msg,

NSString *defaultButton, NSString *alternateButton,

NSString *otherButton, ...);

下面的代码可以生成图 15.1 的 Alert panel

int choice = NSRunAlertPanel(@"Fido", @"Rover",

@"Rex", @"Spot", @"Fluffy");

注意到 panel 上的 icon 就是程序的 icon.第二个和第三个 button 是可选的. 使用 nil 参数来代

替标题,button 就不会显示出来

函数 NSRunAlertPanel()会返回一个 int 值来指示用户点击了那个 button. 这个值会是全局常

量中的一个: NSAlertDefaultReturn, NSAlertAlternateReturn, 和 NSAlertOtherReturn.

NSRunAlertPanel()函数有多个参数输入, 其中第二个参数是一个 string,和 printf一些可以使用

一些代替符号. 从 otherButton 参数后面开始提供的参数值就是提供给第二个参数的值.所以

以下代码显示图 15.2 Alert panel

int choice = NSRunAlertPanel(@"Fido", @"Rover is %d",

@"Rex", @"Spot", nil, 8);

Page 197: Cocoa programming for mac os

197 页

lert panel 运行在 modally 模式. 在 alert panel 退出消失前,程序中的其他 windows 将不会接

受到任何的事件.

Alert 也可以运行在 sheet 模式.sheet 是莫个 window 上下拉出来的一个 window. 在 sheet 消

失前,它关联的那个 window 接受不到任何按键和鼠标事件

让用户确认删除

当用户点击 Delete button, 在删除记录前,将会以一个 sheet 方式弹出一个 Alert panel 如图

15.3

Figure 15.3. Completed Application

为了实现这个功能,首先打开 MyDocument.nib,选择 table view,打开 Inspector. 设置容许用户

多选.如图 15.4

Figure 15.4. Inspect TableView

接下来,我们设置 Delete button 发送一个消息给 MyDocument 来让用户确认删除动作.如果用

户确认要删除,那么 MyDocument 将发送 removeEmployee:消息给 array controller 来删除所选

Page 198: Cocoa programming for mac os

198 页

择的 Person

在 XCode 中, 打开 MyDocument.h 文件,添加 Delete button 将要触发的方法

- (IBAction)removeEmployee:(id)sender;

在 MyDocument.m 中实现 removeEmployeeL 方法. 我们将会先实现一个 sheet alert

- (IBAction)removeEmployee:(id)sender

{

NSArray *selectedPeople = [employeeController selectedObjects];

NSAlert *alert = [NSAlert alertWithMessageText:@"Delete?"

defaultButton:@"Delete"

alternateButton:@"Cancel"

otherButton:nil

informativeTextWithFormat:@"Do you really want to delete %d people?",

[selectedPeople count]];

NSLog(@"Starting alert sheet");

[alert beginSheetModalForWindow:[tableView window]

modalDelegate:self

didEndSelector:@selector(alertEnded:code:context:)

contextInfo:NULL];

}

这个方法会生成一个 sheet,当用户点击 sheet alert 中的一个 button,将会给 document 对象发

送 alerEnded:code:context:消息 [为什么会给 document 对象发送该消息呢?那是因为我们在

beginSheetModalForWindow 方法中将 modalDelegate 参数设置为了 self]

- (void)alertEnded:(NSAlert *)alert

code:(int)choice

context:(void *)v

{

NSLog(@"Alert sheet ended");

// If the user chose "Delete", tell the array controller to

// delete the people

if (choice == NSAlertDefaultReturn) {

// The argument to remove: is ignored

// The array controller will delete the selected objects

[employeeController remove:nil];

}

}

打开 MyDocument.nib. Control-drag Delete button 到 File's Owner.让 File's Owner 成为新的

Page 199: Cocoa programming for mac os

199 页

target [MyDocument.nib file''s Owner 占位符,将就是设置成 MyDocument 对象了]. 设置 action

为 removeEmployee: 如图 15.5

Figure 15.5. Change target/action of Delete Button

编译运行程序.

挑战

给 Alert sheet 添加另外一个 button: Keep. 当点击这个 button 时,将会把所选的 employees 的

raises 设置为 0.

Page 200: Cocoa programming for mac os

200 页

第十六章: 本地化

当你创建了一个非常有用的程序后,你希望把它分享给世界上更多的人. 不幸的是,我们不是

一个母语. 假如我们希望讲法文的朋友能使用 RaiseMan 程序. 那我们就会说:""我需要对

RaiseMan 进行法文本地化"

如果我们的程序被世界上人们所用 , 那么我们至少需要本地化下面这些语言 :

English,French,Spanish,GerMan,Dutch,Italian,Japanese 和 chinese. 我们不需要为每个语言重新

编写我们的程序.实际上,我们不需要重新编写任何 Objective-C 代码.也就是说, 世界上所有的

国家和民族可以和平友好的使用同一个可执行包.

与创建多个可执行包相反 ,我们将创建 string table 来本地化 resource.在工作目录

中,English.lproj 目录包含了所有的 English 语言本地化资源: nib 文件,图像,和声音. 为了本地

化法语,我们需要添加 French.lproj 目录, nib,图像以及声音当然是法文的. 在运行的时候程序

会根据用户在 language preference 中语言的设定来自动加载对应的 resource. [自动体现在

resource 的名字,English.lproj French.lproj]

那么在程序中那些地方需要使用到语言本地化呢?举个例子,在 MyDocument.m,有以下代码:

NSAlert *alert = [NSAlert alertWithMessageText:@"Delete

defaultButton:@"Delete"

alternateButton:@"Cancel"

otherButton:nil

informativeTextWithFormat:@"Do you really want to delete %d people?",

[selectedPeople count]];

对于每个语言,我们将要有一个 string table. 并请求 NSBundle 查找 string, NSBundle 将自动根

据用户的 language preference 的设定来查找正确的语言版本如图 16.1

Figure 16.1. Completed Application

Page 201: Cocoa programming for mac os

201 页

本地化 nib 文件

在 XCode 中,选择-不要打开-Mydocument.nib, 打开它的 Info panel. 点击 Add Localization 按

钮如图 16.2

Figure 16.2. Create a French Version of MyDocument.nib

在地区提示选择框中选择 French.

通过 Finder,我们可以发现这时候会在 French.lproj 下创建了 English.lproj/MyDocument.nib 的

副本.在XCode,Resources组下面,我们可以看到两个版本的MyDocument.nib: English和French.

如图 16.3 .双击打开 French 版本

Figure 16.3. Completed Application

Page 202: Cocoa programming for mac os

202 页

将 window 修改成如图 16.4

我们需要使用 Option 键来输入重音字符. 例如要输入é,按住 Option 键,输入 e, 然后再次输入

e.(在 System Preferences 的 International 面,你可以在你的输入菜单上添加 Keyboard Viewer,

如果你需要输入大量的非常规字符,Keyboard Viewer 能帮助你学习那些组合键能输入那些字

符 )

现在,你已经创建一个本地化资源. 注意,如果你改变了你的程序[UI 改变了],你就可能要修改

道所有的 nib 文件(French 和 English 版本).所以,等程序开发并测试通过后在去本地化它.

编译程序.在运行前,打开 System Preferences 的 International 面,设置 Françis 语言.然后运行,

这是程序会自动加载了 French 版本的资源

同样, Document 架构同时以及完成了一些本地化.例如,当你关闭一个没有存储的 document,

将会有 French 版本的询问信息出现的

String Tables

对于每个语言版本,你可以创建多个 string table. 一个 string table 就是一个以后缀名

为.strings 的文件.比如,如果你有一个 Find Panel,那么你可以创建不同语言版本的 find.strings

文件来本地化这个 Find Panel, 例如其中的 None found 字符串

string table 其实就是一些 key-value 组. key 和 value 都以双引号包起来,每组 key-vaule 以分号

结束如:

"Key1" = "Value1";

"Key2" = "Value2";

你可以通过 NSBundle 来找到对应 key 值的 vaule.

NSBundle *main = [NSBundle mainBundle];

NSString *aString = [main localizedStringForKey:@"Key1"

value:@"DefaultValue1"

table:@"Find"];

Page 203: Cocoa programming for mac os

203 页

上面的代码会在 Find.strings 中查找"Key1"对应的 vuale 字符串. 如果没有提供用户指定的语

言的本地化资源,那么就会查找第二个所选语言,如果第二个也没有本地化资源,就依次找下

去. 如果到最后还是没有找到,那么 ""DefaultValue1"将会返回. 如果你没有提供 string table

的名字,将使用 Localizebale 作为 string table 的名字. 所以,大部分程序都会每个支持的语言提

供一个 string table - Localizable.strings

创建 string table

我们来给 English 创建一个 Localizable.strings 文件,在 XCode 中选择 New File..菜单,创建一

个 empty 文件并命名为 Localizable.strings. 将它保持在 Englisth.lproj 目录中,如图 16.5

Figure 16.5. Create an English String Table

编辑该文件,添加下面内容:

"DELETE" = "Delete";

"SURE_DELETE" = "Do you really want to delete %d people?";

"CANCEL" = "Cancel";

保存(不要忘记了分号噢)

现在来 French 本地化这个文件。选择 Localizable.strings 文件,打开它的 Info Panel,创建本

地化如图 16.6

Page 204: Cocoa programming for mac os

204 页

Figure 16.6. Create a French String Table

编辑文件如下:

"DELETE" = "Supprimer";

"SURE_DELETE" =

"Etes-vous sûr de vouloir effacer ces %d personnes ?";

"CANCEL" = "Annuler";

为了保存非一般字符,你要选择 Unicode(UTF-8)的文件压码。在 French.lproj/Localizable.strings

的 info panel 中,设置文件压码为 UTF-8.在弹出的 panel 上,单击 Convert 按钮如图 16.7

Figure 16.7. Change the File Encoding

Page 205: Cocoa programming for mac os

205 页

保持文件

使用 Strings table

app 只有一个 string table,可以这样来调用,如下

NSString *deleteString;

deleteString = [[NSBundle mainBundle]

localizedStringForKey:@"DELETE"

value:@"Delete?"

table:nil];

幸运的是,在 NSBundle.h 定义了一个宏帮助完成

#define NSLocalizedString(key, comment)

[[NSBundle mainBundle] localizedStringForKey:(key)

value:@""

table:nil]

在 MyDocument.m 中,找到弹出 Alert Panel 的地方,使用下面代码替换

NSAlert *alert = [NSAlert

alertWithMessageText:NSLocalizedString(@"DELETE", @"Delete")

defaultButton:NSLocalizedString(@"DELETE", @"Delete")

alternateButton:NSLocalizedString(@"CANCEL", @"Cancel")

otherButton:nil

informativeTextWithFormat:NSLocalizedString(@"SURE_DELETE",

@"Do you really want to delete %d people?"),

[selectedPeople count]];

编译程序,将系统语言修改为 French。然后运行 App,当删除 table 中的一条记录,你就看

到了一个 French 版本的 Alert Panel。

思考:ibtool

很显然,当你本地化很多程序时,你将需要做很多的翻译工作。如果有一个自动化工具,帮

你被这些翻译 stirng 贴到 nib 文件中,就会节省很多工作。 ibtool 就是其中一个

从 terminal 运行 ibtool 命令,它可以列举一个 nib 文件中的类或对象,也可以把其中的本地

化字符串抽取出来到一个 plist 文件。下面的例子时将 English.lproj/MyDocument.nib 文件中

的本地化字符串抽取到文件 Doc.strings 文件中

> cd RaiseMan/English.lproj

Page 206: Cocoa programming for mac os

206 页

> ibtool --generate-stringsfile Doc.strings MyDocument.nib

Doc.strings 文件如下:

"100026.headerCell.title" = "Name";

这时,我们可以创建 Spanish 版本的 nib 文件。先生成 Spanish 版本的 Doc.strings,如下:

"100026.headerCell.title" = "Nombre";

然后生成 Spnish 版本的 nib

> mkdir ../Spanish.lproj

> ibtool --strings-file Doc.strings

--write ../Spanish.lproj/MyDocument.nib MyDocument.nib

你可以输入 man ibtool来获得 ibtool的帮助

> man ibtool

思考:用格式化串标明 Token 的顺序

把文本是从一种语言转换为另一种语言时,因为语序的变化,词句也会出现相应变化。例如

语句: “Ted wants a scooter. ”,在另一种语言中语序可能是“A scooter is what Ted wants ”。

假如您尝试像这样本地化格式字符串:

NSString * theFormat = NSLocalizedString(@"WANTS", @"%@ wants a %@");

x = [NSString stringWithFormat:theFormat, @"Ted", @"Scooter"];

在第一种语言中,这可以工作得很好:

"WANTS" = "%@ wants a %@";

但在第二种语言中,你需要显示的标明插入 token 的位置,token 包含 1 个数字和 1 个$符号:

"WANTS" = "A %2$@ is what %1$@ wants".

Page 207: Cocoa programming for mac os

207 页

第十七章: 自定义 View

程序中所有的可视对象要么是window,要么是 view.在这一章中,你将创建一个NSView 的子类.

随着时间的推移,你一般会需要创建自定义的 view 来完成自定义画图和事件响应.即使你没

有打算这样做,你也应该通过学习创建 view 类来了解 cocoa 的内部工作机制

window 是 NSWindow 的对象.每个 window 都会有多个 views,每个 view 描述 window 中的一

个矩形区域. view 负责该区域的画图动作以及鼠标事件响应. view 也可以响应键盘消息. 你

以及和多个 view 的子类打过交道了: NSButton, NSTextField,NSTableView,和 NSColorWell 都是

view (注意,window 不是 NSView 的子类)

View 的层次

view 是按一定层次关系组织(如图 17.1) .window 包含了一个叫做 content view 的 view.该 view

填满了整个 window 内部区域[除去 title bar. 你可以在 NSWindow 类中找到 contentView 和

setContentView 方法] 通常,content view 会有自己的子 view.而这些子 view 有会有自己的子

view.一个 view 知道自己的父 view 和子 view,并知道自己所属的窗口 [到 NSView 类声明中,

找找和它们相关的方法]

找到了么?

- (NSView *)superview;

- (NSArray *)subviews;

Page 208: Cocoa programming for mac os

208 页

- (NSWindow *)window;

任何类型的 view [这么说是指 view 的子类,如 NSButton,NSTableView...]都可以包含多个子

view.不过对于大部分类型的 view,我们不会给它添加子 view. 下面 5 中类型 view 通常有子

view

1. window 的 content view

2. NSBox. box 中的内容就是它的子 view

3. NSScrollView. scroll view 中包含的 view 就是它的子 view. scroll bar 也是它的子 view

4. NSSplitView. SplitView 中的 view 就是它的子 view 如图 17.2

5. NSTabView. 当用户点选不同的 tab 时. 不同的子 view 交替切换如图 17.3

Page 209: Cocoa programming for mac os

209 页

让一个 View 画自己

在本节中,将创建一个简单的 view. 它将自己刷成绿色.就像 17.4 [灰色的..呵呵]

新建一个 Cocoa Application 工程, 命名为 ImageFun. 点选 File->New file 菜单,创建一个

Objective-C NSView 子类.命名为 StretchView.

创建一个 View 子类的对象

打开 MainMenu.nib. 从 Library 中拖一个 CustomView(view&cell->Layout View)放置在 window

上.如图 17.5

Figure 17.5. Drop a View on the Window

Page 210: Cocoa programming for mac os

210 页

将 view 大小改变接近 window. 然后打开 info panel. 将它的类设置成为 StretchView.如图 17.6

Figure 17.6. Set the Class of the View to StretchView

大小检查

StretcView 对象是 window content view 的子 view. 这就有个有意思的问题:当父 view 改变大

小的时候, view 有什么反应呢?在 info panel 中有一个 tab 页面来指定这样的行为. 打开 Size

Info Panel. 设置如图 17.7. 现在你改变窗口大小. view 的大小会跟着变化了.

Figure 17.7. Make the View Resize with the Window

想保持 view 的等比高度,应当使 view 的底边同父 view 一同变化,你还可以让 view 右边距

同窗口一起变化。当然,如果你想让 view 始终对齐窗口的左上角,且不随父 view 一道变化,

设置如图 17.8

Page 211: Cocoa programming for mac os

211 页

Figure 17.8. Not This!

图 17.9 是 Size Inspector 设置的完整说明

保存并关闭 nib 文件

drawRect

当一个 view 需要刷新显示时,它将会收到 drawRect:的消息,参数为一个需要重画的矩形区域,

这个方法会被自动调用-你不需要直接在代码中调用. 如果你需要让一个 view重画,可以调用

Page 212: Cocoa programming for mac os

212 页

方法 setNeedsDisplays

[myView setNeedsDisplay:YES];

该方法将 myView 设置成"脏"的. 在事件处理中,myView 将被重画

[cocoa , 系统]在调用 drawView:前, 会对这个 view lock focus. 每一个 view 都有自己的

graphics context-包含了 view 的坐标系统,当前颜色,当前字体等. 当 view 被 lock focus,它的

graphics context 将激活,而当 unlock focus 后,它的 graphics context 将不是激活状态. 任何时候

的draw命令都是在当前激活的 graphics context进行 [对于Mac的画图draw, 可以看看Quarz

2D . graphics context 也是它里面的概念咯. 其实 cocoa view 画图也是通过 Quarz 2D 来实现,

要对屏幕显卡进行绘制,那么就要有一个绘制环境,这个环境也就是 graphics context , cocoa

中在每个 view 中保存了一个各自的 graphics context . 绘制到那个 view 时就将它的 graphics

context 设置为当前 Quarz 用来绘制的 graphics context - 通过 lock focus]

你可以使用 NSBezierPath 来绘制线条,圆形,曲线和矩形. 你可以使用 NSImage 来在 view 上绘

制合成图像. 在本节例子中, 绘制一个绿色的矩形

打开 StretchView.m. 添加如下代码

- (void)drawRect:(NSRect)rect

{

NSRect bounds = [self bounds];

[[NSColor greenColor] set];

[NSBezierPath fillRect:bounds];

}

如图 17.10, NSRect 结构由两个成员组成:origin - NSPoint 类型, 和 size - NSSize 类型

NSSize 结构有两个成员: width 和 height(都是 float 类型)

NSPoint 结构有两个成员:x 和 y(都是 float 类型)

Page 213: Cocoa programming for mac os

213 页

因为性能的原因 ,Objective-C 类中很少使用到结构 . 你有可能用到的一些 cocoa 结构:

NSPoint,NSRect,NSRange,NSDecimal 和 NSAffineTransformStruct 等等 . NSRange 描述区间 .

NSDecimal 描述数字精度, NSAffineTransformStruct 描述图形线性变换

注意,view 通过 bounds 知道自己的位置. 在 drawRect 中得到 bounds 区域,将当前 color 设置

为绿色,再使用当前色来填充整个 bounds 区域

通过参数传递的 NSRect 描述了这个 view 需要重画的"脏"的区域.它有可能会小于整个 view

的大小.如果绘制比较费时间的东西,可以只对该脏的区域进行重新绘制

setNeedsDisplay:将激发 view 整个可见区域重画. 如果需要激发 view 某个指定区域进行重话

可以使用 setNeedsDisplayInRect:

NSRect dirtyRect;

dirtyRect = NSMakeRect(0, 0, 50, 50);

[myView setNeedsDisplayInRect:dirtyRect];

编译运行程序.试着改变 window 的大小看看

使用 NSBezierPath 绘制

如果想绘制线条,曲线或多边形,可以使用 NSBezierPath. 前面,你使用了 NSBezierPath 的

fillRect 类方法来给 view 上色.在这节中你将使用 NSBezierPath 绘制随机点间的线条

如图 17.11

首先你需要一个成员变量来保存 NSBezierPath 对象.并创建一个方法来返回随机点 . 在

StretchView.h 中修改如下

#import <Cocoa/Cocoa.h>

@interface StretchView : NSView

Page 214: Cocoa programming for mac os

214 页

{

NSBezierPath *path;

}

- (NSPoint)randomPoint;

@end

在StretchView.m中,重载 initWithFrame方法-这是NSView的designated initializer[还记得它吧] .

它会在 view 对象创建时自动调用[在这个例子中,是在 nib 文件加载是 cocoa 调用]. 修改

StretchView.m 在 initWithFrame 中,创建了一个 path 对象

#import "StretchView.h"

@implementation StretchView

- (id)initWithFrame:(NSRect)rect

{

if (![super initWithFrame:rect])

return nil;

// Seed the random number generator

srandom(time(NULL));

// Create a path object

path = [[NSBezierPath alloc] init];

[path setLineWidth:3.0];

NSPoint p = [self randomPoint];

[path moveToPoint:p];

int i;

for (i = 0; i < 15; i++) {

p = [self randomPoint];

[path lineToPoint:p];

}

[path closePath];

return self;

}

- (void)dealloc

{

[path release];

[super dealloc];

}

// randomPoint returns a random point inside the view

- (NSPoint)randomPoint

{

NSPoint result;

NSRect r = [self bounds];

result.x = r.origin.x + random() % (int)r.size.width;

result.y = r.origin.y + random() % (int)r.size.height;

return result;

Page 215: Cocoa programming for mac os

215 页

}

- (void)drawRect:(NSRect)rect

{

NSRect bounds = [self bounds];

// Fill the view with green

[[NSColor greenColor] set];

[NSBezierPath fillRect: bounds];

// Draw the path in white

[[NSColor whiteColor] set];

[path stroke];

}

@end

编译运行程序,怎么样?酷吧! 好了,现在用[path fill] 代替[path stroke]看看,有什么不一样?

NSScrollView

在美术世界里,在同样的质量下,绘制的越大就越美观啊. 你的 view 很漂亮了.不过能不能让

它更大一点呢., 你需要将它放置在 scroll view 中如图 17.12

scroll view 由 3 个部分组成: document view, content view,和 scroll bar. 在本例中.你的 view 将

成为 document view,并显示在 content view 中-它是 NSClipView 的对象

Page 216: Cocoa programming for mac os

216 页

虽然这个看上去复杂,其实很容易办到. 实际上都不需要添加代码. 打开 mainmenu.nib 文件,

选中 view. 从 LayOut 菜单中选择 Embed Objects in Scroll View 如图 17.13

当 window 改变大小时,你希望 scroll view 跟着改变,而 document view 确不改变. 打开 Size

Inspector, 选择 Scroll view. 设置他的 Size Inspector,这样它就跟着 window 改变了如图 17.14

Figure 17.14. Make Scroll View Resize with Window

注意 view 的长和宽

Page 217: Cocoa programming for mac os

217 页

双击 scroll view 内部,选中 document view.你可以看到这是 inspecotr 的标题变成 Stretch View

Size. 将 view 的大小修改为 scroll view 的 2 倍. 同时绑定左下角并不要跟随改变大小如图

17.15. 编译运行程序

通过程序创建 View

你可以在 Interface Builder 中实例化多个 view. 有时候,你会需要通过程序来创建 view.例如,

假定你希望在 window 上创建一个 button

NSView *superview = [window contentView];

NSRect frame = NSMakeRect(10, 10, 200, 100);

NSButton *button = [[NSButton alloc] initWithFrame:frame];

[button setTitle:@"Click me!"];

[superview addSubview:button];

[button release];

Page 218: Cocoa programming for mac os

218 页

思考:cells

NSControl 从 NSView 继承得到. 因为 view 有自己的 graphics context. 这让 view 成为一个大,

高价的类. 当初,在提供 NSButton 类时, 有人要编写一个计算器程序, 他第一件事就是创建

10 行 10 列的 NSButton. 这样就有 100 个 view 别创建,效率是相当低啊. 后来,有人想到了一

个聪明的主意: 将 NSButton 的大脑移到另外一个类[大脑? 就是 button 的主要功能了](不再

是 view 类).并创建一个大的 view(叫做 NSMatrix). 用来装那 100 个 button 大脑. 我们把这个

button 大脑内叫 NSButtonCell [这个就如设计原则所说,内的聚合咯,少用继承,多用聚合,看

到好处了] 如图 17.16

到最后,NSButton 就是一个简单的 view 再加上它的大脑 NSButtonCell. button cell 做了所有事

情,而 NSButton 只是 window 上的一块绘制区域如图 17.17

同样的 .NSSlider 就是一个包含了 NSSliderCell 的 view. NSTextField 就是一个包含了

NSTextFieldCell 的 view. NSColorWell, 抱歉,它没有 cell :)

你拖一个 control 到 window 上,然后选择 Embed Objects In -> Martix. 这样就创建了一个

NSMatrix. 可以按住 Option 拖动 martix 来设定它的行列数.如图 17.18

Page 219: Cocoa programming for mac os

219 页

Figure 17.18. A Matrix of Buttons

NSMatrix 有一个 Target 和 action. Cell 也有 target 和 action. 如果 cell 点击,cell 的 target ,action

将激发,如果 cell 没有设置它的 target 和 action.那么 matirx 的 target,acton 将激发.

在处理 matirx 时,你常常要面对这样的问天,哪个 cell 激活?cell 也可以设置它的 tag

- (IBAction)myAction:(id)sender {

id theCell = [sender selectedCell];

int theTag = [theCell tag];

...

}

cell 的 tag 可以通过 Interface Builder 来设定

思考: isFlipped

PDF 和 PostScript 使用的是标准的迪卡尔坐标系统.当向上移动页面时,y 值增加. Quartz 使用

了同样的模型. view 的原点在左下点.

对于有些绘制,如果让原点在左上方会更方便. 也就是当向下移动页面是 y 增加,这时我们叫

这个 view 是 filpped 的

你通过重载方法 ifFlipped 来 filp 一个 view

- (BOOL)isFlipped

{

return YES;

Page 220: Cocoa programming for mac os

220 页

}

当我们讨论坐标系统时, x 和 y 是使用点来计数的. 一般 72 点=1 英寸. 默认的 1.0point 及时

屏幕上的一个像素.不过,你可以通过改变坐标系统来改变 point 的大小

// Make everything in the view twice as large

NSSize newScale;

newScale.width = 2.0;

newScale.height = 2.0;

[myView scaleUnitSquareToSize:newScale];

[myView setNeedsDisplay:YES];

挑战

NSBezierPath 可以会在 Bezier 曲线. 绘制看看咯.(请查看 NSBezierPath 帮助文档)