讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 11.4 委託 >

11.4 委託

委託是一種面向對象的設計模式,指的是兩個對像間的關係,其中主對象的行為是通過另一個對像定制或協助處理的。第2個對象就是主對象的委託。這裡面不涉及子類化,實際上,第1個對象對委託類一無所知。

由於Cocoa實現了委託,下面來看看委託的運作方式。內建的Cocoa類有一個通常叫作delegate的實例變量(名字中當然會有delegate了)。對於Cocoa類的某些實例來說,你會將該實例變量的值設為你自己的類的實例。在運行中的某些時刻,Cocoa類會通過向其委託發送消息來決定接下來該做什麼:如果Cocoa類實例發現其委託不為nil,同時該委託可以接收這個消息,那麼Cocoa實例就會向其委託發送消息。

回憶一下第10章關於協議的介紹,委託大量使用了協議。過去,委託方法列在了Cocoa類的文檔中,其方法簽名通過非正式協議(NSObject的一個類別)來告知編譯器。但現在,類的委託方法通常會列在協議自己的文檔中。目前有70多個Cocoa委託協議,這表明Cocoa在大量使用委託。大多數委託方法都是可選的,但有時你會發現有些則是必需的。

11.4.1  Cocoa委託

要想通過委託定制Cocoa實例的行為,你需要從一個類開始,這個類需要實現相關的委託協議。當應用運行時,你會將Cocoa實例的delegate屬性(或其他名字)設為你的類一個實例。你可以通過代碼完成,也可以在nib中完成,方式是將實例的delegate插座變量(或其他名字)連接到作為委託的恰當的實例上。除了作為該實例的委託,委託類還可能會做其他一些事情。事實上,委託的一個好處就在於你可以隨意在類架構中插入委託代碼;委託類型是個協議,因此實際的委託可以是任何類的實例。

在這個簡單的示例中,我要確保應用的根視圖控制器(一個UINavigationController)不允許應用旋轉,當該視圖控制器起作用時,應用界面只能位於縱向。不過UINavigationController並不是我定義的類;它是Cocoa定義的。我自己的類是個不同的視圖控制器,即UIViewController子類,它作為UINavigationController的孩子。那麼這個孩子如何告訴父親該如何旋轉呢?UINavigationController有個類型為UINavigationControllerDelegate(這是個協議)的delegate屬性。在需要知道該如何旋轉時,它會向這個委託發送navigationControllerSupportedInterfaceOrientations消息。因此,為了能夠對非常早期的生命週期事件作出響應,我的視圖控制器會將其自身設為UINavigationController的委託。它還實現了navigationControllerSupportedInterfaceOrientations方法。問題很快就迎刃而解了:


class ViewController : UIViewController, UINavigationControllerDelegate {
    override func viewDidLoad {
        super.viewDidLoad
        self.navigationController?.delegate = self
    }
    func navigationControllerSupportedInterfaceOrientations(
        nav: UINavigationController) -> UIInterfaceOrientationMask {
            return .Portrait
    }
}
  

Apple的共享應用實例UIApplication.sharedApplication()有一個委託,它在應用的生命週期中扮演著重要的角色,甚至連Xcode應用模版都會自動提供一個,即名為AppDelegate的類。第6章介紹過如何通過調用UIApplicationMain來啟動應用,它會實例化AppDelegate類,並讓該實例成為共享應用實例(已經創建好了)的委託。正如第10章所指出的那樣,AppDelegate正式使用了UIApplicationDelegate協議,這表示它已經為該角色做好了準備;接下來會向應用委託發送respondsToSelector:,看看實現了哪些UIApplicationDelegate協議方法。然後會向應用委託實例發送消息,讓其知曉應用生命週期中的主要事件。這正是UIApplicationDelegate協議方法UIApplicationDelegateOptions:如此重要的原因所在;它是你的代碼可以運行的最早期階段之一。

UIApplication委託方法也用作通知。這樣,除了應用委託,其他實例也能很便捷地接收到應用生命週期事件(通過註冊)。還有其他一些類提供了類似的重複事件;比如,UITableView的tableView:didSelectRowAtIndexPath:委託方法就是通過通知UITableViewSelectionDidChangeNotification進行匹配的。

根據約定,很多Cocoa委託方法名都會包含情態動詞should、will或did。will消息會在某件事發生前發送給委託;did消息會在某件事剛剛發生後發送給委託。should消息比較特殊:它返回一個Bool,如果為true就做出響應;如果為false就不會。文檔會列出其默認響應是什麼;如果默認響應可以接受,那就無須再實現should方法了。

在很多情況下,屬性會控制某種總體性行為,而我們可以通過委託方法在運行期根據情況來修改該行為。比如,用戶是否可以輕拍狀態欄讓滾動視圖快速滾動到頂部是由滾動視圖的scrollsToTop屬性決定的;不過,即便該屬性值為true,你也可以通過讓滾動視圖委託的scrollViewShouldScrollToTop:返回false來針對某種特定的輕拍動作而禁止該行為。

在搜索文檔以查找如何收到某種事件的通知時,請確保查看相對應的委託協議(如果有)。你可能想要知道用戶什麼時候輕拍了UITextField開始進行編輯了?這是無法在UITextField類文檔中找到的;你需要查看的是UITextFieldDelegate協議的textFieldDidBeginEditing:,諸如此類。

11.4.2 實現委託

對於Cocoa中的委託來說,其職責是由協議來描述的,這種模式值得你在編寫代碼時效仿。你需要通過實踐來掌握這種模式,並且要多花時間,不過它通常都是正確的解決方案,因為它會恰當地將知識與職責分配給相關的各種對象。

我們來考慮一個實際的情況。在我開發的一個應用中有一個視圖控制器,其視圖包含了3個滑塊,用戶可以移動滑塊來選擇顏色。此外,該視圖控制器是UIViewController的子類,名字是ColorPickerController。當用戶輕拍Done或Cancel時,視圖會隱藏起來;不過首先,用於展現該視圖的代碼需要知道用戶選擇了哪個顏色。因此,我需要從ColorPickerController實例向展現該視圖的實例發送一條消息。

下面是個消息聲明,ColorPickerController在銷毀前會發送這條消息:


func colorPicker (picker:ColorPickerController,
    didSetColorNamed theName:String?,
    toColor theColor:UIColor?)
  

問題在於:應該在哪裡以及如何聲明這個方法呢?

現在,我知道應用中實際用於呈現ColorPickerController:的實例所對應的類,那就是SettingsController。因此,我可以在SettingsController中聲明這個方法。不過,如果這麼做,那就意味著為了向SettingsController發送這條消息,ColorPickerController必須得知道用於呈現它的視圖是SettingsController。但讓SettingsController接收消息僅僅是個特例而已;它應該對用於呈現與隱藏ColorPickerController的所有類開放,這樣才能接收到這條消息。

因此,我們希望ColorPickerController本身來聲明自己會調用的方法;它可以向某個接收者隨意發送消息,無論該接收者所對應的類是什麼都如此。這正是協議的用武之地!解決方案就是讓ColorPickerController定義一個協議,並且將該方法作為協議的一部分;讓呈現ColorPickerController的類遵循該協議。ColorPickerController還有一個類型適當的delegate屬性;這提供了通信的通道,並且告訴編譯器發送這條消息是合法的:


protocol ColorPickerDelegate : class {
    // color == nil on cancel
    func colorPicker (picker:ColorPickerController,
        didSetColorNamed theName:String?,
        toColor theColor:UIColor?)
    }
    class ColorPickerController : UIViewController {
        weak var delegate: ColorPickerDelegate?
    // ...
}
  

(請參見第5章瞭解這裡所用的weak特性的含義與原因。)當SettingsController實例創建並配置好了ColorPickerController實例後,它還會將自身設為ColorPickerController的delegate——它是可以這麼做的,因為它使用了協議:


extension SettingsController : ColorPickerDelegate {
    func showColorPicker {
        let colorName = // ...
        let c = // ...
        let cpc = ColorPickerController(colorName:colorName, andColor:c)
        cpc.delegate = self
        self.presentViewController(cpc, animated: true, completion: nil)
    }
    func colorPicker (picker:ColorPickerController,
        didSetColorNamed theName:String?,
        toColor theColor:UIColor?) {
            // ...
    }
}
  

現在,當用戶選擇顏色時,ColorPickerController就知道該向誰發送colorPicker:didSetColorNamed:toColor:了,就是其委託!編譯器也允許這麼做,因為委託使用了ColorPickerDelegate協議:


@IBAction func dismissColorPicker(sender : AnyObject?) { // user tapped Done
    let c : UIColor? = self.color
    self.delegate?.colorPicker(
        self, didSetColorNamed: self.colorName, toColor: c)
}