iOS 7教程 - StoryBoards Part2
原文地址:http://www.raywenderlich.com/50310/storyboards-tutorial-in-iOS-7-part-2
iOS 7教程 - Storyboards Part1 译文地址:iOS 7教程 - Storyboards Part1
iOS开发也好些日子了,一直都是纯code,偶尔用用xib,最近闲来无事想体验下storyboard,于是网上找到了各种关于storyboard的入门教程,可惜很多都比较老了都是iOS5,和iOS6系列的教程,我想这xcode都更新小半年了,iOS7.1都快出了,再整老版本不太合适吧,于是找到了泰然网翻译的一篇iOS7 storyboard教程,可惜只翻译了第一篇,第二篇估计还没来得及吧!捉摸着都看了开头了,怎么着也得看完是吧,就寻着原文找到了第二篇,坑爹的英文,虽然不是很感冒鸟文,加上Google也多少能看明白,一下算是自己理解的然后加上些口水话,有兴趣的童鞋凑合着看吧,第一次翻译,哈哈哈,开始吧,Ready,go…
温馨提示:教程成员Matthijs Hollemans(iOS Apprentice系列的作者)已经发表了这个教程的iOS5版本至iOS7版本。这是本书第三版的预览,这个版本将会更新到iOS 7。好好享受吧!
如果你想了解storyboard on iOS7,那你来对地方了!
在storyboard on iOS7系列教程的第一部分,已经介绍了使用Interface Builder来创建和连接各种视图控制器的基本知识,以及如何直接从storyboard编辑器进行自定义表视图单元格。
在本教程系列的第二个和最后部分,我们将讨论segues,静态表视图单元格,添加播放器场景,和游戏场景!
如果你跟着上一次的教程走完了整个步骤,那就把上一次的工程打开!
好吧,让我们深入到storyboard其他酷毙的功能!
##介绍segues
现在是时候添加更多的视图控制器到storyboard了,我们来创建一个新的场景。
打开Main.storyboard并拖动一个Bar Button Item到Players场景里的navigation bar中。在属性检查器**(Attributes inspector)改变它的标示符(Identifier)变为加号(Add)**。
当用户点击这个 + 按钮的时候应用程序会弹出一个新的表演者详细信息的模态视图。
将一个新的导航控制器**(Navigation Controller)拖拽到表演者视图的右边。记住,你可以双击storyboard缩小让你有更大的工作空间。新的导航控制器都附带一个表视图控制器,应该已经得心应手了吧!
窍门:点击刚刚在表演者视图上添加的那个 **+ 按钮,按住ctrl并且拽向导航控制器
松开鼠标将会弹出如下选项:
选择modal,这是视图跳转的一种新的路径(这句不知道怎么翻译好,就理解为两种视图之间的跳转好了):
这样的链接关系就称之为:segue。这种故事板的链接关系是从一个视图控制器跳转到另外一个视图控制器。
使用segue很酷的事情就是,你再也不用编写任何代码来实现控制器跳转,也不必为你的按钮挂接到IBActions。只需要想刚才那样一个拖动就可以搞定了。(注:如果您的控件已经有了一个IBAction连接,那么segue将覆盖它。)
让我们来运行一下,点击刚才添加的那个 + 按钮,一个新的表视图将呈现在你的眼前
这个所谓的模态关系。新的场景将完全掩盖了前一个场景。用户不能与前一个场景交互,直到他们关闭了模态视图。以后你还会看到,在导航控制器的堆栈上还可以以推动的方式跳转。
新的场景挺好用吧,但有个问题是你不能关闭他返回到刚才的场景。这是因为segue是单向的,一个场景到另一个场景。回去的时候,你必须使用delegate模式。鉴于这种情况,你需要先新建视图控制器。
添加一个新的文件添加到项目,并命名为PlayerDetailsViewController,它是UITableViewController的子类。切换回Main.storyboard并选择新的表视图控制器的场景**(Root View Controller)。在Identity inspector中设置其class为PlayerDetailsViewController**。我就是经常忘了这么做,所以这里需要提醒一下。
改变新建表视图的标题为:Add Player(通过双击导航栏)。同时添加两个Bar Button Item到导航栏左右两边。在属性检查器**(Attributes inspector)中,左边的按钮改名为:Cancel,右边的按钮改名为:Done**(你也可以通过改变按钮的stye来得到相同的效果)。
把PlayerDetailsViewController.h
改成下面这样:
1 | @class PlayerDetailsViewController; |
这里我们定义了一个委托协议,好让新的场景可以返回上一个场景,并且添加了cancel和done按钮的两个点击事件。
切换到故事版,点击新建的表视图导航栏上那个done按钮,按住ctrl进行拖拽,拖拽箭头如下图所示:
选择Sent Actions栏里的done
事件,同样,cancel按钮也如此步骤,选择cancel
事件。
到PlayerDetailsViewController.m
源文件里添加下面两个方法
1 | - (IBAction)cancel:(id)sender |
这是对应两个按钮的点击事件方法。我们可以用它来调用代理,通过代理方法来关闭当前的场景或者视图。它是由委托关闭屏幕。(这里默认的是必须实现方法,你要不喜欢可以改成选用,在协议方法前面加上@optional字段。或者你也可以通过当前控制器自行关闭,并且在关闭前后通知代理)。
注释:在这里代理方法只有一个对象作为参数,在这种情况下,PlayerDetailsViewController。好处代理总是知道是哪个对象发送的消息。
之前,我们已经给了PlayerDetailsViewController委托协议,但是你需要实现这个协议和协议方法。PlayersViewController将呈现的添加播放器屏幕的视图控制器。以下内容添加到PlayersViewController.h
:
1 | #import "PlayerDetailsViewController.h" |
实现代理方法PlayersViewController.m
:
1 | #pragma mark - PlayerDetailsViewControllerDelegate |
目前,这些委托方法只是关闭当前视图控制器。以后会让他们做更多有趣的事情。 **
现在还差一件事情没做了,设置PlayersViewController为PlayerDetailsViewController的代理。你可能以为在Interface Builder我们可以连线来解决。但很不幸的是,这是不可能的。传输数据到新的视图控制器,你仍然需要编写一些代码。(最后不怎么会翻译,就理解为storyboard**的方式设置代理没办法连线解决,只能通过代码设置代理)
将以下方法添加到PlayersViewController.m
:
1 | - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender |
prepareForSegue这个方法在新视图从storyboard上加载的时候就会被处罚,但是它是不可见,需要手动实现,所以我们可以用它来设置代理。
注意:一般情况下不会调用
prepareForSegue:
方法。这是一个从UIKit的一条消息,让你知道,segue被触发了。
这个segue的目的地是导航控制器,因为那是你连接到栏按钮项目。要获得PlayerDetailsViewController实例,你必须通过viewControllers的导航控制器的堆栈来得到它。
运行应用程序,按下 + 按钮,并尝试关闭添加播放器画面。它仍然无法正常工作!
那是因为你没有给segue标识符。从prepareForSegue代码检查该标识符**(“AddPlayer”)。因此建议经常做这样的检查,因为你可能有多个segue到一个视图控制器,你就需要他们各自的区别开来。
要解决此问题,请打开Main.storyboard并点击玩家屏幕和导航控制器之间的segue。请注意,栏按钮项目现在亮起,这样你就可以很容易地看到哪些控制触发此SEGUE。
在属性检查器(Attributes inspector)中,标识符(Identifier)设置为AddPlayer**:
再次运行程序,这下是不是就可以正常关闭弹出的模态视图啦!
注意:这本来完全可以调用dismissViewControllerAnimated:来完成弹出模态画面的。也没有规定说,委托必须做到这一点。我个人更喜欢让委托处理这一点,但如果你想模态视图关闭本身,那么请随意。
顺便,在属性检查器里可以设置segue的跳转过程(视图控制器的跳转过程),你可以设计几种过场动画:
选择一个你喜欢的过场动画,但是记住千万别改变风格**(style)**设置,因为这里的链接是模态关系 - 改变会导致应用程序的崩溃,切记!
我们将会在本教程中使用几次委托协议,下面是个核对列表供你检查两个场景之间的链接关系:
- 通过一个按钮或者其他控件创建一个sugue来链接源场景到目标场景(如果你是新建模态视图,这时目标场景将是一个导航控制器)。
- 给segue的唯一标识符(源场景必须有唯一的标示符,不同的场景可以使用相同的标识符)。
- 为目标场景创建一个委托协议。
- 调用取消和完成按钮的委托方法,你的目的地的场景需要与源场景进行通信(两个视图控制器之间的通讯)。
- 源场景实现委托协议。当取消或完成按钮被按下,它应该关闭该目标视图控制器。
- 在源视图控制器实现
prepareForSegue:
方法,并且把目标视图控制器的代理设置为源视图控制(eg:playerDetailsViewController.delegate = self)。
委托是必须的,因为至少在iOS5没有东西可以作为一个**”反向segue”。当segue被触发,当segue被触发的时候总是会为目标视图控制器创建一个实例。你当然可以做一个反向的segue从目的到源,但可能不会做你所期望的(这段搞不懂,谷歌翻译的)。
如果你是为表演者场景创建一个segue回来的取消按钮,例如,那么这将不会关闭添加表演者场景,并返回到表演者,但它创造的表演者场景的新实例。你这样一遍一遍的创建新的视图控制器会导致内存溢出(也是没看懂,没办法翻译,针对上面没看懂的这两段,我没管,看下面这个提示就够了!)。
请记住:segue只有一条路可走,它们只是用来进入一个新的场景。要返回您之前的场景(或从导航控制器堆栈中弹出它),通常是用delegate。segue仅由源控制器使用。目标视图控制器甚至不知道它是被segue**调用。
##静态单元格
在本章结束的时候你的添加表演者场景会像下面这样:
这是一个分组表视图,当然,你不必为此表格创建一个数据源。您可以在Interface Builder中直接设计它 - 没必要单独为它写一个cellForRowAtIndexPath
方法,这种称为静态单元格。
在添加表演者控制器里选择表视图,在属性检查器(Attributes inspector)**里改变content为static cells。把style改变为Grouped,并且设置两个组(两个section)**。
当您更改section的属性值时,编辑器将复制现有的部分(你也可以自己手动复制)。
完成后的场景里每个section里只有一个cell,多余的就把它们删了吧(别说你不会删)。
选择Table View Section,这个可能不是很好点中,那你就在编辑器或者导航条上选吧,这样就妥妥的了,像这样:
或者这样
OK,我们选中之后就在属性检查器中把Header改为Player Name。
然后我们拖拽一个TextField控件到cell里面去,调整边距到cell的contentview一样大,然后设置font为system 17,border style改为第一个样式(目前是圆角样式),把adjust to fit的勾勾取消掉。
我们将使用Xcode的助理编辑功能,在PlayerDetailsViewController里创建之前拖拽到cell里的那个text field的接口。打开**助理编辑用工具栏(Assistant Editor)**上的按钮(一个看起来像燕尾服/外星人的脸)。它应该会把PlayerDetailsViewController.h
自动打开(如果没有,请使用jump bar在右手分割窗口来选择头.h文件)。
选择text field并按住Ctrl键拽入.h文件:
松开鼠标按钮就弹出了这种小窗口
把新的接口命名为nameTextField,链接之后xcode会自动在PlayerDetailsViewController.h加上属性:
1 | @property (weak, nonatomic) IBOutlet UITextField *nameTextField; |
现在,点击第二个section里的静态单元格,把Style改为Right Detail,这是一个标准的单元格样式,点击cell里左边的label,把label内容改为Game并且把accessory风格改为Disclosure Indicator。(不知道是版本问题还是什么,我在xcode5.0.2上发现改了label的内容什么都没有,后来发现字体大小为0,次奥,以前xib没这情况啊,继续。。。)
把cell右边的label也改了,内容改为Detail,然后重复刚才拖拽text field的步骤一样,为这个label新建一个接口并命名为detailLabel,就像这样
添加播放器页面的最终看起来像这样
提示:现在这个设计的屏幕都为iPhone5的,屏幕高568个像素点,相对于以前的iPhone机型的4英寸屏幕的高度为480个像素点。您可以使用最左边的按钮,从小事浮动面板,坐落在画布的底部这两种形式的因素之间进行切换。
很明显,你的应用程序应该正常工作与两个屏幕大小。您可以使用自动调整大小口罩或从的iOS 6新的自动布局技术实现这一点。对于评级的应用程序,你不必做任何幻想。它仅使用表格视图控制器,它们会自动调整大小以适合在iPhone5的额外的屏幕空间。
返回到添加表演者页面。当您使用静态的单元格,你的表视图控制器不需要一个数据源。由于您使用的Xcode的模板来创建PlayerDetailsViewController类,它仍然有数据源的一些占位符代码,这将防止静态单元格无法正常工作。
打开PlayerDetailsViewController.m
,把下面这段一下的代码都删掉(除了你自己添加任何代码):
1 | #pragma mark - Table view data source |
这样删除之后你运行来试试看是不是可以工作了,对于静态单元格你基本可以不用写任何代码,除了控制代码之外。
这里还有个问题就是text field没有整个覆盖在cell上面,你点击cell边缘的时候键盘是不会弹出的。
为了避免这种情况,你应该让第一行单元格里调出键盘。这是很容易做到,只需添加一个这个方法didSelectRowAtIndexPath:
实现如下:
1 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath |
这只是说,如果用户点击第一个单元格,应用程序应该激活text filed。我们用代码调整当点击第一个section里的cell时候,应该激活text field,这是为了更好的用户体验。
还有,我们应该选择cell,把selection的风格从blue改变为none,这样就没有了之前那样点击text field没有覆盖到的cell部分的时候出现高亮暗影而影响用户体验了。
好吧,这是添加表演者场景的设计。现在,让我们真正使其发挥作用。
##让添加表演者视图工作吧
现在你会忽略了游戏的行,只是让用户输入玩家的名字。
当用户按下Cancel按钮,屏幕应该关闭,任何数据都将丢失。这部分已经没问题了。委托(在玩家屏幕)接收”并取消”的消息,并简单地反馈给视图控制器。
当用户按下Done(完成),但是,你应该创建一个新的表演者对象,并填写其属性。那么你应该告诉代理已经添加了一个新的,使它好更新自己的视图。
现在PlayerDetailsViewController.m
导入
1 | #import "Player.h" |
接着改变done:
方法:
1 | - (IBAction)done:(id)sender |
done:
方法现在创建一个新的player实例,并把它发送给该委托。委托协议目前并无此方法,所以PlayerDetailsViewController.h
文件内容要稍稍改变一下:
1 | @class Player; |
所以在新的代理方法里面要作一些新的改变:
1 | - (void)playerDetailsViewController:(PlayerDetailsViewController *)controller didAddPlayer:(Player *)player |
这里的意思就是把新建的player对象加到players数组里去,然后通知表视图(table view)**新增加了一行,因为表视图(table view)和数据源(data source)**数据必须同步。
你可以只执行[self.tableView reloadData]
这个方法,但是为了给表视图插入新的行有动画效果,UITableViewRowAnimationAutomatic
这个枚举类型的参数是自动选择动画效果,非常方便。
现在试试吧,你应该能在列表里看到新加入的行了!
如果你想知道这些故事板的性能表现,那么你应该知道,一次加载整个故事板是不是一个大问题。故事板并不是马上实例化所有的视图控制器,只有初始视图控制器的时候才会。因为你的初始视图控制器是一个标签栏控制器,它包含两个视图控制器也被加载。
然而别的控制器也并不实例化,直到你连线桥接给他们。当您关闭这些视图控制器,他们会立即释放,所以只有积极使用视图控制器是在内存中,就像如果你使用独立的nib一样。
让我们实践来看看。在PlayerDetailsViewController.m
中添加下面的方法:
1 | - (id)initWithCoder:(NSCoder *)aDecoder |
你重写的initWithCoder:
和dealloc
的方法,使他们能在Xcode的调试窗口打印信息。现在再次运行该应用程序,并打开添加选手画(Add Player页面)。你应该能看到,这个时候视图控制器没有分配到内存。
当你关闭添加播放器屏幕(Add Player页面),按点击取消或完成,你应该看到dealloc
内的消息被打印。如果你再次打开这个页面,你也应该看到的initWithCoder:
方法的响应。这应该向你保证,视图控制器是按需加载而已,就像他们如果你是手工加载nib一样。
关于静态单元格:他们只工作在UITableViewController。虽然界面生成器可以让你把它们作为一个Table View对象添加到一个普通的UIViewController中,但这将不会在运行时正常工作。这样做的原因是,UITableViewController中提供了一些额外的特殊照顾的数据源的静态单元格。 Xcode中甚至会阻止您编译这样一个项目,并显示错误消息:”非法配置:在嵌入的UITableViewController情况下的静态表视图才有效”。
原型单元格,另一方面,只是正常工作中,你放置一个表视图到普通视图控制器内。虽然是在nib下工作。目前,如果你想使用原型单元格或静态单元格,你必须使用一个故事板。
它不是不可想象的,你可能想,结合静态单元格和常规动态单元格的单表视图,但这不是很好被SKD支持。如果你需要在你的app中加入这些,你可以看看这个论坛:his post on the Apple Developer Forums