第3章 iPad 开发

第3章 iPad开发

学习目标

0  1

  • 熟悉iPad开发和iPhone开发的异同

  • 掌握UIPopoverController和UISplitViewController的使用

对于某些应用而言,我们想在iPhone和iPad这两个平台上运行,但是直接将iPhone程序放到iPad上是不能运行的,这是因为它们的尺寸、应用场景不同,且某些控件在展现方式上也会有很大的差异,因此iPad有一些特有的API。本章将针对iPad开发进行详细的讲解。

3.1 iPhone和iPad开发的异同

iPad是苹果公司发布的一款平板电脑,定位介于智能手机iPhone与笔记本电脑之间,它与iPhone一样,都搭载iOS操作系统,但是两者在开发上却有着很大的不同,具体分为以下几种情况。

1.屏幕的尺寸和分辨率

在iOS开发中,iPhone设备的屏幕尺寸要比iPad设备小,接下来,通过一张表来列举iPhone和iPad的屏幕尺寸和分辨率,具体如表3-1所示。

表3-1 iPhone和iPad的屏幕尺寸和分辨率

型号

屏幕尺寸(英寸)

分辨率

iPhone 4S

3.5

960×640

320×480

iPhone 5/5C/5S

4

1136×640

320×568

iPhone 6/6S

4.7

1334×750

375×667

iPhone 6 Plus/6S Plus

5.5

1920×1080

414×736

iPad/iPad 2

9.7

768×1024

768×1024

iPad 3/iPad 4/iPad Air/iPad Air 2

9.7

1536×2048

768×1024

iPad Mini

7.9

768×1024

768×1024

iPad Mini 2/3

7.9

1536×2048

768×1024

iPad Pro

12.9

2732×2048

1366×1024

由表3-1可知,iPhone设备拥有多个尺寸,但iPad只有7.9英寸、9.7英寸和12.9英寸3种尺寸。

2.UI元素的排布与设计

由于iPad的屏幕尺寸要比iPhone大,可以容纳更多的UI元素,因此,针对同一个应用,其在iPhone和iPad上的界面布局有很大的区别,具体如图3-1所示。

图像说明文字

图3-1 iPhone和iPad的新浪微博

图3-1所示是iPhone和iPad上的新浪微博,由图可知,由于iPad和iPhone两种设备的屏幕尺寸不同,所以它们的界面和导航也有明显的差异。

3.键盘

iPhone和iPad设备都存在虚拟键盘,不同的是,iPad的虚拟键盘多了一个“退出键盘”的按钮,具体如图3-2所示。

QQ20130908-2.png

图3-2 iPad的虚拟键盘

4.支持的屏幕方向

通常情况下,设备包含两个方向,分别为横屏和竖屏,由于Home键所处方向的不同,针对横屏和竖屏,又可以细分为4种情况,分别是Portrait、Upside Down、Landscape Left、Landscape Right。其中,iPhone只支持3个方向,而iPad支持4个方向,具体如图3-3所示。

..\tu\0303.tif

图3-3 iPhone与iPad支持的屏幕方向

需要注意的是,iPhone不支持Upside Down方向。针对iPhone应用程序,最好只有一种方向,要么横屏,要么竖屏,而苹果官方建议,iPad应用最好同时支持横屏和竖屏两种方向。

5.公用API

iPhone与iPad都使用iOS操作系统,它们所使用的API基本上都是一样的,其中,某些API显示的效果会有稍微的差异,如UIActionSheet,具体如图3-4所示。

图像说明文字

图3-4 iPhone与iPad的按钮列表

图3-4所示是iPhone与iPad的按钮列表,其中左边为iPhone的按钮列表,右边为iPad的按钮列表,通过比较可知,iPad上的按钮列表只有一个“确定”按钮,相对而言比较简单。

6.特有API

除了公共的API之外,iOS提供了iPad专用的API,如UIPopoverController与UISplitViewController,其中UIPopoverController用于呈现“漂浮”类型的视图,而UISplitViewController用于将屏幕分栏,针对这两个类,后面会着重介绍。

总而言之,尽管iPad与iPhone存在着一些差异,但是它们的开发流程都是一样的,在iPhone中用到的知识几乎都适用于iPad。

3.2 UIPopoverController

前面已经提到过,iOS提供了iPad专用的API,UIPopoverController类就是其中一个,它主要用于呈现“漂浮”类型的视图。接下来,本节将针对UIPopoverController类的使用进行详细讲解。

3.2.1 UIPopoverController简介

UIPopoverController类表示弹出框控制器,它直接继承于NSObject基类,是iPad的特有类,用于控制Popover视图。Popover视图是一种临时视图,它以漂浮的形式出现在视图表面,触摸Popover视图以外的区域,则可以关闭视图。接下来通过一张图来描述,具体如 图3-5所示。

..\tu\0305.tif

图3-5 QQ空间的页面

图3-5所示是QQ空间写“说说”的页面,由图可知,Popover视图有一个小箭头指向打开它的视图或按钮,而且不会占用全屏,这样不仅可以在不离开当前屏幕的情况下向用户显示新信息,而且在使用完毕后,只要触摸该视图以外的区域就可自动关闭它。

要想实现Popover视图的效果,既可以使用故事板连线实现,也可以通过代码实现,为此,UIPopoverController类提供了一些常用的属性,接下来,通过一张表来列举UIPopoverController类常见的属性,如表3-2所示。

表3-2 UIPopoverController类的常见属性

属性声明

功能描述

@property (nonatomic, assign) id <UIPopoverControllerDelegate> delegate;

设置代理对象

@property (nonatomic, readonly, getter=isPopoverVisible) BOOL popoverVisible;

判断Popover视图是否可见

@property (nonatomic) CGSize popoverContentSize;

设置Popover视图的尺寸

@property (nonatomic, readonly) UIPopoverArrowDirection popoverArrowDirection;

设置Popover视图箭头的方向

表3-2列举了UIPopoverController类一些常见的属性,由表可知,后两个属性主要用于设置Popover视图的外观,其中,popoverArrowDirection是一个UIPopoverArrowDirection类型的,该类型是枚举类型,它的定义格式如下:

typedef NS_OPTIONS(NSUInteger, UIPopoverArrowDirection) {
 UIPopoverArrowDirectionUp = 1UL << 0,
 UIPopoverArrowDirectionDown = 1UL << 1,
 UIPopoverArrowDirectionLeft = 1UL << 2,
 UIPopoverArrowDirectionRight = 1UL << 3,
 UIPopoverArrowDirectionAny = UIPopoverArrowDirectionUp | 
   UIPopoverArrowDirectionDown | UIPopoverArrowDirectionLeft | 
   UIPopoverArrowDirectionRight,
 UIPopoverArrowDirectionUnknown = NSUIntegerMax
};

在上述代码中,它共包含6个值,其表示的含义如下。

  • UIPopoverArrowDirectionUp:箭头向上。
  • UIPopoverArrowDirectionDown:箭头向下。
  • UIPopoverArrowDirectionLeft:箭头向左。
  • UIPopoverArrowDirectionRight:箭头向右。
  • UIPopoverArrowDirectionAny:任意方向,包含以上4种情况。
  • UIPopoverArrowDirectionUnknown:不确定方向。

3.2.2 UIPopoverController的使用

前面我们已经认识了UIPopoverController,要想使用UIPopoverController弹出Popover视图,大致需要经历以下3个步骤:

(1)设置内容控制器;

(2)设置内容的尺寸;

(3)设置内容视图的位置。

针对以上3个步骤,下面详细地剖析UIPopoverController类使用的技巧,具体如下。

1.设置内容控制器

由于UIPopoverController继承自NSObject类,它不具备可视化的能力,因此,UIPopoverController要显示的内容需要由另一个继承自UIViewController类的控制器来提供,这个控制器称为“内容控制器”,因此,UIPopoverController类提供了设置内容控制器的方法,它们的定义格式如下:

- (instancetype)initWithContentViewController:
   (UIViewController *)viewController;
- (void)setContentViewController:(UIViewController *)viewController 
   animated:(BOOL)animated;

在上述定义中,这两个方法分别是一个构造方法和一个对象方法,它们都需要传入一个viewController参数,用于指定UIPopoverController要显示内容的控制器。需要注意的是,UIPopoverController不可以使用init方法进行初始化,否则会报错。

另外,UIPopoverController类提供了一个属性,也可以设置内容控制器,它的定义格式如下:

@property (nonatomic, retain) UIViewController *contentViewController;

2.设置内容的尺寸

初始化完成后,需要设置内容的尺寸,也就是内容占据屏幕空间大小。通常情况下,若没有设定内容的尺寸,默认是320×480。如果要设定内容的尺寸,UIPopoverController类提供了一个属性,它的定义格式如下:

@property (nonatomic) CGSize popoverContentSize;

另外,UIPopoverController类还提供了一个方法,也可以设定内容的尺寸,它的定义格式如下:

- (void)setPopoverContentSize:(CGSize)size animated:(BOOL)animated;

在上述定义中,该方法需要传入两个参数,其中,size参数是CGSize类型的,用于指定内容的尺寸大小。

3.设置内容视图的位置

除了指定内容视图的大小之外,还需要指定其出现的位置,因此UIPopoverController类提供了两个方法,分别表示两种不同的情况,具体如下。

(1)指定一个按钮作为锚点来呈现Popover视图的方法,它的定义格式如下:

- (void)presentPopoverFromBarButtonItem:(UIBarButtonItem *)item 
permittedArrowDirections:(UIPopoverArrowDirection)arrowDirections 
animated:(BOOL)animated;

从上述代码可以看出,该方法共包含3个参数,这些参数的含义如下:

  • item:围绕着哪个UIBarButtonItem显示;
  • arrowDirections:箭头的方向;
  • animated:是否通过动画显示。

(2)指定一个矩形区域的位置作为锚点来呈现Popover视图的方法,它的定义格式如下:

- (void)presentPopoverFromRect:(CGRect)rect inView:(UIView *)view 
permittedArrowDirections:(UIPopoverArrowDirection)arrowDirections 
animated:(BOOL)animated;

从上述代码可以看出,该方法共包含4个参数,其含义如下。

  • rect:指定箭头所指区域的矩形框范围,即位置和尺寸。
  • view:rect参数是以view的左上角为坐标原点的。
  • arrowDirections:箭头的方向。
  • animated:是否通过动画显示。

为了大家更好地理解,接下来通过一张图来剖析rect与view的关系,具体如图3-6所示。

..\tu\0306.tif

图3-6 rect与view的关系

图3-6展示了参数rect与view的关系,由图可知,view相当于参照物,它决定了rect矩形框的坐标原点,参照这个坐标指定rect矩形框的位置。以button为例,如果希望箭头指向button,那么view参数值为button,rect矩形框的大小应该与button一致,且位置与button自身的左上角重合,所以rect参数值为button.bounds。如果view参数的值为button.superview,依然希望箭头指向button,那么rect矩形框的大小还是与button一致,并以其父控件的左上角为原点坐标,因此rect参数的值为button.frame。

图像说明文字多学一招:iOS 8新特性UIPopoverPresentationController

UIPopoverController原来只是在iPad上使用,现在iPhone上也将适用。准确地说,iOS 8以后将改用一个新类UIPopoverPresentationController来替代UIPopoverController类,它是UIPresentationController的子类。

比起原来的类,新类最大的优势是自适应,在Compact的宽度条件下,UIPopoverPresentation Controller的呈现将会直接变成modal显示出来。这样就不需要再判断是iPhone还是iPad设备了。具体如下:

 1 contentController.modalPresentationStyle = UIModalPresentationPopover;
 2 UIPopoverPresentationController *popPC = 
  contentController.popoverPresentationController;
 3 popPC.permittedArrowDirections = UIPopoverArrowDirectionAny;
 4 popPC.delegate = self;
 5 [self presentationController:contentController animated:YES completion:nil];

将UIViewController的modalPresentationStyle设置成UIModalPresentationPopover,这个值实现popover效果,并且各个平台自动适应。第2行代码中,通过popoverPresentationController属性来获取它的popoverPresentationController,而不是创建一个新的类。然后设置它的界面属性,最后调用presentViewController方法来显示这个controller。这样就可以在iPad和iPhone显示时自动适应popover效果了。

iPhone上的自适应是在delegate中实现的,具体如下。

(1)指定UIModelPresentationFullScreen样式来显示controller:

-(UIModalPresentationStyle)
adaptivePresentationStyleForPresentationController:
(UIPresentationController *)controller

(2)presentedViewController使用UINavigationController包装起来,使得可以在选中某项之后,通过navigationController提供的一些方法来展示内容,或者后退:

- (UIViewController *)presentationController:
(UIPresentationController*)
controllerviewControllerForAdaptivePresentationStyle:
(UIModalPresentationStyle)style

除了自适应之外,新方式的另一个优点是非常容易自定义。可以通过继承UIPopover PresentationController来实现想要的呈现方式。其实更准确地说,应该继承的是UIPresentation Controller,主要通过实现-presentationTransitionWillBegin和-presentationTransitionDidEnd:来自定义展示。以前我们想要实现只占半个屏幕,后面view仍然可见的modal,或者是将从下到上的动画改为百叶窗或者渐隐渐现,都是比较麻烦的事情。而在UIPresentationController的帮助下,一切变得十分自然和简单。在自己的UIPresentationController子类中即可实现,具体如下:

 1 - (void)presentationTransitionWillBegin{ 
 2  id<UIViewControllerTransitionCoordinator>
 3  coordinator=self.presentingViewController.transitionCoordinator;
 4  [coordinator 
 5   animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context){
 6   // 执行动画
 7   } completion:nil];
 8 }
 9 - (void)presentationTransitionDidEnd:(BOOL)completed{
 10  // 清除
 11 }

具体的用法和 iOS 7里的自定义转场很类似,设定需要进行呈现操作的 ViewController 的transition delegate,在UIViewControllerTransitioningDelegate的-presentationControllerFor PresentedViewController:sourceViewController: 方法中使用-initWithPresentedViewController: presentingViewController: 生成对应的UIPresentationController子类对象返回给 SDK。

3.2.3 实战演练——弹出Popover视图

为了大家更好地理解,接下来,通过一个Popover视图的案例,讲解UIPopover Controller的使用,具体步骤如下。

1.创建工程,设计界面

(1)新建一个Single View Application应用,名称为01_UIPopoverController,需要注意的是将Devices设置为iPad。

(2)将提前准备好的图片资源放到Images.xcassets文件中,具体如图3-7所示。

图片 1

图3-7 图片资源添加完成

(3)进入Main.storyboard,默认使用Autolayout,打开文件检查器面板,取消勾选“Use Auto Layout”复选框,这时弹出一个提示框,如图3-8所示。

图片 3

图3-8 取消勾选“Use Auto Layout”复选框

在下拉列表选择iPad,单击“Disable Size Classes”按钮,这时“Use Auto Layout”和“Use Size Classes”两个复选框的勾选被取消了。

(4)选中storyboard的View Controller,选择菜单栏中的“Editor”→“Embed In”→“Navigation Controller”,这时storyboard自动添加了一个Navigation Controller,View Controller作为它的根视图控制器,同时箭头指向了新添加的Navigation Controller,使其作为程序界面的入口。

(5)向View Controller添加两个UIButton、一个Bar Button Item,其中,Bar Button Item位于导航栏的左侧,一个UIButton位于导航栏的中间,另一个位于View内部,双击使其处于可编辑状态,分别输入“菜单”“主页”“选择颜色”,设计好的界面如图3-9所示。

图片 3

图3-9 设计好的界面

2.创建控件对象的关联

单击Xcode 6.1界面右上角的图片 231图标,进入控件与代码的关联界面。依次选中3个Button,添加3个单击事件,分别命名为menuClick:、titleClick:、selectColor:,用于表示单击“菜单”按钮、“主页”按钮、“选择颜色”按钮后执行的行为,添加完成的界面如图3-10所示。

图片 232

图3-10 关联完成的界面

3.项目添加分组

这个项目主要包含3个部分,为了使项目的结构更加清晰,可以通过添加多个分组来实现,大致分为以下步骤。

(1)选中窗口左侧的“01_UIPopoverController”文件夹,右击鼠标弹出一个下拉菜单,选择“Show in Finder”,之后Finder打开了项目窗口,如图3-11所示。

图片 2

图3-11 Finder中的项目窗口

(2)双击图3-11中的第1个文件夹,在该目录下添加文件夹,命名为Classes。再次双击打开Classes文件夹,在此目录下依次添加4个文件夹,分别命名为Menu、Title、Color、Other,并将与Classes处于同级目录下的全部代码文件拖曳到Other目录下,完成后的文件结构如图3-12所示。

图片 4

图3-12 Finder中的文件结构

(3)重新回到Xcode,由于Finder中代码文件位置发生了变化,导致项目浏览窗口的代码文件失去链接,故文件名称变为红色。将这5个文件全部删除,再次找到Finder中Classes文件夹,将其拖曳到项目文件夹内,弹出一个窗口,如图3-13所示。

图片 6

图3-13 Finder中的文件结构

需要注意的是,图中虚线框框选的1个单选按钮和2个复选框都必须是选中状态。

(4)单击图3-13的“Finish”按钮,项目文件夹成功添加分组Classes,单击Classes前面的图片 10图标,查看其内部的层次结构,如图3-14所示。

图片 7

图3-14 项目文件组织结构

4.弹出菜单Popover

单击“菜单”按钮,弹出一个Popover视图,它展示了3个表格行,且每行都包含一个图标和一个标题。按照一定的步骤,可弹出特定样式的Popover视图,具体如下。

(1)由于单元格的布局一致,将单元格抽取成一个模型。选中Menu分组,创建一个类,命名为MenuItem,继承自NSObject基类。

(2)进入MenuItem.h文件,定义两个属性,分别表示图标和标题,提供一个创建单元格的初始化方法。进入MenuItem.m文件,对模型的属性进行初始化,代码如例3-1和例3-2所示。

【例3-1】MenuItem.h

 1 #import <Foundation/Foundation.h>
 2 @interface MenuItem : NSObject
 3 @property (nonatomic, copy) NSString *icon;  // 图标
 4 @property (nonatomic, copy) NSString *text;  // 标题
 5 - (instancetype)initWithIcon:(NSString *)icon text:(NSString *)text;
 6 @end

【例3-2】MenuItem.m

 1 #import "MenuItem.h"
 2 @implementation MenuItem
 3 - (instancetype)initWithIcon:(NSString *)icon text:(NSString *)text
 4 {
 5   if (self = [super init]) {
 6    self.icon = icon;
 7    self.text = text;
 8   }
 9   return self;
 10 }
 11 @end

(3)选中Menu分组,同样创建一个类,继承自UITableViewController类,命名为MenuViewController,作为一个内容控制器。在MenuViewController.m文件中,定义一个数组,用于保存多个单元格模型,采用懒加载的方式初始化数组,代码如下:

 1 #import "MenuViewController.h"
 2 #import "MenuItem.h"
 3 @interface MenuViewController ()
 4 // 保存模型数据
 5 @property (nonatomic, strong) NSArray *items;
 6 @end
 7 @implementation MenuViewController
 8 #pragma mark - 懒加载
 9 - (NSArray *)items
 10 {
 11  if (_items == nil) {
 12    MenuItem *item1 = [[MenuItem alloc]initWithIcon:@"movie" text:@"电影"];
 13    MenuItem *item2 = [[MenuItem alloc]initWithIcon:@"music" text:@"音乐"];
 14    MenuItem *item3 = [[MenuItem alloc] initWithIcon:@"video" text:@"录像"];
 15    _items = @[item1, item2, item3];
 16  }
 17  return _items;
 18 }
 19 @end

(4)实现表视图的数据源方法,取出数组中的模型对象,将其展示到表视图中,代码如下:

 1 #pragma mark - 数据源
 2 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:
 3 (NSInteger)section
 4 {
 5   return self.items.count;
 6 }
 7 - (UITableViewCell *)tableView:(UITableView *)tableView 
 8 cellForRowAtIndexPath:(NSIndexPath *)indexPath
 9 {
 10  NSString *ID = @"MenuCell";
 11  UITableViewCell *cell = [tableView 
 12  dequeueReusableCellWithIdentifier:ID];
 13  if (!cell) {
 14    cell = [[UITableViewCell alloc] initWithStyle:
 15    UITableViewCellStyleDefault reuseIdentifier:ID];
 16  }
 17  // 获取模型
 18  MenuItem *item = self.items[indexPath.row];
 19  cell.textLabel.text = item.text;
 20  cell.imageView.image = [UIImage imageNamed:item.icon];
 21  return cell;
 22 }

(5)进入ViewController.m文件,定义一个属性,表示菜单的Popover视图,并采用懒加载的方式初始化,保证Popover视图对象只会被创建一次,代码如下:

 1 #import "ViewController.h"
 2 #import "MenuViewController.h"
 3 @interface ViewController ()
 4 // 菜单的Popover
 5 @property (nonatomic, strong) UIPopoverController *menuPopover;
 6 @end
 7 #pragma mark - 懒加载menuPopover
 8 - (UIPopoverController *)menuPopover
 9 {
 10  if (_menuPopover == nil) {
 11    // 1.创建内容控制器
 12    MenuViewController *menuVC = [[MenuViewController alloc] init];    
 13    // 2.创建UIPopoverController,并且设置内容控制器
 14    _menuPopover = [[UIPopoverController alloc]
 15    initWithContentViewController:menuVC];
 16  }
 17  return _menuPopover;
 18 }
 19 @end

(6)在menuClick:方法中,展示出菜单的Popover视图,且该视图的箭头方向朝上,代码如下:

 1 /**
 2  * 菜单的单击
 3  */
 4 - (IBAction)menuClick:(UIBarButtonItem *)sender {  
 5   // 显示在什么位置
 6   [self.menuPopover presentPopoverFromBarButtonItem:sender 
 7   permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];  
 8 }

(7)显而易见,菜单的Popover视图的尺寸还未设定,它应该由内部控制器的内容所决定。在MenuViewController.m的viewDidLoad方法中,设定Popover视图的尺寸,代码如下:

 1 - (void)viewDidLoad {
 2   [super viewDidLoad];
 3   // 设置之后在UIPopoverController中展示的大小
 4   self.preferredContentSize = CGSizeMake(120, 44 * self.items.count);
 5 }

值得一提的是,内容控制器可以自行设置自己在Popover视图中显示的尺寸,通过UIViewController类提供的一个preferredContent Size属性实现,针对任意一个容器布局子控制器。

(8)运行程序,单击“菜单”按钮,弹出一个带有箭头的窗口,它里面展示了3个带有图标和标题的单元格,如图3-15所示。

图片 8

图3-15 弹出菜单的Popover视图

5.弹出标题Popover

单击“主页”标题按钮,弹出一个默认尺寸的Popover视图。其中,内容控制器顶部有一个导航条,导航条包含3个部分,分别为左侧的按钮、中间的标题、右侧的按钮。另外,内容控制器的视图中间有一个“跳转”按钮,单击该按钮,弹出另一个控制器的视图。按照一定的步骤,弹出特定样式的Popover视图,步骤如下。

(1)选中Title分组,创建一个类,继承自UIViewController基类,命名为OneView Controller,作为内容控制器。同样的方式创建一个类,命名为TwoViewController,表示被弹出来的另一个控制器。

(2)在OneViewController.m文件中,设定导航条的内容,并完成跳转的功能,代码如例3-3所示。

【例3-3】OneViewController.m

 1 #import "OneViewController.h"
 2 #import "TwoViewController.h"
 3 @interface OneViewController ()
 4 @end
 5 @implementation OneViewController
 6 - (void)viewDidLoad {
 7   [super viewDidLoad];
 8   // 1.设置背景颜色
 9   self.view.backgroundColor = [UIColor yellowColor];  
 10  // 2.设置左上角和右上角的图标
 11  self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] 
 12  initWithBarButtonSystemItem:UIBarButtonSystemItemCamera 
 13  target:nil action:nil];
 14  self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] 
 15  initWithBarButtonSystemItem:UIBarButtonSystemItemEdit 
 16  target:nil action:nil];  
 17  // 3.设置第二个控制器的返回按钮
 18  self.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] 
 19  initWithTitle:@"返回" style:UIBarButtonItemStyleDone 
 20  target:nil action:nil];  
 21  // 4.设置标题
 22  self.title = @"第一个控制器";
 23  // 5.添加一个按钮
 24  UIButton *btn = [[UIButton alloc] init];
 25  btn.frame = CGRectMake(100, 100, 120, 50);
 26  [btn setTitle:@"跳转" forState:UIControlStateNormal];
 27  [btn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
 28  [btn addTarget:self action:@selector(btnClick) 
 29  forControlEvents:UIControlEventTouchUpInside];
 30  [self.view addSubview:btn];
 31 }
 32 /**
 33 * 单击“跳转”按钮
 34 */
 35 - (void)btnClick
 36 {
 37  TwoViewController *twoVC = [[TwoViewController alloc] init];
 38  [self.navigationController pushViewController:twoVC animated:YES];
 39 }
 40 @end

(3)在TwoViewController.m文件中,设置视图的背景颜色及导航条的标题,代码如例3-4所示。

【例3-4】TwoViewController.m

 1 #import "TwoViewController.h"
 2 @interface TwoViewController ()
 3 @end
 4 @implementation TwoViewController
 5 - (void)viewDidLoad {
 6   [super viewDidLoad];
 7   // 1.设置背景颜色
 8   self.view.backgroundColor = [UIColor blueColor];
 9   // 2.设置标题
 10  self.title = @"第二个控制器";
 11 }
 12 @end

(4)进入ViewController.m文件,导入OneViewController.h头文件,定义一个属性,表示标题的Popover视图,代码如下:

 1 // 标题的Popover
 2 @property (nonatomic, strong) UIPopoverController *titlePopover;

(5)同样采用懒加载的方式初始化titlePopover,保证Popover视图对象只会被创建一次,代码如下:

 1 #pragma mark - 懒加载titlePopover
 2 - (UIPopoverController *)titlePopover
 3 {
 4   if (_titlePopover == nil) {
 5    // 1.创建内容控制器
 6    OneViewController *oneVC = [[OneViewController alloc] init];
 7    UINavigationController *navigationVC = 
 8    [[UINavigationController alloc] initWithRootViewController:oneVC];
 9    // 2.创建UIPopoverController,并设置内容控制器
 10    _titlePopover = [[UIPopoverController alloc] 
 11    initWithContentViewController:navigationVC];
 12  }
 13  return _titlePopover;
 14 }

(6)在titleClick:方法中,展示出标题的Popover视图,且该视图的箭头方向朝上,代码如下:

 1 /**
 2  * 标题的单击
 3  */
 4 - (IBAction)titleClick:(UIButton *)sender {
 5   // 展示出标题的Popover,Rect:要指向的矩形框,inView:决定坐标原点
 6   [self.titlePopover presentPopoverFromRect:sender.bounds 
 7   inView:sender permittedArrowDirections:UIPopoverArrowDirectionAny 
 8   animated:YES];
 9 }

(7)运行程序,单击“主页”按钮,弹出一个带有箭头的窗口,它里面展示了一个带有导航条的控制器的视图,单击“跳转”按钮,弹出另一个控制器的视图,如图3-16所示。

6.弹出颜色Popover

单击“选择颜色”按钮,弹出一个固定尺寸的Popover视图。其中,该视图内部仅有一张图片,单击该图片的任意位置,吸取该位置的颜色,Popover视图消失,“选择颜色”按钮的背景颜色更改为吸取的颜色,具体步骤如下。

(1)选中Color分组,新建一个类,继承自UIViewController,命名为ColorViewController,用于表示一个内容控制器。需要注意的是,需勾选“Also create XIB file”复选框,采用xib的方式布局界面。

图像说明文字

图3-16 弹出标题的Popover视图

(2)在ColorViewController.xib中取消使用Autolayout,xib的View的尺寸是固定的,它的尺寸应该与图片保持一致。因此,打开其对应的属性检查器,更改Size为Freeform,之后打开大小检查器,设置Width和Height分别为225、250。

(3)从对象库中拖曳一个Image View到View上,与View可完全重合,设置Image View的Image为colorWheel,完成后的界面如图3-17所示。

图片 4

图3-17 xib视图

值得一提的是,xib所示框的周围会出现 浅灰色的多个小圆点,这表示View的尺寸是可变的。

(4)在ColorViewController.m文件中定义一个属性,表示图像视图,并设定之后在UIPopover Controller对象中展示的尺寸,代码如下:

 1 #import "ColorViewController.h"
 2 @interface ColorViewController ()
 3 // 图像视图
 4 @property (weak, nonatomic) IBOutlet UIImageView *colorView;
 5 @end
 6 @implementation ColorViewController
 7 - (void)viewDidLoad {
 8   [super viewDidLoad];
 9   // 设置之后在UIPopoverController中展示的大小
 10  self.preferredContentSize = self.colorView.image.size;
 11 }
 12 @end

(5)根据单击的位置,吸取其颜色,传递给另一个控制器,针对这种情况,通过代理的方式可以实现。在ColorViewController.h文件中,定义一个代理协议和属性,代码如例3-5所示。

【例3-5】ColorViewController.h

 1 #import <UIKit/UIKit.h>
 2 @class ColorViewController;
 3 // 声明一个代理协议
 4 @protocol ColorViewControllerDelegate <NSObject>
 5 @optional
 6 - (void)colorViewController:(ColorViewController *)colorVC 
 7 withColor:(UIColor *)selectedColor;
 8 @end
 9 @interface ColorViewController : UIViewController
 10 //代理
 11 @property (nonatomic, weak)id<ColorViewControllerDelegate> delegate; 
 12 @end

(6)由于根据图片的触摸点获取颜色的功能过深,无法诠释得更好,因此该功能的实现通过一个分类来完成。选中Other分组,添加一个新的分组Category,在此分组中添加UIImage+GetColor分类。该分类的声明文件中提供了一个方法,定义格式如下:

- (UIColor *)pixelColorAtLocation:(CGPoint)point;

在上述代码中,只要传入一个点,就能够获取到该点所处的颜色。

(7)在ColorViewController.m文件中,导入UIImage+GetColor.h,通过touchesBegan: withEvent:方法获取触摸点,通过该点获取其所处的颜色,并通知代理,代码如下:

 1 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
 2 {
 3   // 1.获取触摸点
 4   CGPoint p = [[touches anyObject] locationInView:self.view];  
 5   // 2.获取颜色
 6   UIColor *selectedColor = [self.colorView.image pixelColorAtLocation:p];
 7   // 3.通知代理
 8   if ([self.delegate respondsToSelector:
 9   @selector(colorViewController:withColor:)]) {
 10    [self.delegate colorViewController:self withColor:selectedColor];
 11  }
 12 }

(8)在ViewController.m文件中,遵守ColorViewControllerDelegate协议,定义两个属性,分别表示颜色的Popover视图和选择颜色按钮,代码如下:

 1 // 颜色的Popover
 2 @property (nonatomic, strong) UIPopoverController *colorPopover;
 3 // 选择颜色按钮
 4 @property (weak, nonatomic) IBOutlet UIButton *selectColorBtn;

(9)同样采用懒加载的方式初始化colorPopover,保证Popover视图对象只会被创建一次,代码如下:

 1 #pragma mark - 懒加载colorPopover
 2 - (UIPopoverController *)colorPopover
 3 {
 4   if (_colorPopover == nil) {
 5    // 1.创建内容控制器
 6    ColorViewController *colorVC = [[ColorViewController alloc] init];    
 7    // 2.设置代理
 8    colorVC.delegate = self;    
 9    // 3.创建UIPopoverController,并设置内容控制器
 10    _colorPopover = [[UIPopoverController alloc] 
 11    initWithContentViewController:colorVC];
 12  }
 13  return _colorPopover;
 14 }

(10)在selectColor:方法中,展示出颜色的Popover视图,且该视图箭头的方向是任意的,代码如下:

 1 /**
 2  * 选择颜色
 3  */
 4 - (IBAction)selectColor:(UIButton *)sender {
 5   // 展示出颜色的Popover,Rect:要指向的矩形框,inView:决定坐标原点
 6   [self.colorPopover presentPopoverFromRect:sender.frame 
 7   inView:sender.superview 
 8   permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
 9 }

(11)实现colorViewController: withColor:方法,更换按钮的颜色及让Popover视图消失,代码如下:

 1 #pragma mark - ColorViewControllerDelegate
 2 - (void)colorViewController:(ColorViewController *)colorVC 
 3 withColor:(UIColor *)selectedColor
 4 {
 5   // 1.设置颜色
 6   self.selectColorBtn.backgroundColor = selectedColor;  
 7   // 2.让Popover视图消失
 8   [self.colorPopover dismissPopoverAnimated:NO];
 9 }

需要注意的是,单击Popover视图以外的区域,Popover视图就会消失,调用dismiss PopoverAnimated:方法能够主动关闭Popover视图。

(12)运行程序,单击“选择颜色”按钮,右侧弹出一个带有箭头的窗口,里面展示了一个颜色轮,单击颜色轮中的任意位置,按钮的背景颜色更改为该位置的颜色,如图3-18所示。

图像说明文字

图3-18 弹出颜色的Popover视图

3.3 UISplitViewController

除了UIPopoverController类之外,UISplitViewController类也是iPad专用的视图控制器类,它是构建导航模式应用的基础。接下来,本节将针对UISplitViewController类的使用进行详细讲解。

3.3.1 UISplitViewController简介

由于iPad的屏幕要比iPhone大很多,故iPhone的导航模式是不适用于iPad的,为此,iOS提供了UISplitViewController类,以呈现屏幕分栏的效果。

UISplitViewController属于iPad特有的界面控件,适合用于主从界面的情况(“Master View”→“Detail View”),Detail View跟随Master View进行更新,屏幕左边是主菜单,单击每个菜单屏幕右边会随着刷新,屏幕右边的界面内容又可以通过UINavigationController进行组织,以便用户进入Detail View进行更多的操作。接下来,通过一张图来描述UISplitViewController的使用场景,具体如图3-19所示。

图片 2

图3-19 iPad的Settings应用

图3-19所示是iPad横屏状态下的Settings应用,由图可知,屏幕被分割为左右两个视图,左右视图都带有导航栏。其中,左侧Master View是一个表视图,每单击一行,右侧的Detail View会随着刷新。需要说明的是,Master View的导航列表占有320点的固定大小,竖屏的情况下,它会隐藏起来,具体如图3-20和图3-21所示。

..\tu\0320.tif

图3-20 横屏SplitView

..\tu\0321.tif

图3-21 竖屏SplitView

由图3-20和图3-21可知,竖屏的Master View是隐藏的,我们可以采用向右拖曳屏幕,或者单击工具按钮的方式来激活Master View。

3.3.2 UISplitViewController的使用

前面我们已经认识了UISplitViewController,要想使用UISplitViewController实现分栏的效果,既可以采用拖控件的方式,也可以通过代码的方式。针对这两种情况,下面进行深入剖析。

1.从对象库拖曳Split View Controller

(1)新建一个Single View Application的iPad应用,取消勾选“Use Auto Layout”复选框。进入Main.storyboard,删除默认带有的View Controller,从对象库拖曳一个Split View Controller到程序界面,如图3-22所示。

图片 11

图3-22 拖曳Split View Controller到程序界面

从图3-22中看出,Split View Controller默认带有两个子控制器,它们的宽度是不一样的。其中,上面的子控制器是包装了Navigation Controller的Table View Controller,作为左侧Master View对应的控制器,下面的子控制器就是Detail View对应的控制器。

(2)如果Split View Controller默认的子控制器不符合需求,可以将它们删除,从对象库拖曳合适的控件到程序界面。右击Split View Controller,弹出一个黑色列表框,如图3-23所示。

从图3-23可以看出,Triggered Segues子节点包含master view controller和detail view controller两项,分别表示分栏的左右两个控制器。将鼠标放到其对应的空心圆圈,会出现“+”号图标,按住鼠标左键拖曳到合适的子控制器,这样就指定了子控制器。

2.通过代码使用UISplitViewController

除了最简单的方式外,还可以通过纯代码的方式,创建UISplitViewController类的实例,使用该类声明的方法或者属性,实现控制左右两个控制器,具体步骤如下。

图片 12

图3-23 弹出黑色列表框

(1)创建UISplitViewController类对象。

(2)通过viewControllers属性为UISplitViewController设置左右两个控制器。

(3)为UISplitViewController设置delegate属性,该属性必须是遵守了UISplitView ControllerDelegate协议的对象,它负责处理UISplitViewController左侧导航栏的显示和隐藏事件。

值得一提的是,UISplitViewController本身的用法极其简单,仅仅需要管理左右两个控制器而已,当iPad屏幕转为横向或者纵向时,分别会激发delegate的两个方法,具体定义格式如下:

// 当左侧导航栏将要隐藏时激发该方法
- (void)splitViewController:(UISplitViewController *)svc 
willHideViewController:(UIViewController *)aViewController 
withBarButtonItem:(UIBarButtonItem *)barButtonItem 
forPopoverController:(UIPopoverController *)pc;
// 当左侧导航栏将要显示时激发该方法
- (void)splitViewController:(UISplitViewController *)svc 
willShowViewController:(UIViewController *)aViewController 
invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem;

以上两个方法均是iOS 7提供的,另外,iOS 8提供了另外一个方法来替代,它的定义格式如下:

- (void)splitViewController:(UISplitViewController *)svc 
willChangeToDisplayMode:(UISplitViewControllerDisplayMode)displayMode;

在上述代码中,UISplitViewControllerDisplayMode是一个枚举类型,它包含4种情况。需要注意的是,该方法存在一个弊端,即当屏幕处于竖屏状态时,程序首次启动,该方法不会被主动调用,故无法正确判断是否显示激发按钮。

注意:

在iOS 8下,你可以直接在iPhone上使用UISplitViewController了,关于它的使用,可以根据这个网址http://www.onevcat.com/2014/07/ios-ui-unique/的博客内容进行深入学习,这里就不过多赘述了。

3.3.3 实战演练——菜谱

饮食是现代生活不可或缺的部分,人们对于饮食的要求也越加苛刻,拥有一个不错的菜谱应用是一个不错的选择。接下来,带领大家使用UISplitViewController开发一个“菜谱”的iPad项目,具体步骤如下。

1.创建工程,设计界面

(1)新建一个Single View Application应用,名称为02_UISplitViewController,需要注意的是将Devices设置为iPad。

(2)将提前准备好的资源拖曳到Supporting Files文件夹,如图3-24所示。

(3)进入Main.storyboard,打开其对应的文件检查器面板,取消勾选“Use Auto Layout”复选框。删除Storyboard上自带的View Controller,从对象库拖曳一个Split View Controller到程序界面,并设置其为启动控制器。由于该应用左右侧均是表格,因此,删除Detail View Controller链接的控制器,从对象库拖曳一个Navigation Controller,它默认带有一个Table View Controller,将它设置为detail view controller,设计好的界面如图3-25所示。

图片 4

图3-24 添加资源到Supporting Files

图片 2

图3-25 设计好的界面

2.项目添加分组

这个项目主要包含3个部分,采用分组的形式,给项目添加一个Classes子节点,在此节点内依次添加3个子节点,分别命名为Main、Menu、Detail,最终的层次结构如图3-26所示。

需要注意的是,系统自带的View Controller已经删除,与它相关联的ViewController类也直接删除即可。

3.左侧导航栏展示“菜系”数据

(1)打开Supporting Files,会看到Plist子节点,在该节点下会看到food_types.plist,它包含了所有的菜系数据,双击打开的界面,如图3-27所示。

图片 2

图3-26 项目文件组织结构

图片 1

图3-27 food_types.plist

图3-27展示了food_types.plist的数据。其中,最外层的Root是Array类型,它共包含16个Dictionary类型的item,每个item都包含一个String类型的idstr和一个String类型的name,分别表示菜系的id标识和名称。

(2)图3-27所示的每个item都是一个单元格,依据MVC的思想,将单元格抽成一个模型。选中Menu分组,新建一个单元格的模型类FoodType,在FoodType.h中声明两个属性,代码如例3-6所示。

【例3-6】FoodType.h

 1 #import <Foundation/Foundation.h>
 2 @interface FoodType : NSObject
 3 @property (nonatomic, copy) NSString *idstr;  // 标识
 4 @property (nonatomic, copy) NSString *name;   // 名称
 5 @end

(3)引入一个第三方框架MJExtension,能够轻松地将plist文件转换为模型数组。选中Other分组,采用分组的形式添加一个Lib子节点,并将MJExtension导入此节点内,如图3-28所示。

图片 2

图3-28 导入MJExtension框架

需要注意的是,该框架的主头文件为MJExtension.h,它默认已经导入了其他头文件,只要引入此头文件即可使用。

(4)同样选中Menu分组,新建一个左侧导航栏的类MenuViewController,继承自UITableViewController类,并将该类设置为storyboard的master view controller关联类。在MenuViewController.m中定义一个NSArray类型的属性,用于保存FoodType对象,并采用懒加载的方式初始化,代码如下:

 1 #import "MenuViewController.h"
 2 #import "FoodType.h"
 3 #import "MJExtension.h"
 4 @interface MenuViewController ()
 5 @property (nonatomic, strong) NSArray *foodtypes;
 6 @end
 7 @implementation MenuViewController
 8 #pragma mark- 懒加载
 9 - (NSArray *)foodtypes
 10 {
 11  if (!_foodtypes) {
 12    // 1. 获取mainBundle中的plist文件
 13    NSString *pathName = [[NSBundle mainBundle] 
 14    pathForResource:@"food_types.plist" ofType:nil]; 
 15    // 2.将数据放到数组中
 16    _foodtypes = [FoodType objectArrayWithFile:pathName];
 17  }
 18  return _foodtypes;
 19 }
 20 @end

在上述代码中,第13行代码获取了food_types.plist的全路径,第16行代码调用objectArrayWithFile:方法,通过plist创建了一个模型数组。要想使用这个方法,需要注意的是,模型类的属性名称必须与plist文件的Key名称保持一致。

(5)在viewDidLoad方法中,设置顶部导航栏的标题为“菜谱”,让表格默认选中第1行,代码如下:

 1 - (void)viewDidLoad {
 2   [super viewDidLoad];
 3   // 设置标题
 4   self.title = @"菜谱";  
 5   // 默认选中第1行
 6   [self tableView:nil didSelectRowAtIndexPath:
 7   [NSIndexPath indexPathForRow:0 inSection:0]];
 8   [self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 
 9   inSection:0] animated:YES scrollPosition:UITableViewScrollPositionTop];
 10 }
 11 - (void)viewWillAppear:(BOOL)animated{}

需要注意的是,表视图即将显示的时候会调用父类的行为,将单元格的选中变为不选中,只要重写viewWillAppear:方法即可。

(6)实现tableView: numberOfRowsInSection:和tableView: cellForRowAtIndexPath:方法,将数组中保存的模型数据展示到左侧的表视图中,代码如下:

 1 #pragma mark - Table view data source
 2 /**
 3  * 返回多少行
 4  */
 5 - (NSInteger)tableView:(UITableView *)tableView 
 6 numberOfRowsInSection:(NSInteger)section {
 7   return self.foodtypes.count;
 8 }
 9 /**
 10 * 返回cell
 11 */
 12 - (UITableViewCell *)tableView:(UITableView *)tableView 
 13 cellForRowAtIndexPath:(NSIndexPath *)indexPath
 14 {
 15  static NSString *ID = @"MenuCell";
 16  UITableViewCell *cell = [tableView 
 17  dequeueReusableCellWithIdentifier:ID];
 18  if (cell == nil) {
 19    cell = [[UITableViewCell alloc] 
 20    initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
 21  }
 22  // 取出模型对象
 23  FoodType *foodType = self.foodtypes[indexPath.row];  
 24  // 设置数据
 25  cell.textLabel.text = foodType.name;
 26  return cell;
 27 }

4.创建菜品模型

(1)再次打开Plist子节点,单击前面的三角图标展开,在该节点下会看到16个名称类似的plist,它们分别对应着不同的菜系。任选其一,双击打开的界面如图3-29所示。

图3-29展示了type_12_foods.plist的部分数据。其中,最外层的Root也是Array类型,它包含多个Dictionary类型的item,每个item都包含6个String类型的Key,从上到下依次为diff、idstr、imageUrl、name、time、url,分别表示菜品的难度系数、id标识、图片地址、名称、时间、路径。

(2)图3-29所示的每个item都是一个单元格,表示一个菜品,依据MVC的思想,将单元格抽成一个模型。选中Model分组,新建一个单元格的模型类Food,在Food.h中声明6个属性,代码如例3-7所示。

图片 6

图3-29 type_12_foods.plist

【例3-7】Food.h

 1 #import <Foundation/Foundation.h>
 2 @interface Food : NSObject
 3 @property (nonatomic, copy) NSString *diff;   // 难度系数
 4 @property (nonatomic, copy) NSString *idstr;  // 标识
 5 @property (nonatomic, copy) NSString *imageUrl; // 图片url
 6 @property (nonatomic, copy) NSString *name;   // 名称
 7 @property (nonatomic, copy) NSString *time;   // 时间
 8 @property (nonatomic, copy) NSString *url;   // url
 9 @end

5.自定义单元格

单击任意一个菜系,右侧的详情控制器以列表的形式展示多行菜品信息,且每行的格式是固定的。由于单元格的标题和详细内容有一定距离的间隙,需要自定义单元格,最好的方式是采用xib的形式实现,具体步骤如下。

(1)选中View分组,新建一个单元格的类DetailCell,继承自UITableViewCell,需要注意的是需勾选“Also create XIB file”复选框。在DetailCell.xib中,会看到一个细长的长条框,它默认的关联类为DetailCell类,如图3-30所示。

(2)找到其对应的大小检查器面板,设置合适的大小。从对象库拖曳一个Image View和两个Label,将它们摆放到合适的位置,分别进行设置,具体如下。

  • 设置Image View的Image的值为timeline_image_placeholder,添加一张占位图片,并勾选“User Interaction Enabled”和“Multiple Touch”复选框。
  • 设置标题Label的Font为System 20.0,设置另一个Label的Font为System 16.0。

按照以上的方式进行设置,设计好的界面如图3-31所示。

(3)在DetailCell.h文件中,定义一个模型属性,用于保存单元格的数据,并提供一个类方法供外界调用,代码如例3-8所示。

图片 4

图3-30 Detail Cell的关联类

图片 5

图3-31 设计好的界面

【例3-8】DetailCell.h

 1 #import <UIKit/UIKit.h>
 2 #import "Food.h"
 3 @interface DetailCell : UITableViewCell
 4 @property (nonatomic, strong) Food *food;
 5 /**
 6  * 通过一个tableView创建DetailCell对象
 7  */
 8 + (instancetype)detailCell:(UITableView *)tableView;
 9 @end

(4)引入SDWebImage框架,实现从网络加载图片,并将其导入到Lib分组。在DetailCell.m文件中,导入UIImageView+WebCache.h,添加3个属性与xib界面相关联,并给这3个属性赋值,代码如例3-9所示。

【例3-9】DetailCell.m

 1 #import "DetailCell.h"
 2 #import "UIImageView+WebCache.h"
 3 @interface DetailCell()
 4 @property (weak, nonatomic) IBOutlet UIImageView *iconView;
 5 @property (weak, nonatomic) IBOutlet UILabel *nameLabel;
 6 @property (weak, nonatomic) IBOutlet UILabel *detailLabel;
 7 @end
 8 @implementation DetailCell
 9 + (instancetype)detailCell:(UITableView *)tableView
 10 {
 11  static NSString *ID = @"DetailCell";
 12  DetailCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];  
 13  if (cell == nil) {
 14    cell = [[[NSBundle mainBundle] loadNibNamed:@"DetailCell" 
 15    owner:nil options:nil] lastObject];
 16    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
 17  }
 18  return cell;
 19 }
 20 - (void)setFood:(Food *)food
 21 {
 22  _food = food;
 23  // 1.设置图片
 24  NSURL *imageUrl = [NSURL URLWithString:food.imageUrl];
 25  [self.iconView sd_setImageWithURL:imageUrl placeholderImage:
 26  [UIImage imageNamed:@"timeline_image_placeholder"] 
 27  options:SDWebImageRetryFailed | SDWebImageLowPriority];  
 28  // 2.设置名称
 29  self.nameLabel.text = food.name;  
 30  // 3.设置详情
 31  self.detailLabel.text = [NSString stringWithFormat:
 32  @"难度:%@ 时间:%@", food.diff, food.time];
 33 }
 34 @end

在例3-9中,第9~19行代码是detailCell:方法的实现,其中,第14行代码调用loadNibNamed: owner: options:方法加载xib文件。第20~33行代码是setFood:方法,用于给其他控件对象赋值。

6.通过代理展示详情控制器的信息

选中任意一个菜系,右侧会展示这个菜系的详细信息,如菜品名称、图片等,并将右侧导航栏的标题设为菜系的名称。要想完成以上需求,通过代理的方式可以实现,即让详情控制器的类成为MenuViewController类的代理,监听选中每一行执行的行为,具体步骤如下。

(1)在MenuViewController.h文件中,声明一个MenuViewControllerDelegate协议,在协议中定义一个供代理实现的方法,并且定义一个delegate属性,代码如例3-10所示。

【例3-10】MenuViewController.h

 1 #import <UIKit/UIKit.h>
 2 @class MenuViewController,FoodType; 
 3 @protocol MenuViewControllerDelegate <NSObject>
 4 @optional
 5 - (void)menuViewController:(MenuViewController *)menuVc 
 6 foodType:(FoodType *)foodType;
 7 @end
 8 @interface MenuViewController : UITableViewController
 9 @property (nonatomic, weak) id<MenuViewControllerDelegate> delegate;
 10 @end

在例3-10中,第5行代码定义了一个@optional关键字修饰的方法,该方法包含menuVc和foodType两个参数,其中,foodType表示选中行对应的模型,根据这个模型的idstr找到详情所对应的plist,以及展示右侧导航栏的标题。

(2)在MenuViewController.m文件中,实现选中单元格的方法,通知代理,并将模型数据传递过去,代码如下:

 1 - (void)tableView:(UITableView *)tableView 
 2 didSelectRowAtIndexPath:(NSIndexPath *)indexPath
 3 {
 4   // 取出模型
 5   FoodType *foodType = self.foodtypes[indexPath.row];  
 6   // 通知代理,并将模型数据传递过去
 7   if ([self.delegate respondsToSelector:
 8   @selector(menuViewController:foodType:)]) {
 9    [self.delegate menuViewController:self foodType:foodType];
 10  }
 11 }

(3)选中Detail子节点,采用分组的形式,在此节点内依次添加3个子节点,分别命名为Controller、View、Model,Detail的层次结构如图3-32所示。

图片 4

图3-32 Detail的层次结构

(4)选中Controller分组,新建一个右侧详情的类DetailViewController,继承自UITableViewController类,并将该类设置为storyboard的detail view controller关联类。在DetailViewController.h文件中,遵守MenuViewController Delegate协议,代码如例3-11所示。

【例3-11】DetailViewController.h

 1 #import <UIKit/UIKit.h>
 2 #import "MenuViewController.h"
 3 @interface DetailViewController : UITableViewController
 4 <MenuViewControllerDelegate>
 5 @end

需要注意的是,DetailViewController类与MenuViewController类无直接关系,外界只能从声明文件中确认其是否遵守协议。

(5)MenuViewController和DetailViewController之间没有必然的关系,要想设置它们的代理,可以通过它们共同拥有关系的分栏控制器完成。选中Main分组,新建一个表示分栏控制器的MainViewController类,继承自UISplitViewController类,并将该类设置为storyboard的SplitViewController关联类。在MainViewController.m文件中,完成代理的设置,代码如例3-12所示。

【例3-12】MainViewController.m

 1 #import "MainViewController.h"
 2 #import "MenuViewController.h"
 3 #import "DetailViewController.h"
 4 @interface MainViewController ()
 5 @end
 6 @implementation MainViewController
 7 - (void)viewDidLoad {
 8   [super viewDidLoad];
 9   // 1.拿到MenuViewController
 10  UINavigationController *menuNav = [self.childViewControllers firstObject];
 11  MenuViewController *menuVc = [menuNav.childViewControllers firstObject];  
 12  // 2.拿到DetailViewController
 13  UINavigationController *detailNav = [self.childViewControllers lastObject];
 14  DetailViewController *detailVc = [detailNav.childViewControllers 
 15  firstObject];
 16  // 3.让DetailViewController成为MenuViewController的代理
 17  menuVc.delegate = detailVc; 
 18 }
 19 @end

在例3-12中,MainViewController的childViewControllers包含firstObject和lastObject两个值,分别对应左右两个控制器。

(6)在DetailViewController.m文件中,定义一个NSArray类型的属性,用于保存Food对象,根据idstr找到对应的plist文件,从该文件中加载数据,代码如下:

 1 #import "DetailViewController.h"
 2 #import "FoodType.h"
 3 #import "Food.h"
 4 #import "MJExtension.h"
 5 #import "DetailCell.h"
 6 @interface DetailViewController ()
 7 // 定义一个数组,保存模型数据
 8 @property (nonatomic, strong) NSArray *foods;
 9 @end
 10 @implementation DetailViewController
 11 #pragma mark - MenuViewControllerDelegate
 12 - (void)menuViewController:(MenuViewController *)menuVc 
 13 foodType:(FoodType *)foodType
 14 {
 15  // 设置标题
 16  self.title = foodType.name;  
 17  // 拼接plist的名称
 18  NSString *fileName = [NSString stringWithFormat:
 19  @"type_%@_foods.plist", foodType.idstr];  
 20  // 加载数据
 21  self.foods = [Food objectArrayWithFilename:fileName];  
 22  // 刷新数据
 23  [self.tableView reloadData];
 24 }
 25 @end

(7)实现tableView: numberOfRowsInSection:和tableView: cellForRowAtIndexPath:方法,将数组中保存的模型数据展示到右侧的表视图中,代码如下:

 1 /**
 2  * 返回多少行
 3  */
 4 - (NSInteger)tableView:(UITableView *)tableView 
 5 numberOfRowsInSection:(NSInteger)section {
 6   return self.foods.count;
 7 }
 8 /**
 9  * 返回cell
 10 */
 11 - (DetailCell *)tableView:(UITableView *)tableView 
 12 cellForRowAtIndexPath:(NSIndexPath *)indexPath
 13 {
 14  DetailCell *cell = [DetailCell detailCell:tableView];  
 15  // 取出模型
 16  Food *food = self.foods[indexPath.row];  
 17  // 设置数据
 18  cell.food = food;
 19  return cell;
 20 }
 21 - (CGFloat)tableView:(UITableView *)tableView 
 22 heightForRowAtIndexPath:(NSIndexPath *)indexPath
 23 {
 24  return 122;
 25 }

7.显示或者隐藏“菜谱”按钮

旋转屏幕到竖屏,左侧的导航栏隐藏,右侧的控制器的导航栏左边多出一个按钮,通过该按钮激发导航栏的显示;旋转屏幕到横屏,左侧的导航栏显示,激发按钮会隐藏。通过这个过程可知,当屏幕旋转时,通知右侧的控制器做出改变,可以通过代理的方式实现,即让DetailViewController成为MainViewController的代理,监听左侧导航栏的显示或者隐藏,具体步骤如下。

(1)在例3-11中,让DetailViewController同样遵守UISplitViewControllerDelegate协议。

(2)在例3-12中,设置DetailViewController为MainViewController的代理对象,代码如下所示:

 1 // 4.让DetailViewController成为MainViewController的代理
 2 self.delegate = detailVc;

(3)在DetailViewController.m文件中,实现左侧导航栏即将显示或者隐藏激发的方法,代码如下:

 1 #pragma mark - UISplitViewControllerDelegate
 2 /**
 3  * 左侧导航栏即将隐藏
 4  */
 5 - (void)splitViewController:(UISplitViewController *)svc 
 6 willHideViewController:(UIViewController *)aViewController 
 7 withBarButtonItem:(UIBarButtonItem *)barButtonItem 
 8 forPopoverController:(UIPopoverController *)pc
 9 {
 10  self.navigationItem.leftBarButtonItem = barButtonItem;
 11 }
 12 /**
 13 * 左侧导航栏即将显示
 14 */
 15 - (void)splitViewController:(UISplitViewController *)svc 
 16 willShowViewController:(UIViewController *)aViewController 
 17 invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
 18 {
 19  self.navigationItem.leftBarButtonItem = nil;
 20 }

在上述代码中,只要将左边按钮设置为barButtonItem,就能添加一个标题为“菜谱”的按钮。

8.展示菜品的详细做法

选中任意一个菜品,中间渐隐地弹出一个窗口,该窗口以网页的形式展示了菜品的详细做法,单击窗口左侧的“关闭”按钮,该窗口逐渐地隐藏,具体步骤如下。

(1)选中Controller分组,新建一个表示弹出窗口的RecipeViewController类,继承自UIViewController基类。在RecipeViewController.h文件中,声明一个Food类型的模型属性,通过赋值的方式,给该控制器传递模型数据,代码如例3-13所示。

【例3-13】RecipeViewController.h

 1 #import <UIKit/UIKit.h>
 2 #import "Food.h"
 3 @interface RecipeViewController : UIViewController
 4 // 直接通过赋值的方式,给RecipeViewController传递数据
 5 (注意:不能重写set方法,因为View这个时候还没有加载出来)
 6 @property (nonatomic, strong) Food *food;
 7 @end

(2)在DetailViewController.m文件中,实现tableView: didSelectRowAtIndexPath:方法,以modal的形式弹出一个窗口,并将选中行的模型传递给RecipeViewController对象,代码如下:

 1 - (void)tableView:(UITableView *)tableView 
 2 didSelectRowAtIndexPath:(NSIndexPath *)indexPath
 3 {
 4   // 1.取出模型
 5   Food *food = self.foods[indexPath.row];
 6   // 2.创建即将modal出的控制器
 7   RecipeViewController *recipeVc = [[RecipeViewController alloc] init];
 8   recipeVc.food = food;  
 9   // 3.包装一个导航控制器
 10  UINavigationController *recipeNav = [[UINavigationController alloc] 
 11  initWithRootViewController:recipeVc];
 12  // 3.1 设置呈现样式
 13  recipeNav.modalPresentationStyle = UIModalPresentationFormSheet;
 14  // 3.2 设置过渡样式
 15  recipeNav.modalTransitionStyle = UIModalTransitionStyleCoverVertical;  
 16  // 4.将控制器弹出来
 17  [self presentViewController:recipeNav animated:YES completion:nil];
 18 }

(3)在RecipeViewController.m中,需要通过UIWebView加载html文件,顾名思义,只要将控制器的View更改为webView即可,代码如下:

 1 #import "RecipeViewController.h"
 2 @interface RecipeViewController ()
 3 @property (nonatomic, weak) UIWebView *webView;
 4 @end
 5 @implementation RecipeViewController
 6 - (void)loadView
 7 {
 8   // 将控制器的view改成webView,才可以加载HTML
 9   UIWebView *webView = [[UIWebView alloc] init];
 10  webView.frame = [UIScreen mainScreen].applicationFrame;
 11  self.view = webView;
 12  self.webView = webView;
 13 }
 14 @end

(4)html网页的样式是由recipe.css控制的,它们是配合使用的。由于html记录的recipe.css的路径为“../css/recipe.css”,沙盒中的css文件夹是虚拟不存在的,将之前导入的HTML文件彻底删除,卸载模拟器上的该应用,使用K快速Clean。选中Supporting Files分组,再次导入HTML资源,弹出如图3-33所示的窗口。

图片 7

图3-33 添加文件的操作

图3-33所示是添加文件时弹出的窗口,将“Create folder references”单选按钮选中,单击“Finish”按钮,Html以蓝色文件夹的形式显示。这时,沙盒程序包中的Html文件的路径为“Html/food/文件名.html”。

(5)在viewDidLoad方法中,根据传递过来的Food模型的idstr,找到与之对应的HTML文件,并展示到webView上,代码如下:

 1 - (void)viewDidLoad {
 2   [super viewDidLoad];
 3   // 1.改变导航栏的左边按钮
 4   self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] 
 5   initWithTitle:@"关闭" style:UIBarButtonItemStyleDone target:self 
 6   action:@selector(close)];  
 7   // 2.拼接HTML
 8   NSString *htmlName = [NSString stringWithFormat:@"Html/food/%@.html", 
 9   self.food.idstr];
 10  NSURL *htmlURL = [[NSBundle mainBundle] URLForResource:htmlName 
 11  withExtension:nil];  
 12  // 3.根据URL创建NSURLRequest对象
 13  NSURLRequest *request = [NSURLRequest requestWithURL:htmlURL];  
 14  // 4.加载对应的request
 15  [self.webView loadRequest:request];
 16 }

(6)实现close方法,表示单击“关闭”按钮后执行的行为,也就是关闭后弹出来的窗口,代码如下:

 1 /**
 2  * 关闭控制器
 3  */
 4 - (void)close
 5 {
 6   [self dismissViewControllerAnimated:YES completion:nil];
 7 }

9.运行程序

单击“运行”按钮,在模拟器上运行程序。程序运行成功后,展示了左右两个视图,以“川菜”举例,单击左侧的“川菜”一行,右侧展示了“川菜”所有的菜品;单击其右侧的“水煮鱼”一行,弹出一个临时窗口,用于显示“水煮鱼”的详细做法;单击“关闭”按钮,临时窗口消失,程序运行的部分效果如图3-34所示。

图像说明文字

图3-34 程序运行的部分效果图

图像说明文字多学一招:添加文件操作的窗口

图3-33所示的窗口有多个单选按钮和复选框,从上到下介绍如下。

  • Copy items if needed:表示拖移资源时是否需要复制一份。
  • Create groups:只是创建虚拟文件夹,在沙盒中不存在此文件夹。
  • Create folder references:在项目当中创建文件,并且会在沙盒里面创建文件夹。
  • 02_UISplitViewController:表示是否将资源添加到main bundle里。
  • 02_UISplitViewControllerTests:表示是否将资源添加到单元测试。

需要注意的是,只要是选中的状态,就表示要执行这个操作。

3.4 本章小结

本章首先介绍了iPad开发和iPhone开发的异同:两者都是基于iOS操作系统,有许多共有的API,但它们的屏幕尺寸不同、UI界面排版与设计不同、键盘不同。然后针对iPad开发特有的类UIPopoverController与UISplitViewController进行了详细讲解,其中UIPopoverController用于呈现“漂浮”类型的视图,而UISplitViewController用于将屏幕分栏,对于这两个类都有各自对应的案例介绍,方便大家学习和理解。

【思考题】

1.简述iPhone和iPad开发的异同。

2.简述使用UIPopoverController实现弹出Popover视图的步骤。

图像说明文字

目录

推荐用户

同系列书

人邮微信
本地服务
人邮微信
教师服务
二维码
读者服务
读者服务
返回顶部
返回顶部