美文网首页
Autorotation, Popover Controller

Autorotation, Popover Controller

作者: ilaoke | 来源:发表于2015-08-16 10:47 被阅读417次

    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》第十七章的总结。

    相关文章

      网友评论

          本文标题:Autorotation, Popover Controller

          本文链接:https://www.haomeiwen.com/subject/ybvwqttx.html