Autorotation, Popover Controllers, Modal View Controller

Autorotation, Popover Controllers, Modal View Controller

本章将完成以下目标:

  1. 在iPad上,当设备颠倒,允许interface旋转。
  2. 在iPad上,将image picker显示在popover controller中
  3. 在iPad上,以模式窗口显示item detail
  4. 在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。通过UIDeviceorientation属性来访问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;
}

通常霸占屏幕的是UINavigationControllerUITabViewController,如果要更改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》第十七章的总结。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容

  • /* UIViewController is a generic controller base class th...
    DanDanC阅读 1,771评论 0 2
  • 更好的阅读体验,请到个人博客阅读: iOS中的系统转场 请忽略标题,😂,本文记录的是对下图所示的Kind, Pre...
    CaryaLiu阅读 2,332评论 0 1
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,946评论 4 60
  • 今日新得 古典:《刘润,我是不经营人脉的》 1.那些能帮到你的人不是你的人脉就那些你能帮到的人才是你的人脉。 ——...
    Pheeb阅读 98评论 0 1
  • 人生,到底有什么意义,相信,一万种人有一万种答案。所处环境不同,对人生赋予的意义也会不同。单纯问抽象的生命有什么意...
    陌乐阅读 1,074评论 2 1