1. 程式人生 > >Split View Controller在應用的中的若干問題及解決

Split View Controller在應用的中的若干問題及解決

               

Split View Controller是iPad中最具特色的檢視控制器之一。它充分利用iPad橫豎屏轉換時的螢幕空間變化,提供了以左右分欄或popover來進行導航的介面檢視。但它在使用上的複雜程度遠不是TableView Controller之類的控制器所能相比。而且由於其本身所具有的限制,我們無法象使用其他控制器元件一樣任意使用它。本文總結了iPad應用中SplitView Controller的一些問題及適用的解決方法,希望能起到拋磚引玉的效果。

一、從IB構建SplitView Controller

對於Split View Controller來說,通過程式碼來使用是比較簡單的,因此不用多講,相信大家並不陌生。但在IB中構建SplitView Controller,尚未有人介紹。雖然在筆者另一篇博文“iPad開發:UISplitViewController應用”曾有過介紹,但那是在Xcode3.2下實現的,隨著Xcode已經升級至4.2,筆者覺得有必要再次羅嗦一番。

新建3個View Controller類:iPadHelpVC、iPadHelpIndexVC、iPadHelpContentVC,注意勾選“WithNib…”。

1、iPadHelpVC

這是一個普通的View Controller,但我們在其中拖入了一個SpliteView Controller元件:

View物件是一個空白UIView。它不包含實質的內容。我們在使用iPadHelpVC類時,主要是為了使用它的Xib檔案中的SplitView Controller物件,因此這個View物件只是個擺設。

提示:你不能刪除View物件,因為它會導致一個IB物件連線錯誤——因為View Controller的view屬性必須連線到一個UIView。

重要的是Split View Controller物件。它下面會自動包含一些子物件:一個NavigationController、一個View Controller。在Navigation Controller下面又包括一個Navigation Bar和一個TableView Controller。

下面我們要對這些物件進行連線。

將Table View Controller的Identity Class修改為iPadHelpIndexVC,待會我們要用它來提供SplitView Controller左邊的導航列表。

將View Controller得Identifty Class修改為iPadHelpContentVC,待會我們用它來提供SplitView Controller右邊的內容檢視。

在iPadHelpVC類中宣告一個出口:

@property (nonatomic, retain) IBOutlet UISplitViewController *splitVC;

⋯⋯

@synthesize splitVC;

將Split View Controller物件和這個splitVC出口連線起來,便於我們在Xcode中引用。

在iPadHelpVC的viewDidLoad方法中:

// Split ViewController 只能作為window的根檢視控制器

SplitDemoDelegate*app=(SplitDemoDelegate*)[[UIApplication sharedApplication]delegate];

    app.stubVC=app.window.rootViewController;

app.window.rootViewController=splitVC;

SplitDemoDelegate是我們這個示例程式的應用程式委託類。我們在這個類中定義了一個頂層物件stubVC:

@property(retain,nonatomic)UIViewController* stubVC;

⋯⋯

@synthesize stubVC;

提示:關於頂層物件,簡單地說就是app 全域性物件。參考另一篇博文: 單例,應用程式委託和頂層資料。

我們需要先在stubVC中儲存一份window.rootViewController的引用。因為SplitView Controller只能在window物件的rootViewController上應用。如果我們不想讓app從頭至尾只使用一個Split ViewController的話,我們需要保持住導航到Split View Controller之前的那個檢視控制器(也許是一個View Controller,也許是一個NavigationController)。這樣我們可以從Split View Controller再次導航回前面的View Controller。

上面的工作做完後,在IB的Objects面板顯示如下:

最後,我們需要將Split View Controller的delegate和I PadHelp IndexVC進行連線。這樣可以在iPadHelpIndexVC類中加入一些程式碼,定製Split View Controller的行為。

選擇Split View Controller物件,在Connections面板中將delegate右邊的圓圈拖到I Pad Help IndexVC物件:

2、iPadHelpIndexVC

這個類提供了左邊欄的導航列表。一般來說,它應該是UITableView子類。它會自帶一個TableView物件,並實現UITableViewDataSouce和UITableViewDelegate協議。用於Split View Controller的delegate是iPadHelpIndexVC,因此還需要宣告實現UISplitViewControllerDelegate協議:

@interface iPadHelpIndexVC :UITableViewController

<UISplitViewControllerDelegate,UITableViewDelegate,UITableViewDataSource>{

說明一個數組作為table view物件的資料模型:

NSArray*model;

宣告一個出口,用於和Split  View Controller進行連線:

@property (nonatomic, assign) IBOutlet UISplitViewController*splitViewController;

⋯⋯

@synthesize popoverController;

然後回到iPadHelpVC.xib,將出口splitViewController和SplitView Controller物件連線在一起:

在iPadHelpIndexVC的viewDidiLoad中,我們初始化model並在model中新增一些資料:

model=[[NSArray alloc]initWithObjects:@"文件1",@"文件2", nil];

接下來,我們先實現Table View的DataSource 方法:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

{

    return 1;

}

- (NSInteger)tableView:(UITableView *)tableViewnumberOfRowsInSection:(NSInteger)section

{

      return model.count;

}

- (UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath *)indexPath

{

    static NSString *CellIdentifier = @"CellIdentifier";

    UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil) {

       cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];

    }

    cell.textLabel.text = [model objectAtIndex:indexPath.row];

    return cell;

}

當用戶點選左邊欄導航列表中的條目,我們修改右邊欄的內容顯示:

- (void)tableView:(UITableView *)tableViewdidSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

    iPadHelpContentVC <DetailViewController>*detailViewController = nil;

    detailViewController= [[iPadHelpContentVC alloc] initWithNibName:@"iPadHelpContentVC" bundle:nil];

    // 修改 split view controller的viewControllers屬性.

    NSArray *viewControllers = [[NSArray alloc] initWithObjects:self.navigationController, detailViewController,nil];

    splitViewController.viewControllers =viewControllers;

detailViewController.lbTitle.text=[model objectAtIndex:indexPath.row];

    [viewControllersrelease];

    // 如果popover視窗在彈出中,解散

    if (popoverController!= nil) {

        [popoverController dismissPopoverAnimated:YES];

    }

    // 重新設定右邊欄的popover按鈕

    if (rootPopoverButtonItem!= nil) {

        [detailViewController showRootPopoverButtonItem:self.rootPopoverButtonItem];

    }

    [detailViewControllerrelease];

}

這個方法中用到的兩個屬性: popoverController 和 rootPopoverButtonItem宣告如下:

@property (nonatomic, retain) UIPopoverController*popoverController;

@property (nonatomic, retain) UIBarButtonItem*rootPopoverButtonItem;

⋯⋯

@synthesize popoverController;

@synthesize rootPopoverButtonItem;

協議DetailViewController 聲明瞭兩個必須由iPadHelpContentVC實現的方法:

@protocol DetailViewController

- (void)showRootPopoverButtonItem:(UIBarButtonItem*)barButtonItem;

- (void)invalidateRootPopoverButtonItem:(UIBarButtonItem*)barButtonItem;

@end

這兩個方法在iPad方向轉變為豎屏和橫屏時呼叫。

最後,我們需要在iPadHelpIndexVC中實現Split ViewController的委託方法。

// 本方法用於豎屏時彈出popover

- (void)splitViewController:(UISplitViewController*)svcwillHideViewController:(UIViewController *)aViewControllerwithBarButtonItem:(UIBarButtonItem*)barButtonItemforPopoverController:(UIPopoverController*)pc {

    // 從引數獲得按鈕和popover controller的引用.

    barButtonItem.title = @"文件";

    self.popoverController =pc;

    self.rootPopoverButtonItem = barButtonItem;

// 獲取右邊欄,在右邊欄中顯示按鈕

    UIViewController<DetailViewController> *detailViewController = [splitViewController.viewControllers objectAtIndex:1];

    [detailViewControllershowRootPopoverButtonItem:rootPopoverButtonItem];

}

// 本方法用於橫屏時顯示左邊欄並消除popover按鈕

- (void)splitViewController:(UISplitViewController*)svcwillShowViewController:(UIViewController *)aViewControllerinvalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem {

    // splite view controller的viewControllers屬性管理了兩個View Controller:左邊欄、

    // 右邊欄,它們分別用索引0和1訪問。

UIViewController<DetailViewController> *detailViewController =[splitViewController.viewControllers objectAtIndex:1];

// 清除popover按鈕(根據DetailViewController協議)

   [detailViewControllerinvalidateRootPopoverButtonItem:rootPopoverButtonItem];

    // 釋放

self.popoverController =nil;

    self.rootPopoverButtonItem = nil;

}

3、iPadHelpContentVC

這個類,很簡單,我們也不準備實現實質性的功能,僅僅是在工具欄的Label上顯示選單的標題。因此它僅包含了一個ToolBar和一個Label物件:

這兩個物件都需要相應出口進行連線:

@property (nonatomic, retain) IBOutlet UIToolbar *toolbar;

@property (nonatomic,retain)IBOutlet UILabel* lbTitle;

⋯⋯

@synthesize toolbar;

@synthesize lbTitle;

然後我們把它們連線在一起:

根據iPadHelpIndexVC中的介紹,iPadHelpContentVC類是需要實現DetailViewController協議的:

@interface iPadHelpContentVC : UIViewController

<DetailViewController>

⋯⋯

#pragma markDetailViewController 協議實現

- (void)showRootPopoverButtonItem:(UIBarButtonItem *)barButtonItem {

    // 在工具欄上加一個popover按鈕,用於彈出導航列表

    NSMutableArray*itemsArray = [toolbar.items mutableCopy];

    [itemsArray insertObject:barButtonItem atIndex:0];

    [toolbar setItems:itemsArray animated:NO];

    [itemsArray release];

}

- (void)invalidateRootPopoverButtonItem:(UIBarButtonItem*)barButtonItem {

    // 橫屏顯示時,將popover按鈕移除

    NSMutableArray*itemsArray = [toolbar.items mutableCopy];

    [itemsArray removeObject:barButtonItem];

    [toolbar setItems:itemsArray animated:NO];

    [itemsArray release];

}

二、在RootViewController中呼叫SplitViewController

假設我們的程式並不是一來就顯示Split View Controller,那麼我們需要將window的rootViewController設定為SplitView Controller物件。這個工作其實已經在iPadHelpVC類的viewDidLoad中做了,因此我們只需要把iPadHelpVC當做普通的ViewController來顯示就可以了。你可以用presentModalView或者pushViewController顯示SplitView Controller:

iPadHelpVC* helpVC=[[iPadHelpVC alloc]initWithNibName:@"iPadHelpVC"

bundle:nil];

        [self.navigationController pushViewController:helpVC animated:YES];

注意:由於viewDidLoad只會在initWithNibName方法中呼叫,因此每次顯示Split View Controller時你必須呼叫initWithNibName方法重新初始化helpVC,否則SplitView Controller不能顯示(這跟Tab Bar Controller是一樣的)。

三、從SplitViewController返回

我們的app存在多個View Controller(起碼兩個,一個Split ViewController和一個其他的View Controller),並且Split View Controller並不是第一個控制器,因此我們必須考慮如何從SplitView Controller返回第一個檢視的問題。

我們首先決定在Split  View Controller的右邊欄加一個返回按鈕。原因很簡單,因為左邊欄在豎屏時不顯示,而右邊欄無論橫屏豎屏總是顯示。

開啟iPadHelpContentVC.xib,在工具欄上放一個Bar ButtonItem,並讓它和相應的IBAction連線:

-(IBAction)backAction;

⋯⋯

-(void)backAction{

    DLTAppDelegate* app=(DLTAppDelegate*)[[UIApplication sharedApplication]delegate];

    app.window.rootViewController=app.stubVC;

    UINavigationController* nc=(UINavigationController*)app.stubVC;

    [nc popViewControllerAnimated:YES];

}

這裡,我們重新把window的rootViewController設定回原來的Controller。

提示:你可能奇怪這個stubVC是什麼時候儲存的。 這是在iPadHelpVC的viewDidLoad方法中:

SplitDemoDelegate* app=(SplitDemoDelegate*)[[UIApplication sharedApplication]delegate];

    app.stubVC=app.window.rootViewController;

此外,最後一句“[nc popViewControllerAnimated:YES];”稍微顯得有些奇怪。因為iPadHelpVC本身還是一個ViewController(它還有一個無用的view屬性),當你pushViewController時,實際上把這個帶有空白View的iPadHelpVC壓入navigationController的棧中了。當你恢復rootViewController時,自然將壓入棧頂的空白View顯示出來了。如果你去掉最後的這句,當從SplitView Controller返回原根檢視時,會返回iPadHelpVC的這個View介面(空白窗體,但帶一個Navigation Bar)。而此時你必須點選NavigationBar上的“返回”按鈕才能返回根檢視。

四、一個Bug

當你執行程式,你會發現如下Bug:

Bug描述:每當你彈出一次popover選單並選擇其中一項,則popover按鈕(即文件按鈕)會往右邊移動一點位置。對比第1張和第3張截圖,你會發現popover按鈕的位置往右移動了約一個BarItem的距離。重複上述動作,popover按鈕會不斷右移,直到不可見。

這個問題在豎屏時出現。在iPadHelpContentVC中,我在工具欄中放入了一個FlexibleSpace Bar Button Item和一個Bar Button Item,以便在右邊欄中顯示退出按鈕:

這就會導致上面的Bug出現。

暫時想到的解決辦法是不要在xib中放入任何Bar Button Item,而改用程式碼動態生成所有Bar Button Item:

- (void)showRootPopoverButtonItem:(UIBarButtonItem *)barButtonItem {

    // Add the popover button to thetoolbar.

    NSMutableArray *itemsArray = [[NSMutableArray alloc]init];

    [itemsArray addObject:barButtonItem];

    UIBarButtonItem* item=[[UIBarButtonItem alloc]

                          initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace

                          target:nil action:nil];

    [itemsArray addObject:item];

    [item release];

    item=[[UIBarButtonItem alloc]

          initWithTitle:@"返回"

          style:UIBarButtonItemStyleBordered

          target:self action:@selector(backAction)];

    [itemsArray addObject:item];

    [item release];

    [toolbar setItems:itemsArray animated:NO];

    [itemsArray release];

}