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];
}