1. 程式人生 > >讀書筆記--關於Cocoa框架中的類

讀書筆記--關於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:

方法. 過程是先繼承UIVIew, 然後在子類中實現drawRect:方法即可.

比如要在window中繪製一條水平線, 因為Cocoa中沒有繪製水平線的類, 故可自定義UIView的子類, 讓它繪製一條水平線即可.

方法如下所示:

  1. 新建工程並新建一個類, 命名為MyHorizontal, 這個類繼承自UIView.
  2. 修改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:方法配置顯示:

  1. 新建工程, 新建一個UILabel的子類, 命名為 MyBoundedLabel
  2. 在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

  1. NSRange:

    它是一個C結構體, 具有兩個屬性: location和length.

    比如location是1, length是2, 表示第零個元素和第一個元素.

    使用NSMakeRange創造NSRange.

    Swift裡面有Range Struct, 可以將NSRange轉換為Swift中的Range,方法是:

    let r = NSRange().toRange()  //可選型別的Range.
  2. 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 ReferenceNSAttributedString 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中不用擔心修改不被察覺的問題.

使用者可以使用copymutableCopy生成某個物件的不可變或可變副本. 但這樣的方式沒有任何方便可言, 因為這些副本是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物件到你的一個物件