有時候看一些討論區裡頭的內容,實在讓人不禁眉頭一皺。比方說,你看到有人問了這樣一個問題-
如果我有一個NSArray存放不固定數量的CGPoin,這些Point在drawRect中都被用來當作是draw的data,但其實這些點也要被某個我的Controller class來增減或改變。
請問這些data object(NSArray contain CGPoint),是放在View的class底下比較好, 還是放Controller的class底下,比較好ㄋ?
然後馬上出現的回應是:
其實你高興就好, MVC 不是強制的, 也有很多灰色地帶
是我的話這類東西通常放在 view, 不過那是我
是啦,雖然不是說不照某種方法設計,程式就會動不了,但是在直接跳到「MVC不是強制的」這種意見之前,是不是忽略了幾個討論-如果是用 MVC 的設計,應該要怎麼做,這種需求難道用 MVC 的方法沒有辦法解決嗎?蘋果自己是怎麼處理這樣的狀況?
另外一個很大的問題是-你是使用者的話,你會敢用抱持著「高興就好」的開發者寫出來的東西嗎?…
如果我們去看 AppKit 或是 UIKit,一定可以看到 view 要跟 controller 要資料的狀況,最常用來處理資料的 view 莫過於 NSTableView 或 UITableView,我們要怎樣把資料提供給 NSTableView 與 UITableView 呢?就是實作這些 table view 需要的 data source method;而所謂的 data source,就是特別強調資料內容這一部分的 delegate method。我相信任何一本講 ObjC、Cocoa 或 iPhone 開發的書,都會講到 delegate。
View 所要求的 delegate,其實就是在這個 view 的成員變數當中,有一個叫做 delegate 的物件指標,這個物件不是在 View 的 class 當中產生,而是在別的地方產生,然後告訴這個 view 這個物件的位置。而當 view 需要做某件事情的時候,就去問一下這個 delegate,遇到這件事情的時候應該怎麼處理,如果 delegate 沒有特別說要做什麼(也就是沒有實作某個 delegate method),那麼 view 就直接按照預設的行為繼續工作。
以 NSTableView 來說,view 就會先問 delegate,我應該要有幾行?然後,某一行裡頭應該是什麼資料?我現在要在這一行把這個資料畫出來了,有沒有什麼特別要做的,像是改改字體大小或顏色?使用者用滑鼠拖拉了 table 的內容,把第三行拖到了第一行,或是把某一行某一列的文字改過了,這時候原始資料的 Array 要不要改些什麼?諸如此類。
這樣的過程就分成三個部分規劃:
- View 要知道自己的 delegate 是誰
- Controller 要把自己設成 view 的 delegate
- 定義 delegate 與 view 之間可以溝通哪些事情,也就是所謂的 delegate methods
來寫點 Code
View 要知道自己的 delegate 是誰
我們現在 view 裡頭加一個成員變數,然後同時來一個 getter/setter。所以在 view 的介面我們這麼宣告-
@interface MyView : UIView { id dataSource; } @property (assign) id dataSource; @end
這邊是以 UIVIew (iPhone 的view)的 subclass 為例,我們產生了一個 UIView subclass,裡頭有一個成員變數叫做 dataSource,透過 ObjC 2.0 的 property 語法產生 getter/setter,記得要在實作的部份加上一行 「@synthesize dataSource」-也可以另外寫 getter/setter,這邊用比較簡單的作法。
Controller 要把自己設成 view 的 delegate
比方說我們要使用的 UIVewController 是 TestViewController,負責管理前面提到的那個 view,同時裡頭有一個 NSMutableArray,裡頭是要 view 負責繪製的點的座標資料。宣告可能會是這樣-
@interface TestViewController : UIViewController { MyView *myView; NSMutableArray *array; } @property (retain, nonatomic) IBOutlet MyView *myView; @end
要怎樣將 myView 的 dataSource(或 delegate )設成自己呢?我們在產生了 TestViewController 之後,做這麼兩件事情
array = [[NSMutableArray alloc] init]; // 產生用來畫圖用的 array 資料 myView.dataSource = self; // 把 myView 的 data source 設成自己
可以在 viewDIdLoad (iPhone)或 awakeFromNib 做這件事情。
定義 delegate methods
接下來,就是怎樣讓 myView 要在畫圖的時候,跟 controlelr 要資料。我們需要定義一個讓 view 跟 controller 通訊的 protocol。ObjC 有兩種方法可以做這種事情,一種叫做 formal protocol,相對的,就是 informal protocol,informal protocol 其實就是 NSObject 的某個 category,比方說,我們要定義一個 informal protocol,叫做 MyViewDataSource,就會這樣寫-
@protocol NSObject(MyViewDataSource) @end
如果是 formal protocol,則是這樣寫-
@protocol MyViewDataSource @end
有兩種方法,哪種方法比較好?在 iPhone 出來之前,Cocoa 比較多採用的是 informal protocol,但是從 iPhone SDK 問世之後,以及現在的 10.6 SDK,絕大部分都是採用 formal protocol。
使用 formal protocol 最大的好處是,如果你指定了某個 class 要當做 delegate/data source 使用,所以你就應該要在這個 class 中,實作某個 protocol 的 method,如果是 informal protocol,Xcode 在 compile time 的時候不會幫你檢查,但是會幫你檢查 formal protocol 有沒有什麼東西沒有實作。如果你寫 code 的習慣比較嚴謹,有檢查的東西總是比沒有檢查的好。
我們現在只要做一件事情,就是 view 跟 data source 要 array,所以介面上,只要定義一個 method 就好:
@protocol MyViewDataSource - (NSArray *)viewRequestArrayForDrawing:(MyView *)myView; @end
View 要怎樣在畫圖的時候跟 data source/delegate 要資料呢?UIView 負責畫圖的 method 是 -drawRect: ,我們在這邊來一行:
- (void)drawRect:(CGRect)rect { NSArray *array = [self.dataSource viewRequestArrayForDrawing:self]; // 拿到 array 之後,來看要怎麼畫.. } @end
Data source / delegate 把資料給 view 的方法,則是實作 viewRequestArrayForDrawing:
- (NSArray *)viewRequestArrayForDrawing:(MyView *)myView { return array; }
在設計 delegate method 的時候,我們往往會把是哪個物件傳來這個要求當做參數,比方說,在上面提到的 -viewRequestArrayForDrawing: ,就傳遞了 myView,在 controller 這邊,就可以知道,到底是哪一個 view 跟他要資料-因為同一個 controller,我們可以當成是很多個不同的 MyView instance 的 data source/delegate,於是,我們可以透過 view 的指標判斷-要資料畫圖的 view 到底是哪一個,然後我們可以給不同的 view 不同的資料。
最後
最後我們要在 MyView 與 TestViewController 的宣告中,指定 MyView 的 data source / delegate 應該實作 MyViewDataSource protocol,TestViewController 有實作 TestViewController protocol。語法像這樣-
@interface MyView : UIView { id <MyViewDataSource> dataSource; } @property (assign) id <MyViewDataSource> dataSource; @end
TestViewController 則是
@interface TestViewController : UIViewController <MyViewDataSource> @end
雖然文章有點久了,但是獲益良多,我卡在這裡卡好久了@@
一直以為viewController只要後面加了<某protocol>,然後把該實作的function寫一寫,就會有物件call它。原來還要把myView.dataSource指定給viewController才行….
UIKit 裡頭有些設計幫你省略了這些事情。例如,如果生了一個 UITableViewController ,在 load view 的時候,就已經把 self.tableView 的 delegate 與 dataSource 全都設成了 self。