从零开始学ios开发(十八):Storyboards(下)
这篇我们完成Storyboards的最后一个例子,之前的例子中没有view之间的切换,这篇加上这个功能,使Storyboards的功能完整呈现。在Storyboards中负责view切换的东西叫做“segue”,只需对它进行简单的设置即可,一切都是傻瓜式的,无需繁琐的代码。好了,开始我们的例子吧。
1)Create a Simple Storyboard 创建一个project,左边选择Application,右边选择Empty Application template(我们这里不使用Single View Application,而是创建了一个Empty Application,之后我们会自己手动添加storyboard,这样你就可以更好的了解storyboard使用方法了),点击Next
将项目命名为Seg Nav,点击Next,完成项目创建
由于我们要创建的项目是基于storyboard,因此需要对BIDAppDelegate.m中didFinishLaunchingWithOptions方法进行如下修改
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Override point for customization after application launch. self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; return YES;}
删除所有行,只保留最后的return YES。
2)添加storyboard 鼠标右击project navigator中的Seg Nav文件夹,然后选择New File...,在弹出的对话框中,左边选择User Interface,右边选择Storyboard,点击Next
之后的Device Family选择iPhone,点击Next,将storyboard命名为MainStoryboard.storyboard,完成创建
之后一步我们所要做的是将MainStoryboard.storyboard设置为程序启动是默认载入的对象,选中project navigator中最顶端的项目名称,这样summary tab就会出现,找到里面的Main Storyboard选项,将其值设置为我们刚刚添加的MainStoryboard,这样在陈旭启动后会默认的载入这个storyboard。
3)设置MainStoryboard.storyboard 再次选中MainStoryboard.storyboard,这是它里面什么都没有,layout area中也是空空如也,在object library中找到Navigation Controller,然后将其拖入layout area中,这样在layout area中瞬间就多了2个controller 为什么会是2个controller而不是一个呢?这是因为UINavigationController只包含一个navigation bar,没有其他的东西,因此当我们拖一个Navigation Controller进入layout area后,系统自动为这个Navigation Controller关联了一个Table View Controller,这样就完整了。上图中,2个Controller的中间有一个箭头相连,它表示右边的Table View Controller是左边Navigation Controller的rootViewController(记住箭头中间的圆圈,之后关联其他Controller的时候,其他箭头中间的形状会有所不同)
在dock中选中Table View Controller的Table View
打开attrbutes inspector,将Content设置为“Static Cells”,layout area中的table view上出现了3个cell,我们只需要2个就够了,删除其中的一个
在dock中依次选中2个Table View Cell,然后在attributes inspector中将他们的Style改成“Basic” 这样在cell上会出现Title 将上面的一个Title改成“Single view”,下面的一个Title改成“Sub-menu”(直接双击Title进行修改)
然后我们对Table View Controller上的Navigator bar进行操作,在dock中选中Navigation item 将attributes inspector中Title设值为“Segue Navigator”,将Back Button设值为“Seg Nav” 注意,这里的Back Button并不是显示在当前的navigator bar上的,而是显示在下一个sub view controller的navigator bar上的返回按钮的文字,用于表面将返回到哪个父contoller
编译运行一下程序
我们可以看到MainStoryboard.storyboard作为默认的view被载入到程序中,界面上显示了我们修改后的navigator bar,还有table view中的2个cell。这样子是不是很简单?到目前为止,对Storyboard的操作完全是基于Interface Builder的,我们没有写过任何代码。
4)创建第一个Segue例子 在project navigator中选中MainStoryboard.storyboard,然后从Object library中找到View Controller,将其拖入到layou area中,放置在现有controller的右边
然后在Object library中找到Label,拖入到刚才添加的View Controller中,将Label的文字改成“Single view”,从添加的文字可以知道,这个view是通过点击table view中的第一个cell打开的
下面就是通过Segue将table view中的第一个cell和Single view关联起来,首先选中table view中的第一个cell,然后control-drag到新添加的view,然后释放鼠标,这时会有一个弹出框弹出, 这个弹出框有2部分组成,Selection Segue和Accessory Action,这2部分的选项是相同的。 Selection Segue的意思是当用户点击table view cell的任何部分,都会产生反应。 Accessory Action的意思是只有当用户点击table view cell右边的圆圈箭头按钮时,才会产生的反应。
在这里,我们选择Selection Segue的push选项(大家可以去一个一个选择其他的选项,看看有什么不同),选中后,在view的上方会自动出现一个navigator bar的占位栏,而在table view cell的右边会出现一个大于号箭头,它们2个view直接会有一个箭头相连 ok,现在编译运行一下,看看效果,点击table view中的第一个cell,切换到下面的view,可以看到,view的最上方是navigator bar,bar的左边是一个退回的按钮,按钮中显示的文字是“Seg Nav”,还记得刚才我们在设置Navigation Item时,在其attributes inspector中的Back Button项中留下的文字吗?就是显示在这里的。
点击Seg Nav按钮可以回到上级view。这个操作过程是不是很简单?回忆之前几篇的例子,我们这里没有写过一行代码,而得到的效果是一样的,Storyboard在这方面还是很强大的,它让程序员的全部注意力都集中在了具体的view的开发上,view之间的切换它都帮我们搞定了。
4)创建第二个Segue例子 下面我们为table view中的第二个cell创建一个controller,然后通过segue将他们连接起来。
第二个cell将连接到我们上一篇的例子Simple Storyboard中BIDTaskListController(点击下载),因此我们在这里可以稍微偷懒一下,直接将他们拖入到现在的项目中
接着我们还需要添加一个文件,选中Project navigator中的Seg Nav文件夹,单击鼠标右键,选择“New File...”,在弹出的窗口中,左边选择Cocoa Touch,右边选择Objective-C class,点击Next按钮,在下一个窗口中将class命名为BIDTaskDetailController,Subclass of命名为UIViewController,点击Next按钮,完成创建。
接着选中MainStoryboard.storyboard,从Object library中拖一个Table View Controller到layout area(你可以适当调整view的位置,使其布局美观合理)
选中新添加的Table View Controller,打开identity inspector,将Class设置为BIDTaskListController
猜到我们接下来要做什么了吗?由于BIDTaskListController是从之前的Simple Storyboard复制过来的,因此我们需要和之前一样,在新添加的Table View Controller中放2个cell,设置每个cell的identifier,为每个cell中添加1个Label,然后分别为他们设置tag值,并将第二个Label的颜色设置为红色。
首先在dock中选中第一个cell(现在应该只有唯一一个cell存在),然后打开attributes inspector,将其identifier赋值为“plainCell”,往第一个cell中拖一个Label,左右拉伸Label直至辅助线的位置,选中Label,打开attributes inspector,找到Tag,设值为1。
再选中第一个cell,然后Command+D,复制一个cell,新的cell出现在其下方,选中新的cell,在attributes inspector中将其identifier赋值为“attentionCell”,再选中Label,在attributes inspector将Label的颜色改成红色,设置Tag值仍为1。
在开始连接segue之前,我们还需要添加另外一个view controller,用于显示并修改TaskList中每一项的内容,并和BIDTaskDetailController关联,从Object library中拖一个View Controller到layout area,放在Task List Controller的右边,然后在identity inspector中将其class设置为BIDTaskDetailController
当我们在Task List Controller中选中一个cell后,view会切换到Task Detail Controller,并在里面显示cell的文字,我们在Task Detail Controller中可以对cell的文字进行修改保存,然后再返回Task List Controller。因此我们首先需要在Task Detail Controller中添加一个UITextView,UITextView应该是我们第一次接触到,它是一个多行的文本视图,和C#中的TextBox类似。
在Object library中找到Text View 拖入到Task Detail Controller中,这时Text View会自动填充满整个view的空间
我们并不需要它占满整个空间,因此我们去调成它的高度,使其只占满view的上半部分,因为下半部分会显示虚拟键盘,调整Text View的高度到200(移动上图中底下中间的那个小白方框进行调整,你也可以在Size inspector中进行调整,看个人喜欢了),宽度还是占满整个view的
好了,终于开始代码的部分了,在dock中选中Task Detail Controller 然后点击Assistant editor,这时应该会打开对应的BIDTaskDetailController.h
选中Text View,然后control-drag到BIDTaskDetailController.h,在填出框中,name命名为textView,点击Connect。
好了,下面可以开始连接Segue。选中Table View Controller中的第二个cell,然后control-drag到Task List Controller,在填出框中选择Selection Segue的push选项。然后选择Task List Controller中的第一个cell,control-drag到Task Detail Controller,在填出框中选择Selection Segue的push选项,选择Task List Controller中的第二个cell,control-drag到Task Detail Controller,在填出框中选择Selection Segue的push选项,这样就有2个箭头同时从Task List Controller指向Task Detail Controller,但是他们的意义是不一样的。
整个的MainStoryboard.storyboard的结构如下图所示
编译运行一下程序,所以controller之间都应该可以顺利切换了。
但是我在运行的时候却发现了一个问题,当我点击Sub-menu准备切换到Task List Controller时,发现Task List Controller竟然是空白的一片,而起xcode也报错说:Unknown class BIDTaskListController in Interface Builder file
我打开BIDTaskListController.h,发现UITableViewController的文字颜色是黑色,这就说明xcode没有认出整个类,那就说明肯定是编译器出来问题,没有设置正确
网上查了一圈后发现这种现象是xcode的一个bug,因为BIDTaskListController不是我们从xcode创建的,而是从另外一个项目中拖进来的,因此xcode没有把它包含进编译范畴,因此也就没有去识别里面所包含的类。解决整个问题的方法也很简单,我们选中Project navigator中的Seg Nav项目,然后打开Build Phases,展开Compile Sources,点击+号,把BIDTaskListController.m添加进去就可以了。
在此编译运行,你会发现controller都正确显示了,xcode中的错误也消失了。
下面我们实现一些方法,使每个controller能够正确的工作,打开BIDTaskListController.m,添加如下代码
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { UIViewController *destination = segue.destinationViewController; if ([destination respondsToSelector:@selector(setDelegate:)]) { [destination setValue:self forKey:@"delegate"]; } if ([destination respondsToSelector:@selector(setSelection:)]) { // prepare selection info NSIndexPath *indexPath = [self.tableView indexPathForCell:sender]; id object = self.tasks[indexPath.row]; NSDictionary *selection = @{@"indexPath" : indexPath, @"object" : object}; [destination setValue:selection forKey:@"selection"]; } }
prepareForSegue:sender方法,当tasklist中的任何cell被点击,并准备开始进行view直接的切换时,这个方法会被触发,这样我们可以利用这个方法来传输一些信息给下一个view使用。 UIViewController *destination = segue.destinationViewController; // 通过参数segue,我们可以知道接下来即将显示的controller是哪个,segue还有另外一个参数segue.sourceViewController,这个是表示即将被移除的controller是哪个 if ([destination respondsToSelector:@selector(setDelegate:)]) { // respondsToSelector用来判断是否实现了某些方法,这里是判断在目标controller中是否实现了setDelegate方法 [destination setValue:self forKey:@"delegate"]; } // 这里是KVC(KEY-VALUE CODING)的一个用法,将目标对象中key为delegate的对象赋值为self,当然到目前为止,在BIDTaskDetailController中什么方法都没有实现,是空的
if ([destination respondsToSelector:@selector(setSelection:)]) { //用来判断在目标对象中是否存在setSelection方法 // prepare selection info NSIndexPath *indexPath = [self.tableView indexPathForCell:sender]; // 这里的sender指向的是用户点击的那个cell,通过它来获得cell的indexPath id object = self.tasks[indexPath.row]; // 获取NSArray中的对象 NSDictionary *selection = @{@"indexPath" : indexPath, @"object" : object}; // 创建一个NSDictionary对象,将indexPath和object对象,然后将他们通过KVC的方法传到目标controller中,这里传递indexPath的作用是如果在BIDTaskDetailController中把object的内容给改了,那么传回来的时候,我们就知道改的是哪个cell了,否则们会云里雾里,不知道哪个cell的内容需要更新 [destination setValue:selection forKey:@"selection"]; } // 这里又用到KVC,将目标对象selection的值设置为selection
下面开始修改BIDTaskDetailController.h,打开它,添加如下代码
#import <UIKit/UIKit.h> @interface BIDTaskDetailController : UIViewController @property (weak, nonatomic) IBOutlet UITextView *textView; @property (copy, nonatomic) NSDictionary *selection; @property (weak, nonatomic) id delegate; @end
刚才使用KVC赋值的2个对象在这里定义好了,注意,selection用的是copy
打开BIDTaskDetailController.m,添加如下代码
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.textView.text = self.selection[@"object"]; [self.textView becomeFirstResponder]; }
在viewDidLoad方法中,首先对textView进行赋值,selection在之前的controller中使用KVC进行过赋值,在这里直接获取就可以了,然后调用becomeFirstResponder,呼出虚拟键盘。
ok,再编译运行一下你的程序,看看效果,随便在BIDTaskListController中选择一个cell,然后立刻会跳转到BIDTaskDetailController,而且textView中会显示cell的内容,并且出现虚拟键盘
但是如果现在修改了textView的内容,然后返回,textView中的内容是没有办法保存的,我们还没有实现这个功能,那是不是我们也可以用刚才同样的方法prepareForSegue来传递值呢?很不幸,不可以,因为prepareForSegue只有当将一个controller放到堆栈上面的时候可以使用,如果将一个controller从堆栈上面移除,是无法使用的,这个具有单向性,好吧,那我们只有另寻他法了,在BIDTaskDetailController.m中添加如下代码
- (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; if ([self.delegate respondsToSelector:@selector(setEditedSelection:)]) { // finish editing [self.textView endEditing:YES]; // prepare selection info NSIndexPath *indexPath = self.selection[@"indexPath"]; id object = self.textView.text; NSDictionary *editedSelection = @{@"indexPath" : indexPath, @"object" : object}; [self.delegate setValue:editedSelection forKey:@"editedSelection"]; } }
现在再看这个方法,应该是很熟悉了吧,里面实现的东西还是一样的,只是没有放在prepareForSegue中而已,setEditedSelection等一会会在BIDTaskListController中实现,需要解释的貌似就一个[self.textView endEditing:YES]:停止一切对textView的编辑动作,等对textView的操作都结束后,那么就可以获取它的值,然后返回了。其他的代码都应该可以理解,最后也用到了KVC方法。
最后还是需要对BIDTaskListController进行一些修改,打开BIDTaskListController.m,做如下修改
@interface BIDTaskListController () @property (strong, nonatomic) NSArray *tasks; @property (strong, nonatomic) NSMutableArray *tasks; @property (copy, nonatomic) NSDictionary *editedSelection; @end
我们将tasks对象替换成可修改的Array,然后再声明一个NSDictionary对象editedSelection
- (void)viewDidLoad { [super viewDidLoad]; // Uncomment the following line to preserve selection between presentations. // self.clearsSelectionOnViewWillAppear = NO; // Uncomment the following line to display an Edit button in the navigation bar for this view controller. // self.navigationItem.rightBarButtonItem = self.editButtonItem; self.tasks = @[@"Walk the dog", @"URGENT: Buy milk", @"Clean hidden lair", @"Invent miniature dolphins", @"Find new henchmen", @"Get revenge on do-gooder heroes", @"URGENT: Fold laundry", @"Hold entire world hostage", @"Manicure"]; self.tasks = [@[@"Walk the dog", @"URGENT: Buy milk", @"Clean hidden lair", @"Invent miniature dolphins", @"Find new henchmen", @"Get revenge on do-gooder heroes", @"URGENT: Fold laundry", @"Hold entire world hostage", @"Manicure"] mutableCopy]; }
将数组copy到tasks对象中
- (void)setEditedSelection:(NSDictionary *)dict { if (![dict isEqual:self.editedSelection]) { _editedSelection = dict; NSIndexPath *indexPath = dict[@"indexPath"]; id newValue = dict[@"object"]; [self.tasks replaceObjectAtIndex:indexPath.row withObject:newValue]; [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; } }
if (![dict isEqual:self.editedSelection]) { // 首先判断dictionary对象是否发生了变化,如果是,继续执行代码 _editedSelection = dict; // _editedSelection也是一个objective-c的语法现象,它是隐式的被创建的,在声明 @property (strong, nonatomic) NSMutableArray *tasks;的时候,系统自动声明的了一个对象_editedSelection,你可以发现,这里并没有之前的synthesize方法,这里系统已经帮我们做完了(貌似以后的编程越来越方便了) NSIndexPath *indexPath = dict[@"indexPath"]; // 获取indexPath对象 id newValue = dict[@"object"]; // 获取object对象 [self.tasks replaceObjectAtIndex:indexPath.row withObject:newValue]; // 根据indexPath替换tasks中的对象 [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; } // 重新载入更新过的cell
好了,至此,左右的代码都写完了,我们的这个例子也完成了,编译运行,试着修改一些cell的内容,然后返回,看看是不是变了
好了,所有关于Storyboard的内容都讲完了,是不是觉得还是蛮简单的,是不是觉得以后再遇到Navigation的项目,都会用Storyboard,而不会自己去写繁琐的code进行view直接的切换呢,好吧,如果没有十分的必要,书本上也建议使用Storyboard的,这样我们可以将更多的精力放在功能的实现上,而无需去处理繁琐的切换效果。