三十而立,从零开始学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 (strongnonatomicNSMutableArray *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的,这样我们可以将更多的精力放在功能的实现上,而无需去处理繁琐的切换效果。

 

Seg Nav

 

 

 

 

 

posted @ 2013-04-11 21:31  minglz  阅读(9898)  评论(13编辑  收藏  举报