Chapter 10 Cocoa Classes
iOS编程时, 实际是进行Cocoa编程. 所以必须熟悉Cocoa, 必须知道Cocoa是什么, 它能够做什么, 你和Cocoa如何进行”交流”.
Cocoa是一个庞大的Framework, 被分割成若干较小的Framework. 任何iOS编程人员都需要花费一定时间来熟练Cocoa.
Cocoa中含有一些主要的规则和组件, 最好是以它们为主线来学习Cocoa.
Cocoa大部分类都是OC写的, 虽然OC类和Swift类能相互转换.但Swift中的Enum和Struct和OC中的不兼容. 不过, 一些重要的Swift对象都能桥接到Cocoa类.
本章主要介绍Cocoa如何组织, 即它的组成, 然后说明一些常用类用法, 最后介绍NSObject类.
1 Subclassing
Swift中自定义类的方式有多种:
- 继承
- 类扩展
- 协议
Cocoa中提供的类若无法满足需求时, 可以进行自定义.
但首先要对Cocoa中的类进行全面了解, 因为有的功能并不是没有,而只是没找到. 所以请熟悉类相关的文档.
某些类总是会被继承, 比如UIViewController类.
另一个例子是UIView, 许多类都继承自UIView, 比如UIButton, UITextField等.
如果要添加额外绘制行为, 可以重写drawRect:
方法. 过程是先继承UIVIew, 然后在子类中实现drawRect:
方法即可.
比如要在window中绘制一条水平线, 因为Cocoa中没有绘制水平线的类, 故可自定义UIView的子类, 让它绘制一条水平线即可.
方法如下所示:
- 新建工程并新建一个类, 命名为MyHorizontal, 这个类继承自UIView.
- 修改MyHorizontal.swift如下所示:
class MyHorizontal: UIView { required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.backgroundColor = UIColor.clearColor() } override func drawRect(rect: CGRect) { let c = UIGraphicsGetCurrentContext() CGContextMoveToPoint(c, 0, 0) CGContextAddLineToPoint(c, self.bounds.size.width, 0) CGContextStrokePath(c) } }
3.在storyboard中的VC控制的scene中的View内再添加一个view,并将这个添加的view类型修改为MyHorizontal
4.运行程序,可以看到需要的线已经画出来了, 如下图所示:
上述代码中,继承自UIView的类绘制了一条水平线, 由于UIView本身没有其他绘制行为,故在
drawRectangle:
方法中没有调用super.drawRect:
.
UIView的子类UILabel中有两个方法:
drawTextInRect:
和textRectForBounds:limitedToNumberOfLines:
如果想自定义UILabel, 可以在它子类中覆盖这些方法. 当绘制UILabel时, 这两个方法会被自动调用.
可以使用drawTextInRect:
方法配置显示:
- 新建工程, 新建一个UILabel的子类, 命名为 MyBoundedLabel
- 在MyBoundedLabel.swift中加入如下代码:
class MyBoundedLabel: UILabel { override func drawRect(rect: CGRect) { let context = UIGraphicsGetCurrentContext()! CGContextStrokeRect(context, CGRectInset(self.bounds, 1.0, 1.0)) super.drawTextInRect(CGRectInset(rect, 5.0, 5.0)) } }
3.在View中添加一个Label控件,并将它类型修改为MyBoundedLabel,运行程序如下所示:
实际工作中用继承方式来自定义子类的情况并不多见(在Cocoa框架内), 虽然有时使用继承的确比较方便.
原则:除非Cocoa需要你使用继承来自定义行为,否则不应该使用.
绝大多数Cocoa Touch类都不需要进行继承(有些在文档中明确表示不能继承), 因为有Delegation的存在.
推荐使用委托来添加自定义行为.
比如UIApplication对象, 它的许多行为都转交给AppDelegate对象进行代理. 而AppDelegate类并不是继承自UIApplication类,而是接受了UIApplicationDelegate**协议**.
2 Categories and Extensions
Category在OC中表示类扩展(具名类扩展),而Cocoa中大量使用Category对类进行组织. 相当于是Swift中的Extension.
利用Extension, 可将类方法或对象方法注入到类中.
Swift中广泛使用extension,原因有两点:
- 对类进行合理组织
- 自定义Cocoa类.
2.1 How Swift Uses Extensions
比如在Swift.h头文件中,使用了许多Extension添加属性或行为.
头文件中Array的Extension:
extension Array : CustomStringConvertible, CustomDebugStringConvertible {
/// A textual representation of `self`.
public var description: String { get }
/// A textual representation of `self`, suitable for debugging.
public var debugDescription: String { get }
}
extension Array {
/// Call `body(p)`, where `p` is a pointer to the `Array`'s
/// contiguous storage. If no such storage exists, it is first created.
///
/// Often, the optimizer can eliminate bounds checks within an
/// array algorithm, but when that fails, invoking the
/// same algorithm on `body`'s argument lets you trade safety for
/// speed.
public func withUnsafeBufferPointer<R>(@noescape body: (UnsafeBufferPointer<Element>) throws -> R) rethrows -> R
/// Call `body(p)`, where `p` is a pointer to the `Array`'s
/// mutable contiguous storage. If no such storage exists, it is first created.
///
/// Often, the optimizer can eliminate bounds- and uniqueness-checks
/// within an array algorithm, but when that fails, invoking the
/// same algorithm on `body`'s argument lets you trade safety for
/// speed.
///
/// - Warning: Do not rely on anything about `self` (the `Array`
/// that is the target of this method) during the execution of
/// `body`: it may not appear to have its correct value. Instead,
/// use only the `UnsafeMutableBufferPointer` argument to `body`.
public mutating func withUnsafeMutableBufferPointer<R>(@noescape body: (inout UnsafeMutableBufferPointer<Element>) throws -> R) rethrows -> R
}
代码中的Extension为协议扩展.
协议扩展在功能上讲是完全没有必要的, 因为可以在Array类中将这些全部写进去. 但为了便于理解, 故按照逻辑功能将类的方法或属性划分成一个个独立的扩展.
2.2 How You Use Extensions
Swift中允许定义全局函数, 这样做没有什么错. 但却不满足OO编程中对封装的要求, 故更好的方式是将函数写到类中去.
假如仅仅为了添加几个方法而去继承一个庞大的类,这样的做法往往得不偿失, 并且很可能不能帮助你完成想完成的任务.(比如想添加一个函数处理多种不同类型, 如果使用继承, 可能这个方法就只能处理该类对象,而其他类对象的处理又必须重新定义方法), Extension还可用在Enum, Struct上, 而继承只能在Class上使用.
而且还有个好处, 如果将某方法用Extension插入到类中, 该类的全部子类都自动获得该方法.
比如要让一个UIButton和UIBarButton都具有某个行为,可以声明具有该行为的Protocol, 然后在Protocol的extension中实现该方法, 那么,这两个类只需要接受Protocol,便自动拥有了该方法,而无需自己实现. 这样的Extension就是协议扩展.
protocol ButtonLike {//协议声明
func behaveLikeAButton()
}
extension ButtonLike {//协议extension,即实现
func behaveLikeAButton {
//...
}
//在使用时,只需声明即可, 用协议扩展类.
extension UIButton : ButtonLike {}
extension UIBarButton : ButtonLike{}
}
这个办法可用于为若干类统一添加行为.
总结一下添加自定义行为的两个办法:
定义某个类的Extension:
extension UIViewController { //直接扩展类添加方法 func saySomething { print("hello") } } //... //使用时,比如在ViewController的viewDidLoad方法中: self.saySomething() //输出hello
协议扩展
protocol someBehavior { func saySomething() } extension someBehavior { //这里理解为扩展该协议,实际和扩展类的道理一样,反正extension中必须有实现 //找到了,如果想分别定义不同行为,又想接受统一个protocol,则这里留空,然后在每个声明中去实现. func saySomething() { print("good morning") } } //... //使用时,先这样声明,这个语法必须记住: extension UIViewController : someBehavior {} //声明这个类接受协议扩展 extension UIView : someBehavior {} //... //然后就可以使用了,比如在UIViewController的子类ViewController中: self.saySomething() //输出good morning
2.3 How Cocoa Uses Categories
Cocoa使用Category对类进行组织. 将类按功能分割为若干组成部分, 每个部分便对应一个Category, 并且每个Category对应一个头文件.
这样的方式会对文档查询造成影响. 比如文档中对NSString类声明只说了三个文件: NSString.h, NSPathUtilities.h和NSURL.h, 但NSStringDrawing.h却没有提到(而是在NSString UIKit Addtions中), 不得不说这是Cocoa文档瑕疵所在.
3 Protocols
OC的Protocol和Swift的Protocol可以相互转化, 而且在Swift中被标记@objc的protocol可以被OC使用.
例如Cocoa中的对象复制时, 有的对象可被复制, 而有的却不能. 因为Cocoa中定义了一个复制对象协议: NSCopying.
NSCopying在NSObject.h中实现, 但NSObject没有接受这个协议.
接受这个协议的类可以使用它:
let s = "hello".copyWithZone(nil)
比如遵守某个协议的属性:
weak var dataSource : UITableViewDataSource?
意思是:不管dataSource是什么类型的,只要它是**接受**UITableViewDataSource协议的类型对象,就可以赋值给dataSource属性.
这里接受的意思是类声明中包含该协议, 并且类中实现了required方法.
协议的另外一个场景: 代理协议.
比如在UIApplication中有一个属性:
unowned(unsafe) var delegate : UIApplicationDelegate?
这个属性用于指定它的代理对象.
Cocoa中协议都有其单独文档, 列出协议内的方法.
比如上面代码中, UIApplicationDelegate就是一个协议.
当一个类接受某协议之后,它的行为变得更多了,这时不仅要关注类的行为,还要关注协议中声明的行为.并且,还需要关注它父类行为和父类接受的协议所规定的父类的额外行为…
3.1 Informal Protocols
Informal Protocol并非真的指协议,它是给编译器提供方法声明的一种途径,好让编译器不再抱怨.
实现Informal Protocol的两种途径:
- 在NSObject上定义一个Category, 这样Cocoa中所有对象都能够接收满足条件的消息
- 定义一个协议, 不是让某个类去接受, 而是让id(AnyObject)类型对象接受, 这样可以清除所有可能的编译错误(指发送协议方法时候).
这种技术之前用得很多,不过自从protocol中有了optional方法之后就替代了它. 但是在Cocoa中还有少部分使用Informal protocol的情况.
3.2 Optional Methods
所有的OC协议,以及在Swift中用@objc声明的协议,都可以拥有Optional方法.
当对象接收一个它无法处理的消息时(它里面没有这个方法), 那就会出错,并抛出异常.
但如果类中有这个方法的声明时, 表明这个类有处理这个消息的能力,只是还没有实现. 这样就可以避免出现异常(这就是Informal protocol的用途, 也即Optional的作用)
OC中调用respondsT:Selector:
来判断某对象能否响应某方法, Swift也使用类似方法.
比如下面的协议:
@objc protocol Flier {
optional var song : String {get} //只读属性
optional func sing()
}
假如某类型接受该协议, 向这种类型对象发送sing?()
消息, 系统会自动调用respondsToSelector:
判断对象能否处理该消息, 然后再决定是否发送该消息给对象(这样便不会出现异常).
当使用Optional方法的时候会调用respondsToSelector:
, 有一部分额外开销.
4 Some Foundation Classes
在开始正式编程之前,需要了解一些常用的类, 详见 Foundation Framework Reference.
4.1 Useful Structs and Constants
NSRange:
它是一个C结构体, 具有两个属性: location和length.
比如location是1, length是2, 表示第零个元素和第一个元素.
使用NSMakeRange创造NSRange.
Swift里面有Range Struct, 可以将NSRange转换为Swift中的Range,方法是:
let r = NSRange().toRange() //可选类型的Range.
NSNotFound是一个整形常量,用于指示未找到的情况.
许多的查找方法都会返回NSNotFound,比如下面的:
let arr = ["hey"] as NSArray let ix = arr.indexOfObject("he") if ix == NSNotFound { print("it's not here.") }
如果找到的话, indexOf方法就会返回一个可选值包装的Int, 否则就是nil.
如果返回的是NSRange的话,那么location的值就会是NSNotFound.
Swift可以自动处理转换问题,使得用户不必担心与NSNotFound比较会出问题.
比如NSString的
rangeOfString:
返回值是NSRange, 如果NSRange中的location值是NSNotFound的话, 那么对应Swift的Range值就是nil:let s = NSString(string: "hello world") let y = s.rangeOfString("good").toRange() //y的值为nil.
这样的好处是:假如你需要一个Swift中的Range, 可以用这个NSRange转换出一个Range; 假如你需要一个NSRange, 那直接使用即可, 它可以直接同Cocoa交互.
let s = "hello" as NSSring let r = s.indexOfString("ha") //这样r即为一个NSRange if r.location == NSNotFound { print("it wasn't found") }
4.2 NSString and Friends
NSString是Cocoa中的字符串, NSString被桥接到Swift的String, 它们二者可以自动转换.
可将Swfit的String作为参数传递给需要NSString的函数,也可以在String上面使用NSString的方法等等.
比如
let s = "hello"
let s2 = s.capitalizedString
//capitalizedString 返回NSString,但s2为HELLO,是Swift的String
转换过程是: 在s上调用capitalizedString:
方法时, 先将它自动转换为NSString, 然后调用方法, 方法返回一个NSString, 再被自动转换成String赋值给s2.
这样的好处是可以直接在String上直接使用NSString方法,但如果你没有import Foundation
, 则会出错(但模拟不了…).
但有时转换无法自动完成,比如:
let s = "my"
let s2 = s.stringByAppendingPathExtension("text") //报错
let s3 = s.substringToIndex(1) //报错
解决办法都是先将s转换为NSString再调用即可:
let s2 = (s as NSString).stringsubstringToIndex(1)
具体的原因是由于NSString和String在元素的表示上面存在差异造成的, 详见苹果String Programming Guide.
还有一个注意点就是NSString是不可变的,要使用它的可变版本,则用NSMutableString.
比如:
let s3 = "abcdefg" //s3是Swift的String
let s4 = NSMutableString(string: s3) //s4是NSMutableString
s4.deleteCharactersInRange(NSMakeRange(1, 3))
let s5 = (s4 as String) + "hh" //s5 is a Swift String
在以后的编程中,经常会需要在String和NSString的”连接桥”上过来过去,但多数情况是在NSString这边, 因为许多优秀方法都在Cocoa里面, 比如字符串搜索:
- NSString有许多类似
rangOfString:
方法, 比如忽略大小写,从尾部搜索等. - 当自己都无法确定当前要搜索什么的时候, 可以使用一定的结构来描述它,如使用
NSScaner
. - 指定选项
.RegularExpressionSearch
支持以正则表达式搜索, 另外正则表达式也可以是单独的类NSRagularExpression
,在里面使用NSTextCheckingResult
来描述匹配结果. - 有复杂文本自动分析支持,比如使用
NSDataDetector
, 它是NSRegularExpression
的子类,可用于高效搜索如URL或电话号码. 还有NSLinguisticTagger
,用于文本语法分析.
下面的例子里,尝试将文本中所有的”hell”替换成”heaven”,并且不想将hello中的hell替换:
let s = NSMutableString(string: "hello world, go to hell")
let r = try! NSRegularExpression(pattern: "\\bhell\\b", options: .CaseInsensitive)
r.replaceMatchesInString(s, options: [], range: NSMakeRange(0, s.length), withTemplate: "heaven")
// s 现在变成了 "hello world, go to heaven"
NSString也可以很方便地表示文件路径, 它经常和NSURL结合使用.
NSString和其他一些类,都提供了读写文件的方法, 文件可以用NSString表示的文件路径指定,也可以用NSURL指定.
NSString里面不含文字样式信息,如果想使用文字样式, 则利用:NSAttributedString, NSParagraphStyle, NSMutableParagraphStyle. 这些类允许自定义文本或段落风格. 同时内置的UI对象可以显示带风格的文本.
NSString以及NSAttributedString上的NSStringDrawing扩展支持字符串绘制.详见String UIKit Additions Reference和NSAttributedString UIKit Addtions Reference.
4.3 NSDate and Friends
通俗来说,NSDate就是表示日期和时间, 内部以秒(NSTimeInterval)表示距某日期的间隔.
调用NSDate的构造函数构造出来的NSDate对象表示当前日期和时间:NSDate()
许多日期操作都涉及NSDateComponents类的使用,如果想要在NSDate和NSDateComponents之间转换,需要使用NSCalendar作为中间媒介.
一般使用日历来构造NSDate:
let greg = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let comp = NSDateComponents()
comp.year = 2016
comp.month = 8
comp.day = 10
comp.hour = 15
let d = greg.dateFromComponents(comp)
//上面的代码中先构造一个日历grep,然后构造一个日期组件comp,设置日期组件,再使用grep的datgeFromComponents方法来获得NSDate
同时,如果想要对日期进行计算的话, 则可NSDateComponents:
let d = NSDate()
let comp = NSDateComponents()
comp.month = 1
let greg = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let d2 = greg.dateByAddingComponents(comp, toDate: d, options: [])
代码中构造先当前日期.然后构造一个日期组件,只设置它的月份.然后使用NSCalendar作为中介,在当前日期上面加一个日期组件的值,得到日期d2.
可以将日期表示为字符串, 默认是0时区的. 如果想获得当前时区中时间,可以使用如下方式:
print(d)
print(d.descriptionWithLocale(NSLocal.currentLocale()))
两条语句的输出结果为:
2016-07-21 08:21:55 +0000
Thursday, July 21, 2016 at 4:21:55 PM China Standard Time
为解析日期字符串,使用NSDateFormatter, 它使用类似NSLog的格式控制字符:
let df = NSDateFormatter()
let format = NSDateFormatter.dateFormatFromTemplate("dMMMMyyyyhmmaz", options: 0, locale: NSLocale.currentLocale())
df.dateFormat = format
let s = df.stringFromDate(NSDate())
代码中创建一个NSDateFormatter对象df,它用于解析日期字符串.然后构造一个自定义的format,这个format的作用就是设置df格式.然后调用df.stringFromDate,即可获得指定格式的字符串.
如果想逆向解析这个字符串,只需使用相同的format设置,然后使用NSDateFormatter的DateFromString方法解析即可.
4.4 NSNumber
NSNumber是一个包含数值的对象. 里面的数值可以是任何OC原子数值类型. 由于OC原子类型并非对象, 所以这样的数值不能用在需要对象的场合中.
但利用它可以将Swift中数值对象转换为原子类型数值.
Swift中为了避免用户同NSNumber直接接触, 将数值对象同OC的数值进行了桥接:
如果需要的是原子数值(即不是对象), 则将Swift中的数值(对象)转换为原子数值
如果需要的是数值对象, 则将Swift的数值对象自动转换为NSNumber.可以进行自动转换的类型有:Int, UIInt,Float, Double, Bool, 下面就是自动进行桥接转换的例子:
let ud = NSUserDefaults.standardUserDefaults() let i = 0 ud.setInteger(i, forKey: "Score") ud.setObject(i, forKey: "Score")
只看第3行和第4行: 这两行的i就是不同方式的转换, i是Swift数值对象, 它在第3行被转换为一个原子数值,而在第四行被转换为NSNumber对象.
如果想在Swift中进行显式转换, 可以使用下面的方式:
let n = 0 as NSNumber //显式类型转换 let u = NSNumber(float:0) //直接使用NSNumber构造方法
值从OC返回Swift时, 大多数情况返回的都是AnyObject, 此时需要进行类型转换.
NSNumber仅仅作为一个数值的容器而已,如果需要里面的数值,还需要手动将数值”解压”出来.
NSNumber的子类NSDecimalNumber支持计算, 但仅限两个相同NSDecimalNumber之间,比如:
let dec1 = NSDecimalNumber(float: 4.0)
let dec2 = NSDecimalNumber(float: 5.0)
let sum = dec1.decimalNumberByAdding(dec2)
NSDecimalNumber经常被用在取整上面, 因为它提供了许多方便的方法.
NSDecimalNumber里面实际是包含一个NSDecimal结构体(对应decimalValue属性),这是一个C结构体. NSDecimal结构体的函数比NSDecimalNumber中的方法速度更快.
4.5 NSValue
NSValue是NSNumber的父类, 它用于包装非数值类型的C值, 比如结构体.
在Swift中无法使用C的结构体, 利用NSValue就可以解决这一问题.
经常用NSValue来包装以及解包CGPoint, CGRect, CGSize等结构体, 以及NSRange, CATransform3D, CMTime等.
通常不需要手动将C结构体保存在NSValue中,但是如果你想的话,是可以进行的. 因为Swift不会自动将C结构体装进NSValue中, 需要你手动完成操作.
另外我们可以将CGPoint装进Swift数组中, 因为CGPoint是Swift结构体(也是对象), 而Swift数组可以存放任何对象.但是在OC中却不行,因为OC数组只能存放对象, 所以在OC中先将CGPoint装进NSValue, 再放入数组中的.
4.6 NSData
NSData实际是一串字节,或说它是一个缓冲池或一块内存区域.
NSData是不可变的, 不过它有可变子类 NSMutableData.
在实际工作中, 有两大情况需要使用NSData:
当从网络下载数据时.
例如NSURLConnection或NSURLSession可以将任何从网络获取的数据保存在NSData中.
假设获取的是一些字符串, 则只要指定正确的解码方式,就可以将字符串从NSData中解析出来.
当将对象保存到文件或用户配置(NSUserDefaults)中时.
例如无法直接将一个UIColor保存到用户配置中去, 当用户选择一个颜色设置并保存的时, 先将UIColor转换成NSData(使用NSKeyedArchiver), 然后再保存:
let ud = NSUserDefaults.standardUserDefaults() let c = UIColor.blueColor() let cdata = NSKeyedArchiver.archivedDataWithRootObject(c) ud.setObject(cdata, forKey: "myColor")
代码中首先获得用户配置ud,然后构造一个cdata(从颜色对象c)它是NSData类型的, 构造cdata使用的是NSKeyedArchiver的
archivedDataWithRootObject:
方法, 最后将这个cdata保存到ud中.
4.7 判等和比较
在Swift中, 操作符可以在类中覆盖定义(类似C++的运算符重载), 并且使用infix,postfix,prefix分别表示二元,一元前缀,一元后缀运算符:
infix operator + {
//....
}
但OC中运算符不支持重载, 要比较两个对象, 比如说判等需要覆盖实现isEqual
对象方法, 该方法从NSObject中继承.
而Swift中将NSObject类或它的子类看作可比的, 比较时自动将”==”操作隐式转换成isEqual
方法的调用.
let n1 = NSNumber(integer: 1)
let n2 = NSNumber(integer: 2)
let n3 = NSNumber(integer: 3)
let ok = n2 == 2
let ok2 = n2 == NSNumber(integer: 2)
let ix = [n1, n2, n3].indexOf(2)
上面代码无错的原因有两点:
- Swift自动将数值包装成NSNumber对象
- Swift自动将”==”操作符转换成调用isEqual方法
NSNumber里面已经实现了isEqual, 所以可以直接使用”==”操作符.
但如果某个NSObject子类没有实现isEqual, 则会执行NSObject中的isEqual, 即比较两个对象是否是同一个, 类似于Swift中的”===”操作符.
class Dog : NSObject {
var name : String
init(name:String) {
self.name = name
}
}
let d1 = Dog(name: "Fido")
let d2 = Dog(name: "Fido")
let ok = d1 == d2
代码中的Dog类,继承自NSObject,但没有实现isEqual, 则判等为假.
OC对象更多地是使用比较函数来进行比较, 如NSNumber的isEqualToNumber:
等等.但这些类上面同样实现了isEqual, 在Swift中肯定是使用”==”比使用isEqualToNumber
方法来得更加简便.
OC中对象的大小比较则要看具体类中是否有相应实现了.标准的比较方法是compare:
, 返回NSComparisonResult对象, 它有三种结果:
.OrderedAscending
升序即接收对象比参数对象小
.OrderedSame
相等即接收对象和参数对象相等
.OrderedDecending
降序即接收对象比参数对象大
Swift不会自动调用compare方法, 即如果代码中出现两个NSObject或其子类对象比较大小的情况, 直接使用”>”这类比较操作符是不行的. 比如下面的代码会出错:
let n1 = NSNumber(integer: 1)
let n2 = NSNumber(integer: 2)
let ok = n1 > n2 //错
此时需要做的就是显式调用compare方法:
let n1 = NSNumber(integer: 1)
let n2 = NSNumber(integer: 2)
let ok = n1.compare(n2) == .OrderedAscending //true
代码中n1为1,n2对象为2, n1的compare方法返回的是n1小于n2对应的NSComparisonResult,即.OrderedAscending
,故ok值为true.
4.8 NSIndexSet
NSIndexSet是数值集合, 主要用于表示关系集合中元素的下标.
比如想同时访问数组中多个对象,可以将这些对象下标先全部存放到一个NSIndexSet中.
可以传递一个NSIndexSet给UITableView指示在哪些section中插入或删除元素.
假设需要访问数组中下标为 1, 2, 3, 4, 8, 9, 10的元素, 可以先将这些下标存放在NSIndexSet对象中.
同样, NSIndexSet是不可变的, 它有一个可变子类 NSMutableIndexSet.
可以传递NSRange以构造NSIndexSet, 但下标情况复杂时, 可以使用NSMutableIndexSet, 利用append添加NSRange进去:
let arr = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 11]
let ixs = NSMutableIndexSet()
ixs.addIndexesInRange(NSRange(1...4))
ixs.addIndexesInRange(NSRange(8...9))
let arr2 = (arr as NSArray).objectsAtIndexes(ixs) //arr2 为[8, 7, 6, 5, 1, 0]
代码中首先构造一个NSMutableIndexSet对象ixs, 然后将该对象附加两个范围, 之后通过这个ixs指定的下标构造新数组.
可以使用for...in
遍历NSIndexSet中存放的下标,也可以使用enumerateIndexesUsingBlock:
或enumerateRangesUsingBlock:
以及它们的变体来遍历.
4.9 NSArray and NSMutableArray
NSArray是OC数组类型, 和Swift数组在功能上类似, 并且NSArray和Swift数组也进行了桥接.
与Swift数组相比, NSArray里面必须存放对象,并且对象类型可以不统一.
NSArray同Swift数组相互转换的方法详见书上.
TIP: 在iOS9中, 如果NSArray里面保存的对象都是同一种类型的,则可以在OC中标明该数组的类型.这样Swift中就可以直接读取该数组类型. 通过这样,从OC桥接回Swift就不会再收到一个[AnyObject], 而是一个实在的数组. 这同样适用于NSSet, 以及少部分NSDictionary.
NSArray的长度在count属性中,可以通过objectAtIndex
来获取它里面的元素.下标从0开始,故最后一个元素下标为count - 1.
除了使用objectAtIndex
,也可以直接使用下标操作符,即用诸如”[0]”获取指定下标的元素.
NSArray能够使用下标操作符, 并非因为它桥接到了Swift, 而是它里面实现了objectAtIndexedSubscript:
方法.
可以使用indexOfObject:
或indexOfObjectIdenticalTo:
方法来查找元素.前者调用该类中定义的isEqual, 而后者使用的是类似Swift中”===”的方式. 前面也说过,如果没有找到元素,则返回的是NSNotFound.
NSArray是不可变的,即不可以改变它里面保存的元素,但对于在每个元素内部的修改,NSArray是没有限制的.
NSArray有可变子类NSMutableArray, 使用它可以动态增减元素.
Swift的Array没有桥接到NSMutableArray.
如果从OC回到Swift, NSMutableArray也是一个[AnyObject], 但它不能直接转换成Swift的Array, 需要首先将它转换成NSArray, 然后再转换成对应类型的Swift中Array:
let marr = NSMutableArray()
marr.addObject(1) //数值先自动转存成了NSNumber以存放到可变数组中
marr.addObject(2)
let arr = marr as NSArray as! [Int] //先转换成NSArray,再转换成对应类型的Swift数组.
可以使用block在数组中查找或过滤元素, 还可以对数组排序, 只需指定排序规则. 对于可变数组而言, 直接可以进行排序. 当然在Swift数组中可以很方便实现这样的操作,但了解如何在Cocoa中进行也是非常重要的:
let pep = ["marry", "joe", "mood"] as NSArray
let ems = pep.objectsAtIndexes(pep.indexesOfObjectsPassingTest({ (obj, idx, stop) -> Bool in
return (obj as! NSString).rangeOfString("m", options: .CaseInsensitiveSearch).location == 0
}))
let s = ems as! [String]
print(s) //["marry", "mood"]
上面就是过滤并排序之后输出数组的例子.
4.10 NSDictionary and NSMutableDictionary
NSDictionary是OC中的字典类型, 在功能上和Swift中的字典类似, 它们二者也已被桥接.
NSDictionary的键和值都必须是对象, 键值对类型不必统一, 这和Swift中不一样. 作为键的对象必须接受NSCopying协议, 并且可散列.
NSDictionary和Swift字典的桥接及转换详见书上.
NSDictionary是不可变的, 它有可变子类NSMutableDictionary, 并且Swift字典没有桥接到NSMutableDictionary.
要构造一个可变字典, 可以直接使用它的构造方法: init()
或init(dictionary:)
.
NSDictionary的键可以用isEqual来判别比较. 如果你在可变字典内添加一个键值对, 若该键在字典中还未存在, 那么就直接将这个键值对加入到字典中. 但如果键已存在, 则会用新值覆盖对应键的值, 这和Swift字典的行为类似.
字典的最基本使用是通过键来获取值, 比如使用objectForKey:
方法, 如果key存在, 则返回对应值对象, 若不存在, 则返回nil. 但是在OC中, nil并非对象, 因此不能作为NSDictionary的值对象.
总的来说, 因为Cocoa大部分是OC写的, 所以需要遵守一些OC规则.
Swift处理objectForKey
的返回值的办法是将返回值作为一个AnyObject?
, 即一个包装任意类型的可选值.
在NSDictionary或NSMutableDictionary中都可以使用下标来访问键对应值, 和在NSArray中使用下标操作的道理类似. 在NSDictionary中实现了objectForKeyedSubscript:
方法, 而Swift将该方法等价为下标的getter. 另外在NSMutableDictionary中还实现了setObject: forKeyedSubscript:
方法, Swift将该方法等价为对下标的setter.
可以获取NSDictionary中的全部键的列表或全部值的列表, 或按值排序的键值对列表. 也可以使用Block来遍历键值对, 甚至可以通过测试来过滤NSDictionary中的特定值.
4.11 NSSet and Friends
NSSet是一个无关的互异对象集合. 它里面的对象都是互异的, 即任意两个对象使用isEqual
比较都不会返回true. 无关的就是指两个对象之间不存在逻辑关系.
在NSSet中的查询操作比数组中的查询操作效率更高, 并且对于一个Set, 可以查询它是否是某Set的子集, 或它与另一Set的交集.
使用for...in
遍历Set, 当然遍历出来的值是无序的.
可以过滤一个Set, 就和过滤Array类似.
可以看出, Set上的大部分操作都和数组类似, 当然在Set上无法进行需要元素有序为前提的操作, 比如下标操作.
可以使用NSOrderdSet来构造一个关系集合. NSOrderedSet和数组十分相似, 操作它的方法和数组也类似, 比如可以使用下标访问其中元素.
NSOrderedSet比数组具有更多优势, 比如查询效率上, 另外就是它可以直接进行两个NSOrderedSet之间的并集,交集,差集操作. 故在条件允许的情况下, 可以尽量使用NSOrderedSet.
TIP: 将一个数组传给NSOrderedSet, 意味着顺序仍然得以维护, 但只有互异的元素被传入到集合中.(即自动进行了一次去重操作, 顺序仍和数组中的一致)
NSSet是不可变的. 但可以通过从其他NSSet添加或删除元素得到新的NSSet.
NSSet有可变子类NSMutableSet.
当然NSOrderedSet也有其可变子类NSMutableOrderedSet(使用它可以用下标访问元素,因为其中实现了setObject: atIndexedSubscript:
方法).
向Set中加入已存在的元素不会出错, 只是添加操作不会发生.
NSMutableSet还有一个子类NSCountedSet, 这个子类是可变的, 并且元素允许重复, 它经常被称为bag.
NSCountedSet的实现其实就是一个Set外带记录每个元素出现次数的计数器.
Swift中的Set被桥接到NSSet. 但NSSet中元素必须是对象(类对象或类的实例对象), 而且元素类型不必统一.
NSMutableSet, NSOrderedSet, NSMutableOrderedSet, NSCountedSet都没有被桥接到Swift.
但可以先将NSMutableSet向上转换为NSSet, 然后再转换为Swift中Set(和NSMutableArray的转换类似).
NSOrderedSet从表面上看和Swift中的数组或set并无二致, 因为它们的行为基本相同, 但是建议不要轻易将NSCountedSet或NSOrderedSet转换到Swift中, 能让它们留着OC世界就尽量留在OC世界.
4.12 NSNull
NSNull的作用就是提供一个指向单例(NSNull对象)的指针,使用NSNull()
获取该单例.
某些情况下需要OC对象, 但又不允许使用nil, 此时就使用NSNull代表nil.
比如在OC的任何一种集合中都无法添加值为nil的元素, 因为nil本来不是对象, 这时为了代表nil, 就使用NSNull()来添加”意义上是nil的元素”对象.
可以在NSNull单例对象上使用”==”操作符, 它会自动调用NSObject里面的isEqual
方法, 即判断两个对象指针是否相等.
4.13 Immutable and Mutable
Cocoa中通常都是一个不可变类拥有一个可变子类的类组织方式. 不可变类和可变类就好比Swift中的let和var的区别.
比如使用NSArray, 就和在Swift中使用let定义一个数组的使用方式相同: 用户不能向这种数组中添加元素, 删除元素以及替换元素, 但如果获取到其中的元素, 在元素身上进行的修改, 数组是无权控制的.
而Cocoa框架中使用不可变/可变这样的类组织, 目的就是为了防止未授权的访问. 比如一个数组, 可以先在内部暂时使用它的可变子类, 当需要传递出去时, 则使用不可变的数组. 这样可以防止在外部的修改.
而Swift不存在这样的情况, 因为内部的对象如String, Array等, 修改它们的唯一方式就是创建副本并赋值新修改的副本, 这样的话就可以被setter observer自动检测到. 所以Swift中不用担心修改不被察觉的问题.
用户可以使用copy
或mutableCopy
生成某个对象的不可变或可变副本. 但这样的方式没有任何方便可言, 因为这些副本是AnyObject的,需要进行类型转换.
Warning: 这种不可变/可变类组织方式, 实际是由类簇(class cluster)实现的, 即用户使用的类只是一个接口, 而实际实现的类被隐藏在接口层次的下面. 不必去关系这些作为接口的类下面的实现类的细节. 想关心也关心不了.
4.14 Property List
Property List实际是字符串(XML), 用于存放数据.
只有如下几种类才可以被转换为Property List: NSString, NSData, NSArray, NSDictionary.
NSArray或NSDictionary转换成Property List的条件是: 包含的对象必须都是NSData或NSNumber类型的(这也是为什么在UIColor存储的例子里, UIColor必须先转换为NSData然后才可以存放到User Default中, 因为User Default也是Property List).
Property List的一个重要作用是将数据存储到文件中, 当需要用这些数据时, 还可以再次重构到某对象中.
NSArray和NSDictionary中writeToFile:atomically:
和writeToURL:atomically:
方法可以方便实现Property List的生成和存放, 只需要给出文件的路径或URL即可. 这两个类也提供了通过文件生成数组或字典的方法.
NSData和NSNumber中也提供了文件读写方法, 只是这些方法将对象数据直接写入文件(即写入的不是XML), 而非生成property list然后写入文件.
当由property list文件生成对应数组或字典对象时, 内部包含的字符串或数据都是不可变的.
如果想让它们可变, 或如果想将一个Property list对应的对象转换为另一类型的property list(比如字典Property list 和数组Property list的转换), 可以使用NSPropertyListSerialization
类型.详见苹果Property List Programming Guide.
5 Accessors, Properties, and Key-Value Coding
OC实例变量: 指这个OC变量引用的是一个对象(或说成是这个变量存放的是一个对象).
Swift实例属性: 指这个Swift属性引用的是一个对象.
OC中的实例变量和Swift中的实例属性类似: 它是一个变量, 这个变量是特定类型类的实例,即对象.
但OC的实例变量通常都是私有的, 即一个类看不到另外一个类的实例变量, Swift类也看不到OC类里面.
如果想让外界访问实例变量, 这个类就需要实现accessor方法: 一个setter和一个getter. 并且OC中的accessor方法有特定命名规范:
- getter方法: 该方法名和实例变量名相同, 并且不带下划线. 比如实例变量名为myvar或_myvar,则getter方法名必须是myvar.
- setter方法: 该方法名为实例变量名前面加set, 比如实例变量名为myvar或_myvar, 则setter方法为setMyvar.
OC提供了@property
, 使用这个指令声明的属性会自动获得setter和getter, 并且符合命名规范, 比如:
@property(nonatomic) CGRect frame;
则frame实例变量自动拥有accessor方法: getter为 frame:
, setter为 setFrame:
.
当Swift遇到OC中的@property的时候, 会自动将它等价为Swift中的属性, 上面的frame在Swift中即:
var frame : CGRect
OC的属性名实际是一个语法糖, 比如设置UIView的frame属性 ,使用的是.frame
语法, 实际是调用该属性的accessor方法.
OC中可以直接调用 setFrame
来设置属性, 但在Swift中是不允许的. 即如果OC类中有@Property声明的属性, 该属性的accessor方法对Swift隐藏的.
OC的属性修饰符中有readonly
,它和Swift中属性后的{get}类似, 表示该属性是只读的. 即当遇到readonly, Swift自动将它看做{get}.
5.1 Swift Accessors
因为OC中属性名作为访问方法的”快捷方式”, OC将Swift属性名也作为这种”快捷方式”使用, 即使没有实现对应的访问方法.
比如你在Swift类中定义了一个属性名为prop, 则在OC中你可以直接使用.prop
来访问这个属性, 读取或设置它, 即使在Swift中你并没有实现任何这样的访问方法. 其实这个调用被转接到了隐式访问方法调用上面去.
在Swift中, 不需要显式实现访问方法, 如果你尝试去实现, 编译器会提醒错误. 如果想实现这样的访问方法, 则使用computed property, 比如要设置ViewController的Color属性(computed property),在Swift中有如下定义:
var color : UIColor {
get {
print("getter was called.")
return UIColor.redColor()
}
set {
print("setter was called.")
}
}
则在OC中可以显式调用这个属性的accessor方法, 并且满足命名规范:
ViewController* vc = [ViewController new]; //OC方式新建一个ViewController对象
[vc setColor:[UIColor redColor]]; //显式调用setter, 输出 setter was called.
UIColor* c = [vc color]; //显式调用getter, 输出 getter was called
上面代码表明, 在Swift中使用computed property, OC会认为你实现了accessor方法.
甚至可以在Swift中改变accessor名称, 方法是将@objc(替换名)写到属性前面:
@objc(hue) var color : UIColor?
这样, 当OC中调用这个属性的accessor时, 就使用替换后的名称: getter是hue
, setter是setHue
.
另外可以在setter中添加额外操作, 比如想在UIView的子类中对应的OC的setFrame:方法中添加额外行为:
class Myview : UIView {
override var frame : CGRect {
didSet {
print("g")
}
}
}
5.2 Key-Value Coding (KVC)
Cocoa框架提供了运行时动态调用accessor的途径, 即在运行时通过字符串Key指定需要访问的属性Value, 这就是常说的Key-Value Coding(KVC)机制.
这个机制的原理和通过selector名称调用respondsToSelector
方法的原理类似( selector名称也是一个字符串).
指定的字符串就称为Key, 被Key定位之后, 可以调用该属性的setter或调用该属性getter, setter或getter访问的数据称为Value.
KVC的基石是NSKeyValueCoding
协议, 它是一个informal protocol, 为一个category, 这个category被注入到NSObject中.
如果Swift类对象要使用KVC, 则该对象必须属于NSObject类家族.
KVC中最基本的方法有两个:valueForKey:
以及setValue:forKey:
. 当某对象调用二者之一时, 这个对象便先进行自回应, 就是先尝试有无访问方法, 有则调用访问方法, 没有则直接访问该key名称对应的实例变量.
另外两个常用方法是: dictionaryWithValuesForKeys:
和setValuesForKeysWithDictionary:
, 这两个方法允许在字典上使用一条语句就访问(设置或获取)若干字典内的键值对.
KVC中的Value必须是OC对象, 在Swift看来就是[AnyObject]. 当调用 valueForKey:
时, Swift会接收到一个包装着[AnyObject]的可选对象, 随后就可以将它转换为需要的类型了.
说某个类是KVC兼容(Key-Value Coding compliant)是指这个类提供了对应key的访问方法, 或拥有对应key的实例变量.
如果尝试访问一个不兼容的key, 则出现运行时异常, 比如下面构造一个这样的异常:
let obj = NSObject()
obj.setValue("hello", forKey: "people") //Crash.
如果不想崩溃, 应该怎么做呢?
做法即保证该类中实现了对应key的accessor, 或具有对应key名称的实例变量.
比如上面例子中, 需要有一个setter方法: setPeople
或是一个实例变量:people
.
需要强调的是: 在Swift中实例属性就提供了accessor方法. 因此, 我们可以在任何继承于NSObject的类的Swift对象上面使用KVC, 只需要保证其中有Key对应的属性即可.
比如:
class Dog : NSObject {
var name : String = ""
}
//...使用时:
let d = Dog()
d.setValue("fido", forKey: "name") //完全可以
print(d.name) //输出fido, 可以看到起到了作用!
5.3 Uses of Key-Value Coding
虽说使用KVC和OO封装思想背道而驰, 但KVC在iOS编程中还是有用的, 尤其是在Cocoa中有特殊用途, 比如:
- 如果向NSArray发送
valueForKey:
, 相当于对数组中每一个元素对象都发送valueForKey:
消息, 并且返回一个新生成的value数组. 这种方法十分简便实用. 在NSSet中也有类似用法. - 在NSDictionary中实现了
valueForKey:
, 可以替代objectForKey:
, 这在当你需要操纵一个字典数组的时候尤其有用. 而NSMutableDictionary中的setValue:forKey:
方法等价于setObject:forKey:
, 而且前者设置的value可以是nil, 因为设置成nil时会自动调用removeObject:forKey:
. - NSSortDescriptor对数组进行排序的原理是向每一个元素发送
valueForKey:
消息. 这样可以方便进行字典数组的元素排序. - NSManagedObject经常和Core Data结合使用. 它是KVC兼容的, 当在实用模式下配置了它的属性, 就可以通过
valueForKey:
以及setValue:forKey:
来访问. - CALayer和CAAnimation允许用户使用KVC来定义或修改任意的键值, 就好像这些键值对就是存放在字典中的一样, 而实际上是因为KVC兼容. 这两个类对象通过KVC来配置的话是非常方便的. 而实际工作中也的确是经常使用KVC来配置这两个类的对象.
5.4 KVC and Outlets
KVC为Outlet提供幕后支持.
nib中的Outlet名称是一个字符串, KVC将该名称作为Key来定位到对应的对象属性上面.
假如你有一个Dog类, 它有个Outlet属性master, 你将这个属性联系到nib文件中的person对象上. 当person对象从nib加载时, outlet的名称master被KVC用于调用accessor方法setMaster:
, 然后Dog对象的set方法被隐式调用, 并将person对象设置到master上面.
如果nib文件中的Outlet名称和类中属性名不对应, 则会在运行时出错.
运行时, 当nib加载后, Cocoa会尝试利用KVC来设置nib对象到你的一个对象属性上面, 如果名称不对应, 则会设置失败. 结果是出现异常, 程序会在nib加载时崩溃.
这个错误常见于先配置好Outlet, 然后不小心修改了属性名却没有修改Outlet名的情况.
5.5 Key Paths
key path允许用户使用一条语句进行链式访问. 若对象兼容某个Key的, 而这个key对应的属性又是一个对象且对另外一个key是KVC兼容, 就可以将这些key用在key链上.
使用valueForKeyPath:
和setValue:forKeyPath:
方法来应用key链进行链式访问.
可以将KeyPath看作是一个字符串,只是每个key用点号分割, 如"key1.key2.key3"
.
例如有这样一个KeyPath: "key1.key2"
, 在valueForKeyPath:
中使用这个KeyPath, 实际过程是先用key1调用valueForKey:
, 再在返回值上利用key2继续调用valueForKey:
, 以此类推.
比如myObject中有一个theData属性:
var theData = [
[
"description" : "The one with glasses.",
"name" : "Manny"
],
[
"description" : "Looks a little like Governor Deway.",
"name" : "Moe"
],
]
这个属性本身是一个字典数组, 可以使用KVC, 并利用KeyPath来获取一个新数组:
let arr = myObject.valueForKeyPath("theData.name") as! [String]
上面代码中的KeyPath首先找到theData属性, 然后从这个属性中获取全部的名字, 然后构成一个新数组(原理请看5.3第一条).
TIP: nib中可以设置user-defined runtime attributes. 这个功能也是利用KVC. 而它的第一列所有的key都可以连成一个KeyPath.
5.6 Array Accessor
KVC是一个强大的技术, 并且它还有许多衍生技术.(详见苹果Key-Value Coding Programming Guide).
5.7 The Secret Life of NSObject
因为所有OC类都继承自NSObject, 故有必要单独讨论一下NSObject.
NSObject的构造十分精巧:
- 在它里面实现了一些用于对象实例化及消息的发送和解析的类方法及实例方法.(详见NSObject Class Reference.)
- 它接受NSObject协议. 该协议内声明了与诸如内存管理, 对象和类关系, 内省等相关的方法. 因为所有的NSObject协议方法都是required的, 所以NSObject将所有的方法都进行了实现. 在Swift中, NSObject协议被改名为NSObjectProtocol, 以此避免名字冲突. 详见(NSObject Protocol Reference.)
- 它实现了NSCopying协议, NSMutableCopying协议以及NSCoding协议相关方法, 但是没有接受这些协议(因为这些协议是在它里面声明的). 原因是如果NSObject接受了这些协议, 意味着所有的子类都接受这些协议, 这样是不好的. 正确的做法是让需要这些协议的子类来接受这些协议, 并实现协议内的方法.
- NSObject拥有20多个类扩展, 向它里面注入了大量方法, 这些类扩展分布在不同的头文件中. 比如awakeFromeNib方法就是从UINibLoadingAdditions类扩展中获得的, 在UINibLoading.h中声明.
- 一个类(class object)也是对象, 并且所有的OC类都是
Class
类型的对象, 并且这些类都继承自NSObject(包括Class
). 所有的NSObject实例方法都可以被类对象以类方法的形式调用.比如respondToSelector
方法是在NSObject中定义的对象方法, 但这个方法可以被任意的类对象作为类方法调用.
如果想知道某个类能够接收什么消息, 不用关心这个消息在何处声明, 你只需要弄清楚可以向这个对象发送哪些消息. 但这里出现了一个问题, 即苹果的文档不是以”可以向这个类型的对象发送什么消息”来组织的, 而是以”这个方法在哪里”的形式来组织的. 这样就导致了一个对开发者很不友好的情况发生:
即使基类NSObject, 文档中都没有一个对它的完整的描述, 而是需要同时查找NSObject Class Reference和NSObject Protocol Reference, 外带NSCopying, NSMutableCopying, NSCoding的帮助文档, 再加上对每个NSObject对象方法对应的类方法版本的自我理解.
另外还有一些方法是以类扩展形式插入到NSObject中的, 有一些方法列在了NSObject的类描述文档中, 但有些需要列出的却没有被列出, 只有在实际使用时去找.
不管来偷的还是用抢的, 只有将所有的NSObject方法都凑齐的时候, 才能真正知道NSObject的全貌:
构造, 析构以及内存管理:
用于创建对象的方法, 如alloc和copy, 另外还有一些关于对象生命期的方法, 如initialize何dealloc, 以及内存管理方法.
类关系描述:
用于确定对象的类和继承关系, 比如superclass, isKindOfClass及isMemberOfClass.
对象自回应和比较:
用于查询当发送某消息时对象的反应, 如respondsToSelector, 用字符串描述对象
description
, 以及对象比较isEqual
.消息响应:
向对象发送消息后用于干预对象行为的方法, 如
doesNotRecognizeSelector
. 详见Object-C Runtime Programming Guide.消息发送:
用于动态发送消息. 比如
performSelector
方法, 它使用selector作为参数, 使对象进行selector对应的行为. 假如当运行时才知道需要发送什么消息给该对象, 就只有使用这个方法. 另外,performSelector
的变体允许你在特定线程上面发送消息, 或允许在特定时间间隔后发送消息, 比如performSelector:withObject:afterDelay:
.
TIP: performSelector在Swift2.0之后才可用. 之前这个方法不能被Swift调用. 在实践寻找替代思路的过程中发现, 不使用这个方法也能很好将OC代码转移到Swift上.