haoxue |
2011-11-15 11:57 |
iOS开发基础:iPad开发注意事项 分割视图和浮动窗口 之前花了较长篇幅介绍基于表视图中的选择实现应用程序导航,其中每个选择都会导致顶级视图(填满整个屏幕的视图)滑到左边并调出层次结构中的下一个视图,也可能是另一个表视图。许多iPhone和iPod touch应用程序都以这种方式工作,包括苹果公司自己的应用程序和第三方应用程序。 一个典型的例子是Mail,它支持在服务器和文件夹中下钻,直到最终找到邮件。从技术上讲,此方法也适用于iPad,但它会导致用户交互问题。在iPhone或iPod touch这样大小的屏幕上,让一个屏幕大小的视图滑走以显示另一个屏幕大小的视图,这没有什么问题。然而在iPad这样大小的屏幕上,同样的交互让人感觉不太对劲,有点夸张,甚至有点让人窒息。此外,在这样大的显示屏上仅显示一个表视图在许多情况下有点浪费。 所以,你会看到内置的iPad应用程序没有按此方式工作。任何下钻导航功能(比如Mail中所使用的)都移交给了一个较窄的栏,在用户下钻或返回时它的内容会向左或右滑动。在横向模式下使用iPad时,导航栏固定在左侧的一个位置上,所选项的内容在右侧显示。这就是iPad中所谓的分割视图(参见图10-1)。 分割视图的左侧始终为320像素宽(与纵向模式下的iPhone的宽度相同),分割视图本身并列显示导航栏和内容,并且仅在横向模式下显示。如果将iPad切换为纵向,分割视图仍然有效,但它的显示方式将不同。导航视图不再固定在一个位置,可以点击一个工具栏按钮来激活它,这会导致导航视图弹出一个视图,该视图漂浮在屏幕上所有其他内容的前方(参见图10-2)。这就是所谓的浮动窗口(popover)。
图1 图1:此iPad处于横向模式,显示了一个分割视图。导航栏位于左侧。点击导航栏中的一项(在本例中为特定的邮件账户),该项的内容会在右侧区域中显示
图2 图2:此iPad处于纵向模式,没有显示相同的分割视图。横向模式下分割视图左侧的信息嵌入到了一个浮动窗口中 在本章的示例项目中,将会介绍如何创建一个同时使用分割视图和浮动窗口的iPad应用程序。 创建SplitView项目 开始的步骤非常简单,利用Xcode预定义的一个模板创建一个分割视图项目。我们将构建一个应用程序,它以与第9章介绍的总统应用程序稍微不同的方式进行显示,列出所有美国总统并显示你所选择的总统的Wikipedia条目。 转到Xcode并选择File→New Project…。从iOS Application组中选择Split View-based Application。确保清除了Core Data复选框,并将新项目命名为Presidents。Xcode将执行常规的操作,创建一些类和.xib文件,然后显示该项目。展开Classes和Resources文件夹,浏览其中的每一项。 项目最初包含一个应用程序委托(和平常一样)、类RootViewController和DetailViewCon- troller。这两个视图控制器分别表示将在分割视图左侧和右侧显示的视图。RootView- Controller定义导航结构的顶级视图,DetailViewController定义在选择某个导航元素时在较大的区域中显示的内容。当应用程序启动时,这两部分都包含在分割视图内,你可能还记得,在旋转设备时它们会稍微调整一下形状。 要查看这个应用程序模板提供了哪些功能,可以在模拟器中构建并运行它。在横向模式(参见图10-3)和纵向模式(参见图10-4)之间切换,就会看到分割视图的实际应用。在横向模式下,分割视图在左侧显示导航视图,在右侧显示详细信息视图。在纵向模式下,详细信息视图占据了屏幕的大部分空间,导航元素被限制在浮动窗口中,点击视图左上角的按钮就会调出该窗口。
图3 图3:此屏幕截图显示了横向模式下默认的Split View-based Application模板。请注意此图与图10-1之间的相似性
图4 图4:此屏幕截图显示了纵向模式下默认的Split View-based Application模板。请注意此图与图10-2之间的相似性 我们将以此为基础构建我们想要的总统显示应用程序,但首先来了解一下已经存在的内容。 xib定义结构 很快你就拥有了一组相当复杂的视图控制器:分割视图控制器包含所有元素,导航控制器处理分割视图左侧的操作,根视图控制器位于导航视图内,详细信息视图控制器位于右侧。 在我们使用的默认的Split View-based Application模板中,这些视图控制器主要在主.xib文件中设置和互连,而不是在代码中。除了进行GUI布局,Interface Builder还允许连接不同的组件,无需编写大量代码来建立关系。我们深入分析一下项目的.xib文件,看一下各项内容是如何设置的。 在Resources文件夹中,双击DetailView.xib,在Interface Builder中打开它。此.xib文件非常简单,仅包含一个视图(其中有一个标签)和作为其文件所有者的DetailViewController类。在了解了其他控制器如何结合在一起之后,我们会对此文件进行一些更改。现在可以关闭DetailView.xib了。 回到Xcode中,双击MainWindow.xib,在Interface Builder中编辑它。你一定希望从图标视图切换到列表视图,以更好地了解对象层次结构。图10-5显示了需要查看的所有对象。
图5 图5:在InterfaceBuilder中打开的MainWindow.xib。这个复杂的对象层次结构最好在列表模式下结合连接检查器进行查看 打开连接检查器,单击每个视图控制器,了解它们彼此之间的关系。应用程序委托包含除导航控制器外的其他每个视图控制器的输出口。在其他控制器中,可以看到大部分都没有指向其他控制器的输出口,而一些控制器“包含”一个或多个其他控制器,比如分割视图和导航控制器,分割视图包含导航和细节控制器,导航控制器包含根视图控制器。根视图控制器是个例外,它拥有详细信息视图控制器的输出口,这使它能够在用户的选择更改时更新详细信息视图。 接下来打开属性检查器并单击每个视图控制器。请注意属性检查器包含一个View Controller部分,该部分包含一个NIB Name字段。对于大部分视图控制器,NIB Name字段都是空的,但对于详细信息视图控制器,它设置为DetailView,回想一下就知道这是我们查看的第一个nib文件的名称。这意味着当加载MainWindow.xib和创建详细信息视图控制器时,该控制器不会在相同nib文件中查找自己的GUI,而是加载所指定的其他nib文件。 这种设置带来了很高的灵活性,你可以自由决定哪些视图属于主视图和希望将哪些视图放在单独的nib中。在本例中,MainWindow.xib实际包含一个“有趣的”视图控制器,它的视图从一个外部nib文件加载,所有其他视图控制器是表视图控制器(自动创建它们自己的表视图)或容器视图控制器(比如UISplitViewController和UINavigationController),它们从不显示任何可编辑的视图。 MainWindow.xib中的剩余部分实际上定义了应用程序的各种控制器的互连方式。与大多数使用nib文件的情形一样,这么做消除了大量代码,在大多数时候都是有利的。如果希望在代码中完成所有这些配置,完全可以这么做,但是对于本例,我们将继续使用Xcode所提供的内容。 代码定义功能 在nib文件中完成视图控制器互连的一个主要原因在于,它使源代码不会被不需要存在于这里的配置信息弄得凌乱不堪,因此剩下的只是定义实际功能的代码。首先看一下我们有哪些源代码。Xcode在创建项目时定义了多个类,我们将大概浏览一下它们,然后再进行更改。 首先是PresidentsAppDelegate.h,它类似于: [pre]#import <UIKit/UIKit.h>
- @class RootViewController;
@class DetailViewController;
- @interface PresidentsAppDelegate : NSObject <UIApplicationDelegate> {
UIWindow *window;
- UISplitViewController *splitViewController;
RootViewController *rootViewController;
- DetailViewController *detailViewController;
}
- @property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UISplitViewController *splitViewController;
- @property (nonatomic, retain) IBOutlet RootViewController *rootViewController;
@property (nonatomic, retain) IBOutlet DetailViewController *detailViewController;
- @end
它与目前为止本书中介绍的其他几个应用程序委托非常相似。最大的区别在于其中包含多个控制器的输出口,而不是一个。这使我们能够使用应用程序委托作为一个中央点,可以通过该中央点访问所有控制器。再看一下PresidentsAppDelegate.m中的实现,它看起来类似于以下形式(为了保持简洁,我们删除了大部分注释和空方法): [pre]#import "PresidentsAppDelegate.h"
- #import "RootViewController.h"
#import "DetailViewController.h"
- @implementation PresidentsAppDelegate
@synthesize window, splitViewController, rootViewController, detailViewController;
- - (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOpti*****:(NSDictionary *)launchOpti***** {
- // Override point for customization after app launch.
// Add the split view controller's view to the window and display.
- [self.window addSubview:splitViewController.view];
[self.window makeKeyAndVisible];
- return YES;
}
- - (void)dealloc {
[splitViewController release];
- [window release];
[super dealloc];
- }
@end [/pre]这里也没有什么新颖的内容,此代码设置应用程序主视图(在本例中为将包含所有其他视图的UISplitViewController)的显示,就像在本书中多次执行的操作一样,最后执行清理。 现在看一下RootViewController,它控制包含应用程序导航的表视图的设置。RootViewController.h类似于: [pre] [/pre][pre]#import <UIKit/UIKit.h>
- @class DetailViewController;
@interface RootViewController : UITableViewController {
- DetailViewController *detailViewController;
}
- @property (nonatomic, retain) IBOutlet DetailViewController *detailViewController;
@end [/pre]与它对应的RootViewController.m文件类似于以下形式(删除了其中的非代码内容): [pre]#import "RootViewController.h"
- #import "DetailViewController.h"
@implementation RootViewController
- @synthesize detailViewController;
- (void)viewDidLoad {
- [super viewDidLoad];
self.clearsSelectionOnViewWillAppear = NO;
- self.contentSizeForViewInPopover = CGSizeMake(320.0, 600.0);
}
- - (BOOL)shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)interfaceOrientation {
- return YES;
}
- - (NSInteger)numberOfSecti*****InTableView:(UITableView *)aTableView {
return 1;
- }
- (NSInteger)tableView:(UITableView *)aTableView
- numberOfRowsInSection:(NSInteger)section {
return 10;
- }
- (UITableViewCell *)tableView:(UITableView *)tableView
- cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"CellIdentifier";
- UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
CellIdentifier];
- if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
- reuseIdentifier:CellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryNone;
- }
// Configure the cell.
- cell.textLabel.text = [NSString stringWithFormat:@"Row %d", indexPath.row];
return cell;
- }
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:
- (NSIndexPath *)indexPath {
detailViewController.detailItem = [NSString stringWithFormat:@"Row %d",
- indexPath.row];
}
- - (void)dealloc {
[detailViewController release];
- [super dealloc];
}
- @end
这里存在大量逻辑关系,幸好Xcode在分割视图模板中提供了。多数情况下你看到的是一个标准的表视图控制器。但是对于前面给出的与iPad相关的代码,需要指出几个你之前可能未遇到的问题。 首先,viewDidLoad方法包含一行设置该视图的contentSizeForViewInPopover属性的代码。很容易猜到此方法的用途:它设置此视图控制器在应该显示浮动窗口控制器时将使用的尺寸。这个矩形至少要达到320像素宽,除此之外可以根据你的喜好随意设置它的尺寸。本章稍后将探讨更多浮动窗口问题。 第二个值得提出的地方是shouldAutorotateToInterfaceOrientation:方法。通常在iPhone应用程序中会指定一种适合所需用途的特定方向。但是在iPad应用程序中,一般建议让用户自行选择显示方式。除非设计游戏(此时需要强制使用特定方向进行显示),iPad应用程序几乎总是希望此方法返回YES。 shouldAutorotateToInterfaceOrientation是Ipad开发时实现旋转效果的函数! 需要注意的最后一点是tableView:didSelectRowAtIndexPath:方法。在前面的章节中,当实现用于响应用户行选择的表视图控制器时,通常创建一个新视图控制器并将它推入到导航控制器栈中。但是在此应用程序中,我们希望显示的视图控制器已经存在,它是xib文件中包含的DetailViewController实例,所以我们在这里只需要告诉DetailViewController实例要显示的内容。 Xcode创建的最后一个类是DetailViewController,它负责用户所选的项的实际显示。我们已经浏览了nib文件DetailView.xib。DetailViewController.h文件类似于以下形式: #import <UIKit/UIKit.h>
- @interface DetailViewController : UIViewController <UIPopoverControllerDelegate,
UISplitViewControllerDelegate> {
- UIPopoverController *popoverController;
UIToolbar *toolbar;
- id detailItem;
UILabel *detailDescriptionLabel;
- }
@property (nonatomic, retain) IBOutlet UIToolbar *toolbar;
- @property (nonatomic, retain) id detailItem;
@property (nonatomic, retain) IBOutlet UILabel *detailDescriptionLabel;
- @end
除了前面所引用的detailItem属性(在RootViewController类中),DetailView- Controller有两个输出口用于连接到nib文件中的GUI组件(toolbar和detailDescription- Label),还有一个浮动窗口控制器实例变量(稍后将介绍)。 看一下DetailViewController.m,在其中可以找到以下内容(再次声明,这里进行了一定的删减): #import "DetailViewController.h" #import "RootViewController.h" @interface DetailViewController () @property (nonatomic, retain) UIPopoverController *popoverController; - (void)configureView; @end @implementation DetailViewController @synthesize toolbar, popoverController, detailItem, detailDescriptionLabel; - (void)setDetailItem:(id)newDetailItem { if (detailItem != newDetailItem) { [detailItem release]; detailItem = [newDetailItem retain]; // Update the view. [self configureView]; } if (self.popoverController != nil) { [self.popoverController dismissPopoverAnimated:YES]; } } - (void)configureView { // Update the user interface for the detail item. detailDescriptionLabel.text = [detailItem description]; } - (void)splitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController: (UIPopoverController*)pc { barButtonItem.title = @"Root List"; NSMutableArray *items = [[toolbar items] mutableCopy]; [items insertObject:barButtonItem atIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = pc; } - (void)splitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { NSMutableArray *items = [[toolbar items] mutableCopy]; [items removeObjectAtIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = nil; } - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return YES; } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; self.popoverController = nil; } - (void)dealloc { [popoverController release]; [toolbar release]; [detailItem release]; [detailDescriptionLabel release]; [super dealloc]; } @end 你应该熟悉这段代码中的大部分内容,但是这个类包含一些值得注意的新内容。首先是一个所谓的类扩展,它在靠近文件顶部的地方声明:
@interface DetailViewController () @property (nonatomic, retain) UIPopoverController *popoverController; - (void)configureView; @end [/pre]类扩展可创建来定义这样一些方法和属性:它们将在一个类中使用,但你不希望向头文件中的其他类公开它们。这里我们声明了popoverController属性和一个实用程序方法,popoverController属性将使用之前声明的实例变量,在需要更新显示时将调用该实用程序方法。我们还未告诉你popoverController属性的应有用途,但你很快就会看到! 再往下可以看到此方法: [pre]- (void)setDetailItem:(id)newDetailItem { if (detailItem != newDetailItem) { [detailItem release]; detailItem = [newDetailItem retain]; // Update the view. [self configureView]; } if (self.popoverController != nil) { [self.popoverController dismissPopoverAnimated:YES]; } } [/pre]你可能会对setDetailItem:方法感到惊奇。毕竟我们将detailItem定义为了一个属性,我们合成了它以创建getter和setter,那么为什么在代码中创建setter呢?在本例中,我们需要能够在用户调用setter时作出反应,以便可以更新显示,这是达到此目的的一种不错途径。该方法的第一部分看起来很简单,但在最后它采用了一个调用来解除当前的popoverController(如果存在)。虚构的popupController来自何处?答案就在下一个方法中: [pre]- (void)splitViewController:(UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController:(UIPopoverController*)pc { barButtonItem.title = @"Root List"; NSMutableArray *items = [[toolbar items] mutableCopy]; [items insertObject:barButtonItem atIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = pc; } [/pre]此方法是UISplitViewController的一个委托方法。当分割视图控制器不再固定显示分割视图左侧时(也就是当iPad旋转到纵向时)将调用它。分割视图控制器在委托中调用此方法并传递两个有趣的项:UIPopoverController和UIBarButtonItem。已预先配置UIPopover- Controller来包含分割视图左侧的内容,设置UIBarButtonItem来显示相同的浮动窗口。 这意味着,如果GUI包含UIToolbar(我们的GUI正是如此),我们只需要在工具栏上添加一个按钮,使用户点击该按钮即可调出导航栏。如果GUI未包含UIToolbar,我们还有传入的浮动窗口控制器,可以将它分配给GUI的其他某个元素,使该元素可以弹出该浮动窗口。我们还传入了包装好的UIViewController本身(在本例中为RootViewController),以防需要以完全不同的方式显示它的内容。 这就是浮动窗口控制器的来源。你可能已经预料到,下一个方法将解除该浮动窗口: [pre]- (void)splitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { NSMutableArray *items = [[toolbar items] mutableCopy]; [items removeObjectAtIndex:0]; [toolbar setItems:items animated:YES]; [items release]; self.popoverController = nil; } [/pre]当用户切换回横向模式时将调用该方法,这时分割视图控制器希望重新在固定位置绘制左侧视图,所以它告诉我们删除之前提供的UIBarButtonItem。 Xcode的Split View-based Application模板所提供的内容就大体介绍完了。 接下来,使用尺寸检查器将Web视图锚定到所有4边,使它可以在水平和垂直方向上进行调整(参见图10-7)。要连接创建的输出口,按住Control键并从主nib窗口中的File’s Owner图标拖到我们的新UIWebView,连接webView输出口。保存更改之后,你的工作就完成了!
图6 尺寸检查器,显示Select a President标签的设置
图7 尺寸检查器,显示Web视图的设置 现在可以构建并运行应用程序了,它将会显示每位总统的Wikipedia条目。在两个方向上旋转显示屏,将会看到分割视图控制器为你处理了所有事务,在一定程度上还借助了详细信息视图控制器来处理显示浮动窗口所需的工具栏项(就像在更改之前的原始应用程序中一样)。 本节最后要进行的更改实际上就是进行美化。当在横向模式下运行此应用程序时,左侧导航视图上方的标题为Root View Controller。切换到纵向模式并单击Presidents工具栏按钮,也会看到相同的标题。 要改进此效果,可打开MainWindow.xib,双击主nib窗口中的Split View Controller图标,然后双击视图左上部分显示的文本,将它更改为Presidents(参见图10-8)。保存nib文件,返回到Xcode,然后构建并运行,应该会看到更改生效了。 图8 MainWindow.xib内的分割视图控制器。请注意我们将左上角中的标题更改为了Presidents
|
|