Autorotation, Popover Controllers, Modal View Controller
本章将完成以下目标:
- 在iPad上,当设备颠倒,允许interface旋转。
- 在iPad上,将image picker显示在popover controller中
- 在iPad上,以模式窗口显示item detail
- 在iPhone上,当设备横屏,item detail视图禁用camera button
Autorotation
iOS中有两种不同的方向:device orientation, interface orientation。
device orientation有right-side up, upside down, rotated left, rotated right, on its face, or on its back。通过UIDevice的orientation属性来访问device orientation。
interface orientation是一个正在运行应用的属性:
interface orientation | description |
---|---|
UIInterfaceOrientationPortrait | home键在屏幕下方 |
UIInterfaceOrientationPortraitUpsideDown | home键在屏幕上方 |
UIInterfaceOrientationLandscapeLeft | home键在屏幕右方 |
UIInterfaceOrientationLandscapeRight | home键在屏幕左方 |
当device orientation发生改变,application会收到新的orientation,app可以决定是否将interfacce orientation匹配device orientation。
在General tab可以设置application在iPad/iPhone上支持的interface orientation.
通常在iPad上应用应该能够在四个方向上旋转,而在iPhone上不支持屏幕颠倒。
除了为application选择支持的interface orientation,霸占屏幕的view controller也可以声明其支持的interface orientation。只有rootViewController和application都支持的interface orientation才会起作用。
默认view controller在iPad上支持所有的方向,在iPhone上不支持屏幕颠倒。如果要改变这种默认情况,可以重写view controller的supportedInterfaceOrientations方法。
view controller supportedInterfaceOrientations的默认实现类似如下:
- (NSUInteger)supportedInterfaceOrientations{
// 如果设备是iPad,则支持所有orientation
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationMaskAll;
}else{
return UIInterfaceOrientationMaskAllButUpsideDown;
}
}
如果你的root view controller只支持水平方向,则可以重写为:
- (NSUInteger)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight;
}
通常霸占屏幕的是UINavigationController和UITabViewController,如果要更改orientation的默认行为,需要继承这些类并重写supportedInterfaceOrientations方法。
UITabViewController会询问tabs中每个view controller支持的interface orientation,然后返回他们的交集。
Rotation Notification
当设备方向改变是不是得做点什么?实现本文开始处的目标4,当在iPhone上横屏,禁用camera button,并隐藏image view。
要禁用camera button,选择声明一个属性来引用button。
当interface orientation成功改变,view controller会调用willAnimateRotationToInterfaceOrientation:duration:方法,形参是新的interface orientation。
// 当interface orientation成功改变,view controller会调用此方法。
// toInterfaceOrientation参数是新的interface orientation
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration{
[self prepareViewsForOrientation:toInterfaceOrientation];
}
- (void)prepareViewsForOrientation:(UIInterfaceOrientation)orientation{
// 如果设备是ipad直接返回
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
return;
}
// 如果interface orientation是水平的,则隐藏图片并禁用camera button
if (UIInterfaceOrientationIsLandscape(orientation)) {
self.imageView.hidden = YES;
self.cameraButton.enabled = NO;
} else {
self.imageView.hidden = NO;
self.cameraButton.enabled = YES;
}
}
当view要显示到屏幕上时,也去设置image view和camera button。
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
UIInterfaceOrientation io = [[UIApplication sharedApplication] statusBarOrientation];
[self prepareViewsForOrientation:io];
// .......
}
除了willAnimateRotationToInterfaceOrientation:duration:方法,还可以重写willRotateToInterfaceOrientation:duration:方法,此方法,view的改变没有动画。
当屏幕旋转完成,会调用didRotateFromInterfaceOrientation:方法,可以重写此方法,如果你想在旋转完成后做些什么。此方法的形参是旋转之前的interface orientation。
如果想查看view controller当前的interface orientation,可以查看interfaceOrientation属性。
UIPopoverController
UIPopoverController只在iPad上有效,UIPopoverController用来显示其他view controller's view,将其他view controller设置给其contentViewController属性。
本章将UIImagePickerController显示到UIPopoverController中。
声明BKDetailViewController,实现UIPopoverControllerDelegate protocol,并声明一个UIPopoverController属性。
@interface BKDetailViewController () <UINavigationBarDelegate, UIImagePickerControllerDelegate,UITextFieldDelegate, UIPopoverControllerDelegate>
@property (strong, nonatomic) UIPopoverController *imagePickerPopover;
在takePicture方法中(点击camera button执行此方法),如果设备是iPad,则创建popover controller。
- (IBAction)takePicture:(id)sender {
NSLog(@"Enter takePicture method");
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
// 判断设备是否支持相机拍摄
if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]){
imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
} else {
imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
}
// 设置代理
imagePicker.delegate = self;
// 通过popover controller显示image picker controller
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
// 创建popover controller
self.imagePickerPopover = [[UIPopoverController alloc] initWithContentViewController:imagePicker];
self.imagePickerPopover.delegate = self;
[self.imagePickerPopover presentPopoverFromBarButtonItem:sender permittedArrowDirections:UIPopoverArrowDirectionUp animated:YES];
} else {
// 如果不是ipad设备,直接显示
[self presentViewController:imagePicker animated:YES completion:nil];
}
NSLog(@"Exit takePicture method");
}
当点击屏幕其他地方,popover controller会被移除,此时会发送*popoverControllerDidDismissPopover:消息到其代理。
// 当点击屏幕其他地方时,popover controller被移除,此时会发送此消息到其代理
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController{
NSLog(@"User dismissed popover");
self.imagePickerPopover = nil;
}
当选择完图片后,我们要主动移除popover controller,可以执行其dismissPopoverAnimated:方法:
// image picker选中图片后,其代理收到此消息
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
// 获得图片
UIImage *image = info[UIImagePickerControllerOriginalImage];
// 保存图片到dictionary
[[BKImageStore sharedStore] setImage:image forKey:self.item.itemKey];
self.imageView.image = image;
// 如果image picker是在popover controller中显示的,则调用dismissPopoverAnimated隐藏popover controller
// 不过通过此方法移除popover controller,popover controller不会再发送popoverControllerDidDismissPopover消息给其代理
if (self.imagePickerPopover) {
[self.imagePickerPopover dismissPopoverAnimated:YES];
self.imagePickerPopover = nil;
} else {
// 移除image picker
[self dismissViewControllerAnimated:YES completion:nil];
}
}
需要注意的时,当直接调用dismissPopoverAnimated:方法移除popover controller,则popover controller不会再送popoverControllerDidDismissPopover:到其代理。
原文中这里提到,当第二次点击camera button时,应用会崩溃,不过在模拟器上不能重现,可以参考这里,在iOS7.1之前可以重现此问题,原因是第一次点击camera button,popover controller显示了,此时再次点击camera button,会再次创建popover controller,此时显示的popover controller没有指针引用其对象了,而他还要显示导致应用崩溃,为防止第二次点击camera button再次创建popover controller,添加以下代码在takePicture方法开始处。
if ([self.imagePickerPopover isPopoverVisible]) {
[self.imagePickerPopover dismissPopoverAnimated:YES];
self.imagePickerPopover = nil;
return;
}
More Modal View Controllers
本节将实现,新增item时,在modal view中显示item detail页面,当查看item时,还在原来的item detail页面显示。
BKDetailViewController头文件中声明新的初始化方法。
@interface BKDetailViewController : UIViewController
// 声明指定初始化文法
- (instancetype)initForNewItem:(BOOL)isNew;
@property (nonatomic,strong) BKItem *item;
@end
实现头文件中声明的初始化方法
// 实现头文件中声明的初始化方法
- (instancetype)initForNewItem:(BOOL)isNew{
// 调用父类的指定初始化方法
self = [super initWithNibName:nil bundle:nil];
if (self) {
if (isNew) {
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(save:)];
self.navigationItem.rightBarButtonItem = doneItem;
UIBarButtonItem *cancelItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel:)];
self.navigationItem.leftBarButtonItem = cancelItem;
}
}
return self;
}
前面有讲过,当子类继承父类,并且子类需要自己的指定初始化方法,此时在子类的指定初始化方法中要调用父类的指定初始化方法,并且子类要重写父类的指定初始化方法,并且在该重写的方法中调用自己的指定初始化方法。
所以此处也要重写父类的指定初始化方法,不过实现是直接抛出异常。
// 重写父类的指定初始化方法,抛出异常,提示使用initForNewItem:方法
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
@throw [NSException exceptionWithName:@"Wrong initializer" reason:@"Use initForNewItem:" userInfo:nil];
return nil;
}
在table view中选中一行时,进入detail view,修改其初始化方法。
// 选中一行
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
//BKDetailViewController *detailViewController = [[BKDetailViewController alloc] init];
// 由于BKDetailViewController重写了父类的指定初始化方法并抛出异常,所以不能直接调用init方法了。
// 调用其自己的指定初始化方法
BKDetailViewController *detailViewController = [[BKDetailViewController alloc] initForNewItem:NO];
NSArray *items = [[BKItemStore sharedStore] allItems];
BKItem *selectedItem = items[indexPath.row];
detailViewController.item = selectedItem;
[self.navigationController pushViewController:detailViewController animated:YES];
}
当新增一行时,显示detail view:
- (IBAction)addNewItem:(id)sender{
// 为要插入的行创建index path
//NSInteger lastRow = [self.tableView numberOfRowsInSection:0];
// 新建一条数据
BKItem *newItem = [[BKItemStore sharedStore] createItem];
// NSInteger lastRow = [[[BKItemStore sharedStore] allItems] indexOfObject:newItem];
// NSIndexPath *indexPath = [NSIndexPath indexPathForRow:lastRow inSection:0];
// // 插入一行
// [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationTop];
BKDetailViewController *detailViewController = [[BKDetailViewController alloc] initForNewItem:YES];
detailViewController.item = newItem;
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:detailViewController];
// 注意下面这个方法,presentViewController
[self presentViewController:navController animated:YES completion:nil];
}
清除view controller
要清除一个modal view controller,需要其presenter调用dismissViewControllerAnimated:completion:方法。每个UIViewController都有一个presentingViewController属性,指向其presenter。
在BKDetailViewController.m中,实现cancel/save方法,移除view controller:
- (void)save:(id)sender{
// 调用其presenter 移除detail view controller
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)cancel:(id)sender{
[[BKItemStore sharedStore] removeItem:self.item];
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
Modal view controller styles
在iPhone/iPod上,modal view controller占满整个屏幕,在iPad上有两种选择,通过设置modalPresentationStyle属性为UIModalPresentationFormSheet或UIModalPresentationPageSheet常量。
- (IBAction)addNewItem:(id)sender{
// .......
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:detailViewController];
// 设置modal view controller style
navController.modalPresentationStyle = UIModalPresentationFormSheet;
// 注意下面这个方法,presentViewController
[self presentViewController:navController animated:YES completion:nil];
}
Completion blocks
当modal view controller被移除,table view需要重新加载其数据。
[self.tableView reloadData];
dismissViewControllerAnimated:completion:方法的第二个参数是个block,当view controller被移除后会执行这个block。
在BKDetailViewController.h中声明一个块属性:
@property (nonatomic, copy) void (^dismissBlock)(void);
在创建BKDetailViewController时,指定块的值:
- (IBAction)addNewItem:(id)sender{
// 新建一条数据
BKItem *newItem = [[BKItemStore sharedStore] createItem];
BKDetailViewController *detailViewController = [[BKDetailViewController alloc] initForNewItem:YES];
detailViewController.item = newItem;
// 重新加载table view数据的block
detailViewController.dismissBlock = ^{
[self.tableView reloadData];
};
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:detailViewController];
// 设置modal view controller style
navController.modalPresentationStyle = UIModalPresentationFormSheet;
// 注意下面这个方法,presentViewController
[self presentViewController:navController animated:YES completion:nil];
}
当modal detail view被移除后,重新加载table view的数据。
- (void)save:(id)sender{
// 调用其present view controller 移除detail view controller
[self.presentingViewController dismissViewControllerAnimated:YES completion:self.dismissBlock];
}
- (void)cancel:(id)sender{
[[BKItemStore sharedStore] removeItem:self.item];
[self.presentingViewController dismissViewControllerAnimated:YES completion:self.dismissBlock];
}
Modal view controller transitions
除可以为modal view controller指定presentation style(modalPresentationStyle属性),还可以设置其显示动画(modalTransitionStyle属性)。
modalTransitionStyle | desc |
---|---|
UIModalTransitionStyleCoverVertical | slide up from the bottom |
UIModalTransitionStyleCrossDissolve | fades in |
UIModalTransitionStyleFlipHorizontal | flips in with a 3D effect |
UIModalTransitionStylePartialCurl | peeled up |
Thread-Safe Singletons
利用dispatch_once来保证单例的线程安全。
修改单例类BKImageStore的静态实例化方法:
// 静态方法,调用此该来获取单例实例
+ (instancetype)sharedStore{
static BKImageStore *sharedStore = nil;
// if(!sharedStore){
// sharedStore = [[self alloc] initPrivate];
// }
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedStore = [[self alloc] initPrivate];
});
return sharedStore;
}
Bitmasks(位掩码)
interface orientation constant | 十进制 | 二进制 |
---|---|---|
UIInterfaceOrientationMaskPortrait | 2 | 00000010 |
UIInterfaceOrientationMaskPortraitUpsideDown | 4 | 00000100 |
UIInterfaceOrientationMaskLandscapeRight | 8 | 00001000 |
UIInterfaceOrientationMaskLandscapeLeft | 16 | 00010000 |
按位或(|),按位与(&),按照二进制位来或和与。
前面提到supportedInterfaceOrientations方法,可以返回view controller支持的interface orientation,其返回值是int类型。
interface orientation constant | 十进制 | 二进制 |
---|---|---|
UIInterfaceOrientationMaskPortrait | 2 | 00000010 |
UIInterfaceOrientationMaskPortraitUpsideDown | 4 | 00000100 |
UIInterfaceOrientationMaskLandscapeRight | 8 | 00001000 |
UIInterfaceOrientationMaskLandscapeLeft | 16 | 00010000 |
按位或:
00000010 (2, UIInterfaceOrientationMaskPortrait)
| 00000100 (4, UIInterfaceOrientationMaskPortraitUpsideDown)
-------------
00000110 (6, both UIInterfaceOrientationMaskPortrait and UIInterfaceOrientationMaskPortraitUpsideDown)
按位与:
00000110 (6, both UIInterfaceOrientationMaskPortrait and UIInterfaceOrientationMaskPortraitUpsideDown)
& 00000010 (2, UIInterfaceOrientationMaskPortrait)
--------
00000010 (2, UIInterfaceOrientationMaskPortrait)
00000110 (6, both UIInterfaceOrientationMaskPortrait and UIInterfaceOrientationMaskPortraitUpsideDown)
& 00001000 (8, UIInterfaceOrientationMaskLandscapeRight)
--------
00000000 (0, NO)
非零值即为YES,所以可以用按位与来判断当前view controller是否支持某种interface orientation.
if ([viewController supportedInterfaceOrientations] & UIInterfaceOrientationMaskLandscapeLeft) {
// Allow interface orientation to change to landscape left
}
View Controller Relationships
View controllers之间有两种relationship:parent-child,presenting-presenter。
Parent-child relationships
当使用view controller container,就建立了parent-child关系,例如:UINavigationController, UITabBarController, 和UISplitViewController。view controller container都有一个viewControllers属性。
父子关系的view controllers,组成了family。子view controller可以通过parentViewController属性,找到其父view controller。
在family中访问ancestor的方法还有:navigationController, tabBarController, splitViewController,当一个view controller调用这些方法,会向上搜索其ancestor,直到找到适合类型的view controller,如果没有则返回nil。
Presenting-presenter relationships
当一个view controller被presented modally,就产生了这种关系。
上图中,下面那个view controller是被显示者,通过presentingViewController, presentedViewController属性分别指向两者。
Inter-family relationships
显示者和被被显示者不是同一个view controller family,下图显示了两个家族的关系:
需要注意的:
- 父子关系的属性(parentViewController, navigationController, tabBarController, splitViewController),不能跨越family,不会指向其他家族的view controller。
- 当一个view controller被presented modally,其presentingViewController属性指向presenting家族最老的view controller。
- 注意presentingViewController和presentedViewController属性,家族的每个view controller有这两个属性,并且都指向另一个家族的最老的view controller。
在iPad上,你可以重写这种总是指向最老view controller的行为。每个view controller都有一个definesPresentationContext属性,默认此属性值是NO,如果将此属性设置为YES,则会终止查找最老view controller,同时需要设置被显示view controller的modalPresentationStyle属性为UIModalPresentationCurrentContext。
上图中右下角的属性,应该是presentingViewController
如果presenter 的 definesPresentationContext设置为YES,而presentee的modalPresentationStyle设置为UIModalPresentationCurrentContext,则presentee被模式的显示(背景为灰色),但是只会覆盖到definesPresentationContext为YES的view controller的区域,不会像之前默认那样覆盖整个屏幕,因为之前默认是找presenter的最老view controller。
本文是对《iOS Programming The Big Nerd Ranch Guide 4th Edition》第十七章的总结。
网友评论