讀書筆記--關於Cocoa框架中的類
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:
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物件到你的一個物件