首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》10.5 访问器、属性与键值编码

关灯直达底部

从结构上来说,Objective-C实例变量类似于Swift实例属性:它是一个伴随着类的每个实例的变量,其生命周期与值都关联到这个特定的实例。不过,Objective-C实例变量通常是私有的,这意味着其他类的实例是看不到它的(Swift看不到)。如果实例变量是公共的,那么Objective-C类通常都会实现访问器方法:getter方法与setter方法(如果外界可以改写这个实例变量)。这种情况很常见,因此有如下命名约定:

getter方法

getter应该与实例变量有相同的名字(如果实例变量前有下划线,那么getter是没有这个下划线的)。这样,如果实例变量是myVar(或_myVar),那么getter方法应该命名为myVar。

setter方法

setter方法名应该以set开头,后跟大写的实例变量名(如果实例变量前有下划线,那么setter是没有这个下划线的)。setter应该接收一个参数,即准备赋给实例变量的新值。这样,如果实例变量是myVar(或_myVar),那么setter应该命名为setMyVar:。

这种模式(一个getter方法,可能还有一个命名适当的setter方法)非常常见,它还有一种简写形式:Objective-C类可以通过关键字@property与一个名字来声明属性。比如,下面这行代码来自于UIView类的声明:


@property(nonatomic) CGRect frame;  

(请忽略掉圆括号中的内容。)在Objective-C中,这种声明构成了一种承诺,它会提供一个getter访问器方法frame并返回一个CGRect,同时还会提供一个setter访问器方法setFrame:并接收一个CGRect参数。

如果Objective-C以这种形式声明@property,那么Swift就会将其看作Swift属性。这样,UIView的frame属性声明就会被直接转换为Swift中类型为CGRect的实例属性frame:


var frame: CGRect  

Objective-C的属性名只不过是个语法糖而已。在设置UIView的frame属性时,实际会调用其setFrame:setter方法;在获取UIView的frame属性时,实际会调用其frame getter方法。在Objective-C中,属性的使用是可选的;Objective-C可以并且经常会直接调用setFrame:与frame方法。不过在Swift中却不行。如果Objective-C类有正式的@property声明,那么其访问器方法会对Swift隐藏。

Objective-C属性声明可以在圆括号中包含单词readonly。这表示只有getter但却没有setter。比如(请忽略掉圆括号中的其他内容):


@property(nonatomic,readonly,strong) CALayer *layer;  

Swift会在声明后通过{get}来反映出这种限制,就好像这是个计算只读属性一样;编译器不允许为这样的属性赋值:


var layer: CALayer { get }  

Objective-C属性及相应的访问器方法都有自己的生命周期,独立于任何底层的实例变量。虽然访问器方法可用于访问不可见的实例变量,但不一定总是这样的。在设置UIView的frame属性并且setFrame:访问器方法得到调用时,你是不知道该方法到底做了哪些事情:它可能会设置一个名为frame或_frame的实例变量,但谁知道呢?从这个意义上来说,访问器与属性就是门面,隐藏了底层的实现。这与Swift中设置变量但又不知道或不关心它是个存储变量还是个计算变量类似;对于设置变量的代码来说,变量真正所设置的东西是不重要的(可能也是不知道的)。

10.5.1  Swift访问器

就像Objective-C属性实际上只是访问器方法的简写一样,Objective-C将Swift属性也看作访问器方法的简写形式,即便没有这样的方法亦如此。如果在Swift中声明的类有一个名为prop的属性,Objective-C就会调用prop方法获取其值,调用setProp:方法设置其值,即便没有实现这样的方法亦如此。这些调用会通过隐式的访问器方法路由到属性。

在Swift中,你不应该为属性编写显式的访问器方法!编译器不允许你这么做。如果需要显式实现访问器方法,那么请使用计算属性。比如,我向UIViewController子类添加了一个名为color的计算属性并提供getter与setter:


class ViewController: UIViewController {    var color : UIColor {        get {            print(/"someone called the getter/")            return UIColor.redColor        }        set {            print(/"someone called the setter/")        }    }}  

Objective-C代码现在可以显式调用隐式的setColor:与color访问器方法,当调用时,你会看到计算属性的setter与getter方法实际上会被调用:


ViewController* vc = [ViewController new];[vc setColor:[UIColor redColor]]; // /"someone called the setter/"UIColor* c = [vc color]; // /"someone called the getter/"  

这证明了在Objective-C中,你已经提供了setColor:与color访问器方法。你甚至可以修改这些访问器方法的Objective-C名字!要想做到这一点,请添加一个@objc(...)特性,并在圆括号中放入其Objective-C名字。可以将其添加到计算属性的setter与getter方法中,或添加到属性自身当中:


@objc(hue) var color : UIColor?

Objective-C代码现在可以直接调用hue与setHue:访问器方法了。

如果只是想向setter添加功能,那么可以使用setter观察者。比如,要想向UIView子类中的Objective-C setFrame:方法添加功能,那么可以覆写frame属性并添加一个didSet观察者:


class MyView: UIView {    override var frame : CGRect {        didSet {            print(/"the frame setter was called: (super.frame)/")        }    }}  

10.5.2 键值编码

Cocoa可以通过运行期指定的字符串名动态调用访问器(这样就可以访问Swift属性了),这种机制叫作键值编码(KVC),类似于通过respondsToSelector:使用选择器名进行内省的能力。字符串名是键;传递给访问器或从访问器返回的是值。键值编码的基础是NSKeyValueCoding协议,这是个非正式协议;它实际上是个注入NSObject中的类别。因此,要想使用键值编码,Swfit类必须要继承自NSObject。

基本的键值编码方法是valueForKey:与setValue:forKey:。当在对象上调用其中一个方法时,该对象就会被内省。简而言之,首先会寻找恰当的选择器;如果不存在,那就会直接访问实例变量。另外一对有用的方法是dictionaryWithValuesForKeys:与setValuesForKeysWithDictionary:,你可以通过它们仅使用一个命令就能以NSDictionary的方式获取与设置多个键值对。

键值编码中的值必须是个Objective-C对象,其Swift类型是AnyObject。在调用valueForKey:时,你会接收到一个包装了AnyObject的Optional,需要将其向下类型转换为真实的类型。

对于给定的键来说,如果一个类提供了访问器方法或拥有实例变量来对其进行访问,那么我们会说这个类针对于该键是键值编码兼容的(或KVC兼容的)。对于给定的键来说,如果一个类不是对其键值编码兼容的,那么访问该键会导致运行期异常。当出现这种崩溃时,如果熟悉抛出的消息将是大有裨益的,下面就来故意制造崩溃的结果:


let obj = NSObjectobj.setValue(/"hello/", forKey:/"keyName/") // crash  

控制台会打印出消息“This class is not key value coding-compliant for the key keyName.”。虽然缺少引号,但这条错误消息的最后一个单词是导致崩溃的键字符串。

如何才能让上述方法调用不崩溃呢?接收方法调用的对象所属的类需要有一个setKeyName:setter方法(或keyName及_keyName实例变量)。如10.5.1节所述,在Swift中,实例属性表示存在访问器方法。这样,我们可以在拥有所声明的属性的任何NSObject子类实例上使用键值编码,前提是键字符串是该属性的字符串名。下面就来试一下!类声明如以下代码所示:


class Dog : NSObject {    var name : String = /"/"}  

下面是测试代码:


let d = Dogd.setValue(/"Fido/", forKey:/"name/") // no crash!print(d.name) // /"Fido/" - it worked!  

10.5.3 键值编码的使用

实际上,你可以通过键值编码在运行期根据字符串来决定调用哪个访问器。最简单的一种情况就是,你可以通过字符串访问动态指定的属性。这在Objective-C代码中是非常有价值的;不过,这种自由的内省能力与Swift的精神恰恰相反,在将我自己编写的Objective-C代码转换为Swift时,我发现可以通过其他方式达到相同的目的。

下面是个示例。在flashcard应用中有个名为Term的类,代表一个拉丁语单词。它声明了很多属性。每个卡片会显示一个单词,其各种属性会显示在不同的文本域中。如果用户轻拍了3个文本域之一,那么我希望界面上显示的单词换成下一个,并且这个单词要不同于上一个。这样,代码对于三个文本域来说都是一样的;唯一的差别是在寻找下一个显示的单词时,我们应该考虑哪个属性。在Objective-C中,到目前为止最简单的方式就是使用键值编码:


NSInteger tag = g.view.tag; // the tag tells us which text field was tappedNSString* key = nil;switch (tag) {    case 1: key = @/"lesson/"; break;    case 2: key = @/"lessonSection/"; break;    case 3: key = @/"lessonSectionPartFirstWord/"; break;}// get current value of corresponding instance variableNSString* curValue = [[self currentCardController].term valueForKey: key];  

不过在Swift中,可以通过匿名函数数组来完成相同的功能:


let tag = g.view!.tag - 1let arr : [(Term) -> String] = [    {$0.lesson}, {$0.lessonSection}, {$0.lessonSectionPartFirstWord}]let f = arr[tag]let curValue = f(self.currentCardController.term)  

不过,键值编码在iOS编程中也是颇具价值的,特别是因为很多内建的Cocoa类都允许以特殊的方式使用键值编码。比如:

·如果向NSArray发送valueForKey:,那么它会向数组中的每个元素发送valueForKey:,并返回一个包含了结果的新数组,这是一种优雅的简写方式,NSSet亦如此。

·NSDictionary实现了valueForKey:以作为objectForKey:的替代方案(这对于字典构成的NSArray来说特别有用)。与之类似,NSMutableDictionary会将setValue:forKey:作为setObject:forKey:的同义;只不过在调用removeObject:forKey:时value:可以为nil。

·NSSortDescriptor通过向NSArray中的每个元素发送valueForKey:来对NSArray排序。这样,根据特定的字典键值对字典数组排序,以及根据特定的属性值对对象数组排序就变得很容易了。

·NSManagedObject(与Core Data配合使用)用于确保对在实体模型中所配置的特性做到键值编码兼容。这样,我们就可以通过valueForKey:与setValue:forKey:访问这些特性了。

·可以通过CALayer与CAAnimation使用键值编码来定义并获取任意键的值,就好像它们是字典一样;它们实际上对于每个键都是键值编码兼容的。这对于向这些类的实例附加识别与配置信息是非常有用的。实际上,这是我在Swift中使用键值编码最常用的方式。

10.5.4  KVC与插座变量

键值编码是插座变量连接能够正常运作的关键所在(参见第7章)。Nib中的插座变量名是个字符串,键值编码会在nib加载时将该字符串转换为所要寻找的属性。

假设有一个Dog类,它有一个@IBOutlet属性master,你绘制了一个从nib中的这个类到Person nib对象上的/"master/"插座变量。当nib加载时,插座变量名/"master/"会通过键值编码转换为访问器方法名setMaster:,Dog实例的setMaster:隐式访问器方法在调用时会将Person实例作为其参数,这样会将Dog实例的master属性值设为Person实例(如图7-9所示)。

如果nib中的插座变量名与类中的属性名之间不匹配,那么在运行期,当nib加载时,Cocoa会使用键值编码根据插座变量名来设置对象中的值,但却会失败,并抛出异常,错误消息表示该类针对于这个键(插座变量名)并不是键值编码兼容的;也就是说,应用会在nib加载时崩溃。另一种类似的情况是插座变量是正确的,但后面却修改或删除了类中的属性名(参见7.3.4节)。

10.5.5 键路径

可以通过键路径在一个表达式中将键串联起来。如果一个对象针对某个键是键值编码兼容的,并且该键所对应的值又针对另一个键是键值编码兼容的,那就可以通过调用valueForKeyPath:与setValue:forKeyPath:将这两个键串联起来。键路径字符串看起来像是使用点符号链接起来的一连串键名。比如,valueForKeyPath(/"key1.key2/")会调用消息接收者的valueForKey:,并且将/"key1/"作为键,然后获得调用所返回的对象,并对该对象调用valueForKey:,并且将/"key2/"作为键。

为了演示这种简写方式,假设对象myObject有一个实例属性theData,它是个字典数组,这样每个字典都有一个name键和一个description键:


var theData = [    [        /"description/" : /"The one with glasses./",        /"name/" : /"Manny/"    ],    [        /"description/" : /"Looks a little like Governor Dewey./",        /"name/" : /"Moe/"    ],    [        /"description/" : /"The one without a mustache./",        /"name/" : /"Jack/"    ]]  

我们可以通过键值编码与键路径钻取到该字典数组中:


let arr = myObject.valueForKeyPath(/"theData.name/") as! [String]  

结果是个包含了字符串/"Manny/"/"Moe/"与/"Jack/"的数组。如果不清楚原因,那么请回顾一下之前介绍的NSArray与NSDictionary是如何实现valueForKey:的吧。

再回顾一下第7章介绍的自定义运行时特性。该特性就使用了键值编码!当在对象的身份查看器中定义了运行时特性时,你在第1列中所输入的字符串就是个键路径。

10.5.6 数组访问器

键值编码是一项强大的技术,同时拥有很多衍生品(参见Apple的Key-Value Coding Programming Guide了解详情)。这里只介绍其中一种。如果键的值看起来像是个数组或是集合(即便实际情况并非如此),那么借助于键值编码,对象可以将键合成起来。你需要实现特别命名的访问器方法;在使用相应的键时,键值编码会看到它们。

为了说明这一点,向对象myObject所属的类添加如下方法:


func countOfPepBoys -> Int {    return self.theData.count}func objectInPepBoysAtIndex(ix:Int) -> AnyObject {    return self.theData[ix]}  

通过实现countOf...与objectIn...AtIndex:,我告诉键值编码系统认为给定的键/"pepBoys/"存在并且是个数组。通过键值编码方式获取键/"pepBoys/"的值的操作可以成功,并且返回的对象可以看作NSArray,但实际上它是个代理对象(NSKeyValueArray)。现在可以这样做:


let arr : AnyObject = myObject.valueForKey(/"pepBoys/")!let arr2 : AnyObject = myObject.valueForKeyPath(/"pepBoys.name/")!  

在上述代码中,arr是个数组代理;arr2是由3个男孩的名字构成的相同的数组。这个示例看起来毫无意义:底层实现已经是个数组了,使用/"pepBoys/"与之前所用的/"theData/"有何区别呢?表面上看没什么不同,但实际情况却并非如此。假设并没有数组存在,countOfPepBoys与objectInPepBoysAtIndex:的结果是通过完全不同的操作得到的。实际上,我们创建了一个类似于NSArray的键;并且将一些实现细节隐藏在其后。