苹果开发之COCOA编程(第三版)上半部分

第一章:什么是Cocoa

1.1 历史简介

1.2 开发工具:
Xcode、Interface Builder(一个GUI构建工具)。
在它们内部,使用gcc为编译器来编译代码,并使用gdb来查找错误

1.3 语言
Objective-C的代码由gcc编译-GUN C编译器。该编译器允许自由的在同一文件中混合C、C++及Ojbective-C代码
GUN调试器——gdb,用来设置断点,运行时查看程序中变量的值

1.4 对象、类、方法和消息

对象就像C语言中的结构:它占用内存空间来保存自己的变量。这些变量被称为“成员变量”。所以在处理对象时,首先想到的一个问题是:怎样给一个对象分配空间?这个对象拥有哪些成员变量?在处理完对象后,怎么释放它?
对象的某些成员变量可能是指向其他对象的指针,这些指针使一个对象“know about(知道)”另外一个对象
类是可以用来创建对象的结构。类中定义了对象中拥有的变量,并且负责为对象申请内存空间——对象是类的一个实例
对象优于结构的地方是它可以包含函数——方法

1.5 框架
Cocoa由以下3个framework组成:
1. Foundation:所有的面向对象语言都会有一些标准数值、集合和工具类。字符串、日期、列表、线程和计时器都再Foundation框架中。
2. Appkit:所有和用户界面相关的类都在Appkit框架中。窗口、按钮、文本框、事件,以及画图类包含在Appkit中。它还有个名字:ApplicationKit
3. Core Data:它可以让你很方便地把对象存储成文件或把对象从文件中加载到内存。Core Data是一个持久性(持续性)框架

1.7 常见错误
C和OC是大小写敏感的
使用IB时,忘记连接

第二章:起步

 1.几个主要类型的项目:
Application:带有窗口的程序
Tool:没有用户界面的程序。也就是命令行工具或是后台程序
Bundle 或 Framework:可以被其他程序或者工具使用的资源目录。Bundle(也叫Plug-in)在运行时动态加载。通常应用程序在编译时需要连接某些框架

2.InterfaceBuilder
Library窗口:有用户界面对象的部件
空白窗口:代表了一个储存在你的nib文件中的NSWindow类的实例对象
File's Owner是NSApplication对象。这个对象从事件序列中获得事件,并将它们转发给相应地窗口。

@interface Foo: NSObject{
    IBOutlet NSTextField *textField
}

Foo有一个成员变量textField:它是指向一个NSTextField对象的指针

3.OC代码与Java区别:

import com.megacorp.Bar;
import com.megacorp.Baz;

public class Rex extends Bar implements Baz{
    ......
}

类Rex继承至类Bar,并实现了Baz声明的方法

#import <megacorp/Bar.h>
#import <megacorp/Baz.h>

@interface Rex : Bar <Baz>{
  ....  
}

@end

4. Objective-C中的类型和常量:OC中用到了一小部分C语言中没有的数据类型:
id:指向一个任何类型的类对象的指针
BOOL:和char一样,但是表示的是一个布尔值
Yes:1;NO:0
IBOutlet:空的宏。可以被忽略(注意,InterfaceBuilder在读取类声明的.h文件时,通过它来得到指示,哪些成员变量是outlet)
IBAction:等价于void。和IBOutlet一样,IB得到指示,哪些方法是action方法
nil:和NULL一样,对象指针赋值为空时使用nil

5.#import保证头文件只被包含一次

6.OC与Java方法区别:

public void increment(Object sender){
    .....  
}
- (void) increment:(id)sender{
    ....
}

注意:OC中所有的方法都是public的,所有的成员变量都是protected的。另外OC是C的扩展,所以可以调用标准C和Unix库提供的函数,如:random()

7. awakeFromNib
所有的对象被压缩封装在nib文件里面。在程序启动,开始接收用户事件之前,这些对象被复活。IB让开发者编辑界面对象的属性,然后保存这些状态到一个nib文件中
在事件处理之前,对象解冻之后,会自动调用awakeFromNib方法。

8. 程序的运行过程
当进程开始后,调用了NSApplication对象NSApp. NSApp读取nib文件并把其中的对象解包(解冻)。然后给每个对象发送awakeFromNib的消息,然后就开始等待接收事件了

当接收到用户的键盘或鼠标事件,window server 会把接收到的事件放到适当的应用程序的事件队列中。NSApp从队列中读取事件并转发给界面对象(比如一个按钮),这时我们自己的代码将被调用。如果我们自己的代码改变了某些view,这些view将重画。这样一个事件检查和反馈的过程就是 main event loop,如下图:

第三章:Objective-c 语言

1.NSMutableArray *foo = [[NSMutableArray alloc] init];
alloc方法返回一个指针,指向为这个对象分配好的空间。记住foo只是一个指针变量。在使用foo前必须对其进行初始化,发生init消息来完成初始化。方法init返回一个新的初始化后的对象
NSMutableArray *array; 这样只是声明了一个指针变量,指向一个NSMutableArray对象,此时还没有任何array对象存在

2. #import <Foundation/Foundation.h> : 包含了Foudation框架中所有类的头文件。因为这些头文件都是预编译的,所有编译它们不想要很多时间

3.

4.NSString 对应 C中的char*

5.NSObject、NSArray、NSMutableArray、NSString

NSObject是OC类继承树的根类。它有一个:- (NSString *)description 方法返回一个NSString对象来描述接收者。在调试器中使用po命令,就会调用此方法。同样,使用%@,也会调用此方法
- (BOOL)isEqual:(id)anObject : 此方法定义为当接收者和anObject为同一个对象时就相等——也就是它们指向相同的内存地址。
NSString重载了这个方法,比较接收者和anObject的字符是否相等

NSArray:一个NSArray对象包含指向其他对象的指针列表。指针有一个唯一的整型索引:中间不能有nil,这和java不一样。常用方法:
- (unsigned)count;
- (id)objectAtIndex:(unsigned)i
- (id)lastObject;
- (BOOL)cotainsObject:(id)anObject;
- (unsigned)indexOfObject:(id)anObject;

NSMutableArray:继承于NSArray,扩展了增加、删除对象的功能,可以使用NSArray的mutableCopy方法复制得到一个可修改的NSMutableArray对象。常用方法:
- (void)addObject:(id)anObject;//在接收者的最后插入anObject
- (void)addObjectFromArray:(NSArray *)otherArray;//
- (void)insertObject:(id)anObject atIndex:(unsigned)index;//在索引index处插入。如果index被占用,会把index索引之后的对象向后移动,腾出一个空间
- (void)removeAllObjects;
- (void)removeObject:(id)anObject;
- (void)removeObjectAtIndex:(unsigned)index;
不能将nil加到array中,但如果要给array加一个空的对象,可以使用NSNull来给array添加一个站位对象:
[myArray addObject:[NSNull null]];

NSString:可以存储Unicode字符。它继承自NSObject。常用方法:
- (id)initWithFormat:(NSString *)format,...
- (unsigned int)length;
- (NSString *)stringByAppendingString:(NSString *)aString;

6.继承和组合
建议不要使用继承:如创建NSString、NSMutableArray的子类。最好是让一个较大的对象来包含NSString或NSMutableArray
Objective-c的基本编程思想是:大多数情况下,选择“有一个”,而不是“是一个”
对于强制类型语言,如C++,继承非常重要。但是对于非强制类型语言,如OC,继承就不那么重要了。

7.创建自己的类
NSCalendarDate继承自NSDate类。NSCalendarDate对象包含了日期、时间、时区及一个带有格式的字符串,如:
NSCalendarDate *now = [NSCalendarDate calendarDate];calendarDate创建并返回一个本地日期和时间的默认格式的NSCalendarDate对象,它的时区为机器所设置

+ (id)dateWithYear:(int)year month:(unsigned)month day:(unsigned)day hour:(unsigned)hour minute:(unsigned)minute second:(unsigned)second timeZone:(NSTimeZone *)aTimeZone;这个类方法返回一个自动释放的对象:
NSCalendarDate *hotTime = [NSCalendarDate dateWithYear:2000 month:12 day:30 hour:16 minute:59 seconds:59 timeZone:[NSTimeZone timeZoneWithName:@"PST"];

+ (NSCalendarDate*)dateByAddingYears:(int)year.......此方法返回参数指定的偏移量的日历对象

- (int)dayOfCommonEra;//返回从1A.D到现在的天数
- (int)dayOfMonth;//返回这个月的第几天(1到31)
- (int)dayOfWeek;//返回一周中的星期几(0到6,0是星期天)
- (int)dayOfYear;//返回一年中得第几天(1-366)
- (int)hourOfDay;//返回接收时的小时值(0到23)
- (int)minuteOfHour;//返回接收时的分钟值(0-59)
- (int)monthOfYear;//(1-12)
- (NSDate *)laterDate:(NSDate*)anotherDate;//此方法继承自NSDate,将接收者和anotherDate比较,返回两者中靠后的那个
- (NSTimeInterVal)timeIntervalSinceDate:(NSDate*)anotherDate;//此方法计算接收时与anotherDate之间以秒计算的时间差
- (void)setCalendarFormat:(NSString *)formart;//设置接收者的默认日历格式。日历格式是由一个包含日期转换说明的字符串,如下图:

编写Initializers(初始化器)、带参数的初始化器
创建initializer的规范:
如果父类的initializer够用,不要创建自己的initializer
如果要创建自己的initializer,一定要重载父类的designated initializer
如果创建了多个initializer,让其中一个做真正的初始化工作-designated initializer,其他的都调用它
你的designated initializer将要调用父类的designated initializer

总有一天你会遇到这种情况:你的类必须要有一个参数来进行初始化,那么你可以重载父类的designated initializer来抛出一个异常:

- (id)init
{
    [self dealloc];
    @throw [NSException exceptionWithName:@"BNRBadInitCall" reason:@"Initialize Lawsuit with initWithDefendant:" userInfo:nil];

    return nil;
}

8.调试器
NSAssert(theDate != nil, @"Argument must be non-nil");

9.消息机制的工作原理
类就像一个C结构体。NSObject声明了一个成员变量叫isa。因为NSObject是整个类继承树的根类,所以所有对象都会有一个isa指针,指向创建对象的类结构。而该isa变量指向该对象的类。类结构中包含了类定义的成员变量的名字和类型,以及实现了哪些方法。还包含一个指针,指向自己的父类结构

第四章:内存管理

1.Apple的两套解决方案:retain、垃圾收集器(iOS中是没有的)
如果使用垃圾收集器,当不再需要某个对象时,就不要再指向它了。可以指向nil

2.实现dealloc
当retain计数为1时调用release,对象的dealloc方法将被调用。在dealloc方法中,你必须释放自己所retain的对象并且调用父类的dealloc方法

3.创建自动释放对象

- (NSString *)description
{
    NSString *result;
    result = [[NSString alloc] initWithFormat:@"%@", firstname];
    return result;
}

上面的代码有个内存泄露。alloc方法创建一个string对象,它的retain计数为1.在其他对象调用description方法得到string对象后,它会retain该string对象,此时string对象的retain计数为2。

- (NSString *)description
{
    NSString *result;
    result = [[NSString alloc] initWithFormat:@"%@", firstname];
    //[result release];//错误的解决
[result autorelease];//正确地解决方式
return result; }

错误的解决代码将不能正常工作。当调用release方法时,string对象的retain计数变为0,所以string对象将被释放。返回的是一个已经被释放的对象。
在没有启用垃圾收集器时,使用NSAutoreleasePool来实现它。
给对象发送autorelease消息后,对象将被添加到当前的自动释放池中。当自动释放池被释放时,首先会给池中得对象发送release消息。换句话说,当给一个对象发送autorelease消息时,就表示将来会给该对象发送release消息。
通常在Cocoa程序中,在接收事件时会创建自动释放池,在处理完事件后释放自动释放池。这样除非在中间对对象进行了retain,否则,在一个事件处理完后,所有的autorelease对象将被释放。

4.Release相关规则
使用alloc、new、copy或mutaleCopy创建对象,其retain计数为1,并且不会添加到自动释放池中
当使用任何方法得到一个对象,假定这个对象的计数为1,或者已经添加到了当前的自动释放池中,如果不希望它随着当前的自动释放池一起被释放,一定要调用retain方法

因为我们经常要使用,但又不希望retain对象,所以很多的类都提供了一些类方法来返回自动释放对象。如NSString的stringWithFormat。所以上面代码可简单修改为:

- (NSString *)description
{
    NSString *result;
    result = [[NSString alloc] stringWithFormat:@"%@", firstname];
    return result;
}

5.临时对象:注意,在Cocoa程序中,对象要等到事件处理完成后才被自动释放,这样在中间一定会有临时对象存在。如:

//将一个NSString对象的序列,把其中的字符串对象变成大写,并连接一个字符串对象返回
- (NSString *)concatenatedAndAllCaps:(NSArray *)myArray
{
    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个自动释放对象:13个uppercaseString,13个stringWithFormat:。除了返回的一个字符串对象可能被retain,其他的都在事件处理完成后释放。

6.Accessor方法
如果成员变量不是指针类型,accessor方法很简单:

- (int)foo{
    return foo;
}
- (int)setFoo:(int)x{
    foo = x;
}

当foo是指向其他对象的指针 ,在setter方法中,必须要retain新设置的对象,同时release原来的对象

//三种习惯用法
- (void)setFoo:(NSCalendarDate *)x{
    [x retain];
    [foo release];
    foo = x;
}//评价:如果它们指向同一个对象,retain和release都是多余的
- (void)setFoo:(NSCalendarDate *)x{
    if (foo != x){
        [foo release];
        foo = [x retain];
    }
}//评价:只有当foo和x指向不同对象时,才会去改变。必须指向一次额外的if语句
- (void)setFoo:(NSCalendarDate *)x{
    [foo autorelease];
    foo = [x retain];
}//评价:如果存在retain计数的使用错误,那么只有当事件结束后才出现,这样不利于调试查找错误,而且,自动释放会影响一定得性能

getter方法和非指针类型一样

- (NSCalendarDate *)foo{
    return foo;
}

注意不要将方法命名为getFoo(像java一样)。在OC的编程习惯中,使用get作为前缀,表示需要拷贝对象内部的数据,如:
[myColor getRed:&r green:&g blue:&b alpha:&a];//&返回变量的地址

第五章:Target/Action

1.NSButton、NSSlider、NSTextView、NSColorWell等控件都是NSControl的子类。每个控件都包含target和action。target是一个指向其他对象的指针。action是会发给target的message(selector)

当用户和控件交互时,就会给它们的target发送action消息。例如,点击一个按钮,将会给它的target发送action消息

action方法接收一个参数:发送者。该参数可以让接收者知道是哪一个控件发送了这个action消息

2.常用的NSControl子类
NSButton:
- (void)setEnabled:(BOOL)yn;//激活按钮。非激活的按钮是灰色的
- (int)state;//如果按钮是on状态,返回NSOnState(1),为off状态时,返回NSOffState(0)
- (void)setState:(int)aState;

NSSlider:
- (void)setFloatValue:(float)x;//移动滚动条到位置x
- (float)floatValue;//得到当前滚动条的位置

NSTextField:让用户输入单行文本
- (NSString *)stringValue;
- (void)setStringValue:(NSString *)aString;//这两个方法用来获取和设置文本框中的文本
- (NSObject *)objectValue;
- (void)setObjectValue:(NSObject *)obj;//获取和设置文本框内容数据的任意类型的对象

3.通过代码来设置target:
控件的action是一个selector:
- (void)setAction:(SEL):aSelector;
如何获取一个selector?使用Objective-c编译器指令@selector告诉编译器来查找一个selector。例如,要设置一个按钮的action为drawMickey:,可以:

SEL mySelector;
mySelector = @selector(drawMickey:);
[myButton setAction:mySelector];

如果要在运行时查找selector,使用NSSelectorFromString()函数:

SEL mySelector;
mySelector = NSSelectorFromString(@"drawMickey:");
[myButton setTarget:someObjectWithADrawMickeyMethod];
[myButton setAction:mySelector];

第六章:Helper对象

1.委托
Cococa框架很多类都有一个叫delegate的实例变量。可以让这个变量指向一个helper对象

2.对象委托。委托是一个设计模式,下面是AppKit框架中有delegate outlet的一些类:
NSAlert、NSAnimation、NSApplication、NSBrowser、NSDatePicker、NSDrawer、NSFontManager、NSImage、NSLayoutManager、NSMatrix、NSMenu、NSPathControl、NSRuleEditor、NSSavePanel、NSSound、NSSpeechRecognizer、NSSpeechSynthesizer、NSSplitView、NSTableView、NSText、NSTextField、NSTextStorage、NSTextView、NSTokenField、NSToolbar、NSWindow

3.委托是如何工作的
NSObject有下面这个方法:
- (BOOL)respondsToSelector:(SEL)aSelector;
因此每个对象都有这个方法。如果该对象有一个叫aSelector得方法,它就会返回YES
注意,只有委托对象实现了委托方法,它才会收到消息。如果没有实现,则执行默认动作(respondsToSelector的返回结果通常会缓存在delegate outlet的对象中,性能会比较好)
如果要观察是否存在某个委托方法的检查过程,可以在委托对象中重载respondsToSelector方法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    NSString *methodName = NSStringFromSelector(aSelector);
    NSLog(@"respondsToSelector:%@", methodName);
    return [super respondsToSelector:aSelector];
}

4.创建一个委托

第七章: Key-Value Coding;Key-Value Observing

1.KVC:Key-value coding 机制允许通过变量名设置(set)以及获取(get)变量值。变量名只是个字符串,但我们一般称之为key。
因此如果类Student有一个firstName的变量,类型为NSString:

@interface Student:NSObject
{
    NSString *firstName;  
}
...
@end

就可以像下面这样设置和获取Student实例的firstName

Student *s = [[Student alloc] init];
[s setValue:@"Larry" forKey:@"firstName"];
//获取
NSString *x = [s valueForKey:@"firstName"];

setValue:forKey:和valueForKey:的方法是在NSObject中定义的

2.key-value coding方法只能处理对象,所以不能传入int,而要是NSNumber,但是它在赋值时会自动把NSNumber转换成其他基础数据结构,如int

3.绑定
在Cocoa中,很多图形对象都有bindings。当你绑定一个key,如fido,到一个图形对象的属性上(比如它的值或者它的颜色),视图会自动保持它们之间的同步(使用key-value coding保持同步)。

4.Key-Value Observing
当fido值改变而对象不变的时候会发生什么?图形对象怎么知道它有一个新值?
当图形对象(如文本框)创建后,它会告诉AppController它正在观察fido key(view观察key。key改变,controller通知view)。一旦fido的值被存取方法或者key-value coding改变,AppController就会给图形对象发生消息,通知它fido的值被修改了。

5.让Keys可被观察
如果直接修改变量的值,必须要显示触发并通知观察者:

- (IBAction)incrementFido:(id)sender
{
    [self willChangeValueForKey:@"fido"];
    fido++;
    [self didChangeValueForKey:@"fido"];
}

6.Properties和它们的Attributes
OC 2.0后使用如下一个property声明来替代fido和setFido方法:

@interface AppController:NSObject{
    int fido;
}
@property(readwrite, assign) int fido;
@end//这代码等价于声明setFido:和fido方法

在m文件中,用@synthesize来实现存取方法:@synthesize fido

7.Property的Attributes
@property (attributes) type name;
attributes包括readwrite(默认)、readonly(只有获取方法,没有设置方法)、assign、retain、copy:
assign:默认。创建一个简单地赋值语句。assign不保留新值,如果你处理的object类型,并且没有使用垃圾收集器,不要使用assign
retain:释放旧值,并retain新值。这个属性仅针对Objective-c对象类型时使用。如果使用垃圾收集器,assign和retain等价
copy:创建新值的拷贝,并让变量等于这个拷贝。这个属性常用在property是字符串的时候

attributes还可以包括nonatomic。如果程序是多线程的,设置方法时应该是原子的(atomic)。如果应用程序没有使用垃圾收集器,将会使用一个锁来确保每次只有一个线程执行该设置方法。创建和使用锁需要额外开销。

8.Key Paths
对象之间的关系经常是网状的。如一个人有一个配偶,配偶有一部踏板车,踏板车有一个型号:

为得到某个人的配偶的踏板车的型号,可以使用key path:

NSString *mn;
mn = [selectedPerson valueForKeyPath:@"spouse.scooter.modelName"];

我们可以说spouse以及scooter和Person类有关联,而modelName是Scooter类的attribute
你甚至可以在key path中使用操作符。例如,如果有一个Person的对象数组,可以通过使用key path来获得他们的平均expectedRaise:

NSNumber *theAverage;
theAverage = [employees valueForKeyPath:@"@avg.expectedRaise"];

下面是常用的操作符:
@avg、@count、@max、@min、@sum

了解了key path,现在就可以通过编程创建binding。如果有一个文本框里要显示一个array controller的arranged objects的平均加薪期望值,可以像下面只有创建一个绑定:

[textField bind:@"value" toObject:employeeController withKeyPath:@"arrangeObjects.@avg.expectedRaise" options:nil];
//解除绑定
[textField unbind:@"value"];

当然,在IB创建绑定更容易

9.Key-value Observing

文本框是如何成为AppController对象的fido key的观察者的?当从nib中唤醒文本框时,它将自己添加为观察者。如果想成为一个key的观察者,代码应该是这样的:

[theAppController addObserver:self forKeyPath:@"fido" options:NSKeyValueObservingOld context:somePointer];

这个方法是在NSObject中定义的。它就像是说:”嗨,当fido值改变的时候,给我发个消息吧“。option和context决定当fido值改变的适合,应该将那些额外的数据随消息一起发送出去。
触发方法如下:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void*)context
{
   .......
}

在这个例子宏,keyPath应该是@"fido",object应该是AppController,context则成为一个观察者时的上下文的指针,change字典是一个key-value对集合,保存fido的旧值和/或其新值

第八章:NSArrayController

1.MVC设计模式
Model:模型类型描述你的数据。View:视图类是GUI的一部分,是通用类。Controller:控制器类,负责控制整个应用的流程

2.NSController
NSController是一个抽象类。NSObjectController是它的子类,它的content是一个对象。NSArrayController的content则是一个对象数组

第九章:NSUndoManger

1.NSUndoManager可以给应用程序增加撤销功能。当添加、编辑以及删除对象时,undo manager(撤销管理器)会记录下undo这些修改需要的所有信息;而当撤销时,undo manager则会记录下redo这些修改的所有消息。这个机制使用两个NSInvocation对象栈

2.NSInvocation
把消息(包括selector(选择器)、接受者以及所有的参数)包装成一个对象,这样调用起来会很方便。这个对象就是NSInvocation的实例
invocation的最佳应用场合是消息转发。当一个对象收到一条它不理解的消息的时候,消息发送机制会在报错之前检查对象是否实现了下面这个方法:
- (void)forwardInvocation:(NSInvocation *)x;
如果对象有这个方法,消息就会被打包成NSInvocation对象,然后调用forwardInvocation:方法

3.假如用户打开RaiseMan文档并做了3处修改:
插入一条记录;把姓名从“New Employee”修改为“Rex Fido”; 把raise从0修改为20
每次修改完成后,你的控制器就会添加一个可以撤销本次修改的invocation到撤销栈中。下图显示了在做完这3次修改后,撤销栈的状态

如果用户点击Undo菜单,就从堆栈中取出第一个invocation并调用它。每次当一个元素从撤销栈中弹出并被调用的时候,undo操作的逆操作会被添加到redo栈中

撤销管理器的运作非常巧妙:当用户做修改的时候,undo invocation会被添加到undo栈中,当用户撤销修改的时候,undo invocation回到redo栈中。而当用户重做修改时,undo invocation又回到undo栈上。这些任务会被自动处理,你唯一要做的工作就是提供对应的逆操作,添加到撤销管理器中

假设你写了一个方法叫makeItHotter以及这个方法的逆方法makeItColder,然后就可以像下面这样实现撤销功能:

- (void)makeItHotter
{
    temperature = temperature + 10;
    [[undoManager prepareWithInvocationTarget:self] makeItColder];
    [self showTheChangesToTheTemperature];
}

prepareWithInvocationTarget:方法记下了target并返回撤销管理器本身,然后,撤销管理器重载了forwardInvocation方法,这样它就把makeIteColder方法的invovation添加到undo栈中
为了完成这个例子,还必须实现:

- (void)makeItColder
{
    temperature = temperature - 10;
    [[undoManager prepareWithInvocationTarget:self] makeItHotter];
    [self showTheChangesToTheTemperature];
}

每一栈的invocation是按组放置的。默认的,单个事件涉及的所有invocation放在同一个组里。因此,如果一个用户操作(单个事件)导致多个对象发送变换,只要点击一下撤销菜单,所有的改变都会被撤销

如何获得一个撤销管理器?可以显示创建一个。注意:NSDocument的实例已经有一个它自己的撤销管理器了
NSUndoManager *undo = [self undoManager];

4.Key-value Observing
kvc是一种通过变量名来读取及设置变量的方法,kvo允许当值发生变化时通知你。

为了能够撤销编辑,当对象的变量的值发送改变时,需要通知文档对象。使用addObserver.....方法

插入后开始编辑

5.窗口和撤销管理器
视图可以添加编辑到撤销管理器。如,NSTextView可以把用户所做的每一个文字修改放到撤销管理器中。可以在IB中打开它
文本视图如何知道使用哪个撤销管理器?首先,询问它的委托对象,NSTextView的委托对象可以实现这个方法:
- (NSUndoManager *)undoManagerForTextView:(NSTextView *)tv;

然后询问它的窗口,针对这个目的,NSWindow有一个方法:
- (NSUndoManager *)undoManager;
窗口的委托对象可以通过实现下面的方法来给窗口通过一个撤销管理器
- (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)window;

第十章:Archiving

1.面向对象的程序在运行的时候,会创建一个复杂的对象图。经常会要以二进制流的方法序列化这个对象图,这个过程叫archiving。二进制流可以通过网络发送或者写入文件中。(Java中称这个过程为serialization,而不是archiving)

当你要从二进制流中重修创建对象图时,可以unarchive它。例如,当应用程序开始运行的时候,它会从Interface Builder创建的nib文件中unarchive对象
尽管对象有实例变量和方法,但只有实例变量和类名会被archive。换句话说,只有数据,而不是代码,会被存档。
因此,如果一个应用archive了一个对象,而另一个应用unarchive了这个对象,这两个应用必须有这个类的相关代码。
例如,在nib文件中,你使用了AppKit框架的NSWindow和NSButton类。如果没有链接AppKit框架,你的应用就不能为存档中得NSWindow喝NSButton类创建实例

2.NSCoder和NSCoding

NSCoding是一个protocol,有下面2个方法:
- (id)initWithCoder:(NSCoder *)coder;
- (void)encodeWithCoder:(NSCoder *)coder;

NSCoder是一个抽象的二进制流。你可以把数据写入coder或者从coder中读取数据。对象的initWithCoder:方法会从coder中读取数据,并把数据保存到它相应地实例变量中。对象的encodeWithCoder:方法会读取实例变量,并把这些数据写入到coder中去。
NSCoder是一个抽象类。抽象类不能被实例化,它只提供了一些想让子类继承的方法:
NSKeyedUnarchiver从二进制流中读取对象;NSKeyedArchiver把对象写到二进制流中

编码

NSCoder的常用方法:
- (void)encodeObject:(id)anObject forKey:(NSString *)aKey;
这个方法把anObject写到coder中,让它和key aKey关联,还会调用anObject的encodeWithCoder:方法

对于每种通用的C primitive types(原始类型,如int和float),NSCoder都有一个encode方法:
- (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;

添加下面的方法到Person.m,给Person类增加encoding能力:

- (void)encodeWithCoder:(NSCoder *)coder
{
    [coder encodeObject:personName forKey:@"personName"];
    [coder encodeFloat:expectdRaise forKey:@"expectdRaise"];
}

NSString实现了NSCoding protocol,所以personName知道如何自己编码
所有常用的AppKit和Foundation类都实现了NSCoding protocol,除了NSObject。因为Person继承自NSObject,它不会调用[super encodeWithCoder:coder]。
如果Person的父类实现了NSCoding protocol,方法可能会是下面这样的:

- (void)encodeWithCoder:(NSCoder *)coder
{
    [super encodeWithCoder:coder];
    [coder encodeObject:personName forKey:@"personName"];
    [coder encodeFloat:expectdRaise forKey:@"expectdRaise"];  
}

调用父类的encodeWithCoder:方法给父类提供一个机会,让它把变量写到coder中,因此,继承树上的每个类只负责把它自己的实例变量——而不是它的父类的实例变量——写到coder中

解码

从coder中解码数据时,会使用类似下面的方法:
- (id)decodeObjectForKey:(NSString *)aKey;
- (BOOL)decodeBoolForKey:(NSString *)aKey;
- (double/float/int)......

如果数据流中没有某个key的数据,那么得到的结果将为空。如为float类型,则返回0.0,如果是对象,则返回nil

给person类解码,添加下面代码到Person.m中:

- (id)initWithCoder:(NSCoder *)coder
{
    [super init];
    personName = [[coder decodeObjectForKey:@"personName"] retain];
    expectdRaise = [coder decodeFloatForKey:@"expectdRaise"];
    return self;
}

再次强调,不能调用Person父类的initWithCoder:的实现,因为NSObject没有这个方法。如果Person父类已经实现了NSCoding protocol,方法就应该是下面这样:

- (id)initWithCoder:(NSCoder *)coder
{
    [super initWithCoder:coder];
    personName....
    ....
}

第三章讲到designated initializer负责所有的工作并调用父类的designated initializer。所有其他的initializer调用designated initializer。Person已经有一个init方法,并且它是designated initializer,但这个新的initializer却没有调用它。这是对的,initWithCoder:是initializer规则的一个例外

3.文档架构
多文档应用程序之间有很多共同点。它们都能创建新文档,打开已有文档,保存或打印当前文档,以及当用户关闭窗口或者退出程序的时候,提醒用户保存已修改的文档。
Apple提供了3个类——NSDocumentController、NSDocument以及NSWindowController 帮助处理这些细节。这3个类构成了document architecture(文档架构)
当应用程序启动的时候,它会从Info.plist中读取信息,了解它处理的文件类型。如果发现这是一个document-based(基于文档)的应用程序,它就会创建一个NSDocumentController。你很少用到它,它潜藏在后台,为你打理很多细节问题。例如,当你在菜单中选择新建或者保存全部时,document controller就会处理这些请求。如果必须给document controller发送消息,应该:

NSDocumentController *dc = [NSDocumentController sharedDocumentController];

文档控制器有一个文档对象的数组对应每一个打开的文档

文档对象是NSDocument子类的实例。对多数应用而已,只需要扩展NSDocument来做想做的事情,不需要操心NSDocumentController或NSWindowController

菜单Save、Save As、Save All和Close是各不相同的,但它们处理的是相同的问题:把模型存入文件或者文件包中(文件包是一个目录,但对用户而言,看上去像是一个文件)。要处理这些菜单项,你的NSDocument子类必须实现下面3个方法之一:
- (NSData *)dataOfType:(NSString *)aType error:(NSError *)e;//文档对象把模型作为一个NSData对象存入文件中。NSData本质上就是一个字节缓冲区。在一个基于文档的应用程序中,这是最容易,也是最普遍的保存内容的方法
- (NSFileWrapper *)fileWrapperOfType:(NSString*)aType error:(NSError *)e;//文档对象以NSFileWrapper对象的格式返回它的模型。它会根据用户的选择,把自己写到文件系统的某个位置
- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError**)outError;//文档对象得到一个URL地址以及类型,然后负责把数据存到这个URL地址(URL一般只是文件系统中的一个文件)。outError:如果方法不能完成任务,它就创建一个NSError,并把该error指针放在指定的位置

文档的载入:Open、Open Recent、Revert To Saved:
- (BOOL)readFromData:(NSData*)data ofType:(NSString*)typeName error:(NSError **)outError;
- (BOOL)readFromFileWrapper:(NSFileWrapper*)fileWrapper ofType:(NSString*)typeName error:(NSError*)outError;
- (BOOL)readFromURL:(NSURL*)absoluteURL ofType:(NSString*)typeName error:(NSError**)outError;

当我们打开一个文件时,首先读取文档文件,再读取nib文件。因此,在文件载入完成之前,不能发送消息给用户界面对象(因为他们还不存在)。在nib载入之后,文档对象会收到下面的消息:
- (void)windowControllerDidLoadNib:(NSWindowController *)x;

4.保存和NSKeyedArchiver
+ (NSData *)archivedDataWithRootObject:(id)rootObject;//此方法把对象存档到NSData对象的字节缓冲区中

5.载入和NSKeyedUnarchive:
+ (id)unarchiveObjectWithData:(NSData*)data;

6.防止死循环
如果对象A导致对象B被编码,对象B导致对象C被编码,然后对象C又导致对象A再次被编码,这就是死循环。设计NSKeyedArchiver的时候已经考虑这种情况:
当一个对象被编码的时候,一个唯一的token也会被放置到数据流中。一旦被存档,对象就会被添加到一个已编码对象表对应的那个token下。当要再次编码同一个对象时,NSKeyedArchiver只是简单地在流中加入token。

当从流中解码对象的时候,NSKeyedUnarchiver会同时把对象和token放到表格中。如果它找到一个token没有相对应的数据,unarchiver知道到表格中去找对象,而不是新建一个实例

- (void)encodeConditionalObject:(id)anObject forKey:(NSString*)aKey;
这个方法在这种情况下使用:对象A有一个指针指向对象B,但对象A并不真的关心对象B是否已经存档;如果另外一个对象已经存档了B,A会把B的token放置到流中。但如果B没有被其他对象存档,它会被像nil一样对待

7.统一类型标识符
universal type identifiers(UTIs)。一个UTI就是一个标记文件类型的字符串。UTI具有层次结构

第十一章:Core Data基本原理

1.在运行时,程序读取模型文件来生成一个NSManagedObjectModel对象。
模型使用了不同的术语。类称为实体,成员变量称为property。模型包含了两张property:attributes和relationships。attribute保存简单数据类型,如字符串、日期、数值

2.Core Data是怎么工作的
下图为对象关系图

NSPersistentDocument读取创建好的数据模型来生成一个NSManagedObjectModel对象。在本例中,managed object model有一个NSEntityDescription来描述Car实体。实体描述中包含了多个NSAttributeDescription对象
一旦有了模型,persistent document创建一个NSPersistentStoreCoordinator对象和一个NSManagedObjectContext对象。NSManagedObjectContext对象会从数据模型中取得NSManagedObject对象。当这些managed objected加载到内存的时候,managed object context就会监测这些对象

第十二章:Nib文件和NSWindowController

1.NSPanel继承NSWindow

2.File's Owner
当一个程序运行了一段时间后,需要加载一个新的nib文件,那么之前已经存在的对象就需要一些连接来访问这个新加载的nib文件中的对象。File's Owner提供了这样的连接。
在nib文件中,File's Owner其实就是一个已经存在的对象的占位符。加载nib文件的对象需要提供nib的所有者对象。所有者将取代File's Owner的位置

3.NSBundle
NSBundle是一个目录,其中包含了程序会使用到的资源,这些资源包含图像、声音、编译好的代码、nib文件(用户经常也把bundle称为插件)。NSBundle类来处理bundle

你的程序本身就是一个bundle。使用下面的代码得到main bundle:
NSBundle *bundle = [NSBundle mainBundle];

如果你需要其他目录的资源,可以指定路径来取得bundle:
NSBundle *goodBundle = [NSBundle bundleWithPath:@"~/.myApp/Good.bundle"];
一旦有了NSBundle对象,那么就可以访问它里面的资源了:
NSString *path = [goodBundle pathForImageResource:@"Mom"];//后缀名是可选的
NSImage *momPhoto = [[NSImage alloc] initWithContentsOfFile:path];

bundle可以包含一个库,如果从bundle中获取一个类,bundle会连接库,并通过名字查找这个类:
Class newClass = [goodBundle classNamed:@"Rover"];
id newInstance = [[newClass alloc] init];
如果不知道类的名字,也可以查找主类:
Class aClass = [goodBundle principalClass];
id anInstance = [[aClass alloc] init];

不通过NSWindowController,可以直接使用NSBundle加载nib文件:
BOOL successful = [NSBundle loadNibNamed:@"About" owner:someObject];//owner:指定一个对象作为nib的File's Owner

第十三章:User Default

1.通过NSUserDefaults类来注册程序的出厂设置,保存用户偏好设置,以及读取之前保存的用户偏好设置
一般在用户Home目录 ~/Library/Preferences 中可以找到数据库文件。一般为property list 格式

2.NSDictionary和NSMutableDictionary
id anObject = [dictionary objectForKey:@"foo"];//如果字典中没有对应的键,返回nil
字典使用哈希表来实现,查找速度很快。
NSDictionary常用方法:
- (NSArray *)allKeys;
- (unsigned)count;
- (id)objectForKey:(NSString *)aKey;
- (NSEnumerator *)keyEnumerator;//可以用这个方法来迭代出集合中的所有成员。这个方法是从一个字典中得到所有键的迭代器

NSEnumerator *e = [myDict keyEnumerator];
for (NSString *s in e){
    NSLog(@"key is %@, value is %@", s, [myDict objectForKey:s]);
}

NSMutableDictionary
+ (id)dictionary;//创建一个空得字典
- (void)removeObjectForKey:(NSString *)aKey;
- (void)setObject:(id)anObject forKey:(NSString *)aKey;//使用aKey和anObject组成一条记录,添加到字典中。在添加之前,将会给anobject发送retain消息。如果aKey已经存在于字典中,那么会移除原来对应的值对象,使用新的anobject代替,同时给原来的值对象发送release消息

3.NSUserDefaults
程序每次启动时,首先要加载出厂defaults,这个过程叫:registering defaults。注册完成后,将使用用户defaults配置用户所需,这个过程叫:reading and using the defaults。用户defaults数据库中得数据将会自动从文件系统中读取。
你也有可能创建一个首选项面板来让用户设置defaults,对defaults对象的改变会自动写入文件系统中。这个过程叫:setting the defaults

NSUserDefaults类的常用方法:
+ (NSUserDefaults *)standardUserDefaults;//返回共享的defaults对象
-  (void)registerDefaults:(NSDictionary *)dictionary;//注册程序的defaults
//修改和保存defaults
-  (void)setBool:(BOOL)value forKey:(NSString*)defaultName;
-  (void)setFloat/Integer/Object.....
//读取defaults
- (BOOL)boolForKey:(NSString*)defaultName;
- (float)floatForKey:(NSString*)defaultName;
- (int/id)integerForKey/objectForKey.....
//删除用户偏好设置,恢复出厂设置
- (void)removeObjectForKey:(NSString *)defaultName;

4.不同类型的defaults的优先级
这些优先级称之为domains,下面列举了可以使用的domains,优先级从高到低:
Arguments:通过命令行传递。大部分人都是通过双击程序图标来运行程序,而不是使用命令行,所以很少会用
Application:来自于用户defaults数据库
Global:用户对于整个系统的设定
Language:基于用户所选语言
Registered defaults:程序出厂defaults

5.设置程序的标识符
~/Library/Preferences中为程序创建的property list文件叫什么名字?默认名字为程序的标识符。如程序的标识符为:com.bignerdranch.RaiseMain,文件名则为:com.bignerdranch.RaiseMain.plist

6.命名Defaults中的键
为了保证使用一样的键名,可以使用C的预编译命令#define,不过Cocoa程序员一般都选择全局变量来实现:
在.h文件的#import语句后添加:
extern NSString * const BNRTableBgColorKey;
extern NSString * const BNREmptyDocKey;
在.m文件中定义这些变量,将定义设置放在#import之后,@implementation之前
NSString * const BNRTableBgColorKey = @"TableBackgroundColor";
NSString * const BNREmptyDocKey = @"EmptyDocumentFlag";//BNR是全局变量前缀,为了和其他公司或组织的全局变量区分开来

7.注册Defaults
在接受其他消息前,每个类首先都会接受initialize消息。重载AppController.m的initialize方法,来确保先注册你的defaults:

+ (void)initialize
{
    //创建一个字典
    NSMutableDictionary *defaultValues = [NSMutableDictionary dictionary];
    //封包颜色对象
    NSData *colorAsData = [NSKeyedArchiver archivedDataWithRootObject:[NSColor yellowColor]];
    //将defaults放置在字典中
    [defaultValues setObject:colorsAsData forKey:BNRTableBgColorKey];
    [defaultValues setObject:[NSNumber numberWithBool:YES]];

    //注册defaults字典
    [[NSUserDefaults standardUserDefaults] registerDefaults:defaultValues];
    NSLog(@"registered defaults:%@", defaultValues);
}

读取defaults:

- (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];
}

8.NSUserDefaultsController
有时希望能够绑定NSUserDefaults对象的值,NSUserDefaultsController类可以做这样的时期。程序中所有nib共用一个共享的NSUserDefaultsController

9.使用Command line来读写Defaults
用户defaults存放在~/Library/Preferences/中,你可以使用defaults命令行工具来编辑它们。例如查看XCode的defaults,在Terminal中:
cd Library/Preferences
defaults read com.apple.dt.Xcode

同样,你也可以修改defaults。输入下面的命令,修改NSOpenPanel打开的XCode默认的目录为/Users
defaults write com.apple.Xcode NSNavLastRootDirectoryForOpen /Users

再试试这个:
defaults read com.bignerdranch.RaiseMan//com.bignerdranch.RaiseMan是用户建的应用程序的标识符

查看全局的defaults:
defaults read NSGlobalDomain

第十四章:使用Notifications

1.每一个运行的程序都有一个NSNotificationCenter的成员变量,它的功能类似公告栏
对象注册关注某个确定的notifications(如果有人捡到一只小狗,就告诉我)。我们把这些注册对象叫做observer。其他的一些对象会给center发送notifications(我捡到了一只小狗)。center将该notifications转发给所有注册对该对象感兴趣的对象。把这些发送notifications的对象叫做poster

注意:notification center允许同一个程序中的不同对象进行通讯,但它不能跨越不同的程序。不同于IPC(进程间通信)

2.Notification 和 NotificationCenter
Notification对象非常简单,一个notification就像是poster要提供给observer的信息包裹一样。notification对象有两个重要的成员变量:name和object。
一般object都是指向poster的指针(为了让observer在接受到notification时可以回调poster),所以notification有两个方法:
- (NSString *)name;
- (id)object;

NotificationCenter是这个架构的大脑,允许你注册observer对象,发送notification,撤销observer对象。常用方法:
+ (NotificationCenter *)defaultCenter;//返回notification center
- (void)addObserver:(id)anObserver selector:(SELL)aSelector name:(NSString*)notificationName object:(id)anObject;//注意:notification center没有retain这个observer
//注册anObserver对象,接收名字为notificationName,发送者为anObject 的notification。
//当anObject发送名字为notificationName的notification时,将会调用anObserver的aSelector方法,参数为该notification
//a.如果notificationName为nil,那么notification center将anObject发送的所有notification都转发给anObject
//b.如果anObject为nil,那么notification center将所有名字为notificationName的notification转发给observer

- (void)postNotification:(NSNotification *)notification;//发送notification至notification center
- (void)postNotificationName:(NSString*)aName object:(id)anObject;//创建并发送一个notification
- (void)removeObserver:(id)observer;//从observer链中移除observer

3.UserInfo字典:如果希望notification对象传递更多地信息,可以使用user info字典。notification对象有一个变量叫userInfo,是一个NSDictionary对象,用来存放用户希望随着notification一起传递到observer的其他信息
- (void)postNotificationName:(NSString*)aName object:(id)anObject userInfo:(NSDictionary *)dic;

4.Delegates和Notifications

第十五章:使用Alert Panels
1.Alert Panel是通过一个C函数NSRunAlertPanel()来实现的:
int NSRunAlertPanel(NSString *title, NSString *msg, NSString *defaultButton, NSString *alternateButton, NSString *otherButton,...);

第十六章:本地化

1.nib文件的本地化
在XCode中,选中nob文件,打开它的Info Panel,点击Add Localization按钮

2.字符串表
可以为每一个语言版本创建多个字符串表。一个字符串表就是一个后缀名为.strings的文件。例:如果有个查找面板,可以创建不同语言版本的find.strings文件来本地化一个查找对话框
字符串就是键值对的集合。键和值都用双引号包括,一组键值对以分号结束,如:"Key1" = "Value1"; "Key2" = "Value2";
通过NSBundle查找得到一个键所对应的值:
NSBundle *main = [NSBundle mainBundle];
NSString *aString = [main localizedStringForKey:@"Key1" value:@"DefaultValue1" table:"Find"];
上面的代码会在Find.strings文件中查找“Key1”所对应的值。如果程序没有提供与用户设定的语言相对应的本地化资源,那么系统就会使用用户设定的所选语言对应的本地化资源。如果最后都没找到,那么将返回“DefaultValue1”。如果不提供字符串表的名字(这里是Find),系统将使用Localizebale.strings字符串表。大部分程序只为每个语言版本提供一个字符串表文件-Localizable.strings

3.创建字符串表
创建一个空文件并命名为Localizable.strings,编辑该文件,添加如下文本:
"DELETE" = "Delete";
"SURE_DELETE" = "Do you really want to delete %d people?";
"CANCEL" =  "Cancel";
创建该字符串表文件的中文版本。选中Localizable.strings文件,打开它的Info面板,创建中文本地化版本:
"DELETE" = "删除";
"SURE_DELETE" = "你真的要删除 %d 吗?";
"CANCEL" =  "取消";

4.使用字符串表。当只有一个字符串表时,可以使用如下代码:
NSString *deleteString;
deleteString = [[NSBundle mainBundle] localizedStringForKey:@"DELETE" value:@"Delete?" table:nil];

更方便的,在h文件中定义宏:
#define NSLocalizedString(key, comment) [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]

使用:
NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"DELETE", @"Delete") defaultButton:NSLocalizedString("DELETE", @"Delete") .........

5.ibtool——自动化工具,帮你将翻译字符串贴到nib文件中

在终端运行ibtool命令,它可以列举一个nib文件中的类或对象,也可以把其中的本地化字符串抽取出来保存到一个plist文件中。下面的例子是将English.lproj/MyDocument.nib文件中的本地化字符串抽取到文件Doc.strings文件中:
> cd RaiseMan/English.lproj
> ibtool --generate-stringsfile Doc.strings MyDocument.nib

Doc.strings文件如下:
/* Class="NSTableColumn";headerCell.title="Name";ObjectID="100026";*/
"100026.headerCell.title"="Name";
你可以创建Spanish版本的nib文件。先生成Spanish版本的Doc.strings,如下:
/*Class="NSTableColumn";headerCell.title="Name";ObjectID="100026";*/
"100026.headerCell.title"="Nombre";

使用字符串生成Spanish版本的nib:
> mkdir ../Spanish.lproj
> ibtool --strings-file Doc.strings
    --write ../Spanish.lproj/MyDocument.nib MyDocument.nib

可以输入如下man ibtool命令来获得ibtool的帮助:
> man ibtool

5.格式化字符串中符号的顺序

将文本从一种语言翻译成另外一种语言时,随着文字的改变,文字的顺序也会改变。例如,在一种语言中,文本可能是这样:“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 %@";

对于第二种语言,就必须调整它们的顺序。可以使用一个数字和一个美元符号:
"WANTS" = "A %2$@ is what %1$@ wants";

第十七章:自定义视图

1.程序中所有的可视对象要么是窗口(NSWindwo),要么是视图(NSView)。窗口是NSWindow的实例,注意窗口不是NSView的子类

2.View的层次
View是按一定层次关系组织的(如下图)。窗口包含了一个叫 content view 的view。该view填满了整个窗口内部区域。通常content view可以包含自己的子view。这些子view也可以有自己的子view。一个view知道自己的父view和子view,也知道自己所属的窗口。
NSView相关方法:
- (NSView *)superView;
- (NSView *)subviews;
- (NSWindow *)window;

下面五种类型的view通常包含子view:
a.窗口的content视图
b.NSBox:box中的内容就是它的子view
c.NSScrollView:scroll view中显示view就是它的子view。scroll bar也是它的子view
d.NSSplitView:split view中的view就是它的子view
e.NSTabView:当用户点选不同的tab时,交替切换不同的子view

3.让View绘制自己
drawRect:方法——当一个view要刷新自己时,view将会收到此消息。整个方法是自动调用的。如果要一个view重画,可以调用:
[view setNeedsDisplay:YES];//该方法将myView设置成”脏“的。在当前事件处理结束后,这个view将被重画
此方法将触发view整个可见区域的重画。如果要触发view某个指定区域进行重画,可以用setNeedsDisplayInRect:代替

在调用drawRect:之前,系统会对这个view进行locks focus。每一个view都有自己的graphic context—包含了view的坐标系统、当前颜色、当前字体以及剪裁区域等。当view被locks focus后,它的graphic context将被激活,而当unlock focus后,它的graphic context将不再是激活状态。任何时候绘制命令都是在当前激活的graphic context上进行的

可以使用NSBezierPath来绘制线条、圆形、曲线和矩形,可以使用NSImage来在view上绘制合成图像:

//将整个view绘制成一个绿色的矩形
- (void)drawRect:(NSRect)rect
{
    NSRect bounds = [self bounds];
    [[NSColor greenColor] set];
    [NSBezierPath fillRect:bounds];
}

因为性能原因,Object-c很少用到结构。可能用到一些Cocoa结构:NSSize、NSpoint、NSRect、NSRange(描述区间)、NSDecimal(描述数字精度)和NSAffineTransformStruct(描述图形线性变换)等

4.使用NSBezierPath绘制
绘制随机点间的线条

#import <Cocoa/Cocoa.h>
@interface StretchView : NSView
{
    NSBezierPath *path;
}
- (NSPoint)randomPoint;
@end
-----------------------------------------
#import "StretchView.h"
@implementation StretchView
//此方法会在view对象创建时自动调用
-(id)initWithFrame:(NSRect)rect
{
    if(![super initWithFrame:rect])
        return nil;
    //产生一个随机数生成器,设定随机数种子
    srandom(time(NULL));
    //创建一个path对象
    path = [[NSBezierPath alloc] init];
    [path setLineWidth:3.0];
    NSPoint p = [self randomPoint];
    [path moveToPoint:p];
    for(int i=0; i<15; i++){
        p = [self randomPoint];
        [path lineToPoint:p];
    }
    [path closePath];
    return self;
}
//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;
}

- (void)drawRect:(NSRect)rect
{
    NSRect bounds = [self bounds];
    //使用绿色填充视图
    [[NScolor greenColor] set];
    [NSBezierPath fillRect:bounds];
    //使用白色绘制path
    [[NSColor whiteColor] set];
    [path stroke];
}
@end

5.NSScrollView
scroll view由3个部分组成:document视图、content视图和scroll bar

6.单元格
NSControl从NSView继承而来。因为view有自己的graphics context,这让view成为一个庞大、高价的对象。
为了提高效率,将NSButton的大脑移到另外一个类(不再是view类),并创建一个大的view(叫NSMatrix),这个大脑就叫做NSButtonCell。

到最后,NSButton就是一个view再加上它的大脑NSButtonCell。button单元格做了所有的事情,而NSButton只是在窗口上申请了一块绘制区域

同样地,NSSlider就是包含了NSSliderCell的view,NSTextField就是一个包含了NSTextFieldCell的view。NSColorWell的差别是它没有单元格

7.isFlipped
PDF和PostScript用得是标准的笛卡尔坐标系统,Quartz使用了同样地模型,坐标原点为view的左下点
对于有些类型的绘制而言,如果原点在左上方,向下移动页面时y增加,数学计算会更容易。这时,我们称该view为flipped的
重载方法isFlipped,返回YES来翻转一个view:
-(BOOL)isFlipped{
    return YES;
}

原文地址:https://www.cnblogs.com/mumue/p/3248299.html