斯坦福大学iOS开发公开课总结(六):多态,导航控制器和选项卡栏控制器Demo

本节课的课程地址:控制器多态性、导航控制器、选项卡栏控制器

本节课通过延伸第四节课的纸牌配对Demo(详情请见:斯坦福大学iOS开发公开课总结(三) :纸牌配对游戏Demo)讲解了控制器的多态性,扩展了第五节课的属性字符串Demo(详情请见:斯坦福大学iOS开发公开课总结(四) :属性字符串Demo)讲解了如何使用导航控制器和选项卡栏控制器。

建议读者先了解以上两个博客的内容,因为这样有助于对本节课笔记的理解。

多态


第四节课做的纸牌配对游戏只有一个ViewController,它导入了PlayingCardDek.h类,说明该ViewController只适用于纸牌游戏。如果我们想换一类牌,那么显然目前使用的ViewController是不适用的。

具体看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import "ViewController.h"
#import "PlayingCardDeck.h"

@implementation ViewController

- (CardMatchingGame *)game
{
if (!_game) {
_game = [[CardMatchingGame alloc] initWithCardCount:[self.cardButtons count] usingDeck:[self createDeck]];
}
return _game;
}


- (Deck *)createDeck
{
//实例化了 PlayingCardDeck 类
return [[PlayingCardDeck alloc] init];
}

为了将该ViewController作为通用的控制器,我们需要将它设置为抽象类。通过创造不同的继承它的字类来实现各种各样的纸牌配对游戏。

抽象类

抽象类就是不能被实例化的类,它具有某种普遍性,可以通过继承它来实现基于这种普遍性并带有其他特性的字类。

举个🌰:船是一个具有普遍性的抽象类。基于这个抽象来,如果我们添加了木桨,就可以造一个木桨船;如果我们给它添加了帆,就可以早一个帆船等等。。

因此,在这里,我们需要将纸牌游戏的牌堆抽象出来,如果这个牌堆里是扑克牌,那么这个游戏就是扑克牌配对游戏;如果这个牌堆里的牌是塔罗牌,那么这个牌就是卡洛牌配对游戏。

如何创建抽象类的具体子类?

1. 将该类抽象化:

从上面的代码可以看到,控制器的实例化是基于createDeck方法的(这个方法将扑克牌堆的模型交给了控制器), 如果可以阻止模型的生成就阻止控制器的实例化。因此我们将createDeck的方法的返回值设为nil:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import "ViewController.h"
#import "PlayingCardDeck.h"

@implementation ViewController

- (CardMatchingGame *)game
{
if (!_game) {
_game = [[CardMatchingGame alloc] initWithCardCount:[self.cardButtons count] usingDeck:[self createDeck]];
}
return _game;
}

- (Deck *)createDeck
{
return nil;
}

这样一来,该类无法取得自己的模型实例,就无法正常工作,变得“抽象”。

2. 将需要字类实现的方法放在抽象类的公共API中:

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <UIKit/UIKit.h>
#import "Deck.h"

@interface ViewController : UIViewController

/**
* abstract metod, for subclasses
*
* @return 各种类型不同的纸牌堆
*/
- (Deck *)createDeck;

@end

不难想到,子类将该方法实现的过程就是实例化具有不同特性的具体类的过程!这也就实现了类的多态。

3. 创造继承抽象的子类

新建一个继承于抽象类ViewController的字类PlayingCardViewController

PlayingCardViewController.h

1
2
3
4
5
#import "ViewController.h"

@interface PlayingCardViewController : ViewController

@end

PlayingCardViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import "PlayingCardViewController.h"
#import "PlayingCardDeck.h"

@interface PlayingCardViewController ()

@end

@implementation PlayingCardViewController

//只需要实现
- (Deck *)createDeck
{
return [[PlayingCardDeck alloc] init];
}

@end

这样一来,我们就获得了一个扑克牌配对游戏的ViewController。
将来,如果我们还有别的继承与Deck的牌,就可以用相同的方法:通过创建继承该抽象类并实现createDeck的方法来完成。

多MVC的实现:通过导航控制器管理多个ViewController


导航控制器拥有一个栈数据结构,它可以将多个控制器压入自己的栈结构中来管理这些控制器。我们经常看到的界面滑入滑出的过程就是导航控制器的栈结构在压入弹出控制器的过程。而每个控制器管理一个MVC模型,导航控制器通过管理这些控制器实现了管理多MVC的目的。

需要注意的是:手机的屏幕每次只能显示一个MVC模型的View。在view的切换过程中,手机界面显示的是当前处于导航控制器栈顶的控制器的视图!当该视图被移除界面的时候,该MVC的数据就会被释放。因此,每次要显示一个新的MVC的时候,都会创建一个新的MVC。

控制器在导航控制器的栈结构中弹出

1
2
3
4
5
6
- (void)popViewController
{
//self是当前的控制器,它的navigationController属性指向管理自己的导航控制器
[self.navigationController popViewControllerAnimitaed:YES];

}

跳转到下一个控制器之前执行的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)prepareForSegue:(UIStroyboardSegue *)segue sender: (id)sender
{

//segue的identifier属性用来区分不同的控制器
if ([segue.identifier isEqualToString:@"DoSomething"])
{
//segue的destinationViewController指向的是下一个要滑入界面的控制器
if ([segue.destinationViewController isKindOfClass:[DoSomethingVC class]]){

DoSomethingVC *doVC = (DoSomethingVC *)segue.desitinationViewController;
doVC.infoString = self.infoString;
}

}

}

判断是否可以跳转的方法

1
2
3
4
5
6
7
8
9
10
- (BOOL)shouldPerformSegueWithIdentifier: (NSString *)identifier sender: (id)sender
{

if([segue.identifier isEqualToString:@"DoAParticularThing"])
{
//此方法是用来确定跳转的可行性,因为有时如果缺少下一个界面的数据是不能跳转的
return [self canDoAParticularThing]? YES:NO;
}

}

多MVC Demo

DEMO需求:

  • 在第一个页面可以设置字符属性
  • 点击第一个页面导航栏右侧的按钮跳转到第二个页面
  • 在第二个页面统计第一个页面中添加色彩和边框的字符数量

Demo效果图

|导航控制器|  左:第一个页面;右:第二个页面

|选项卡栏控制器| 左:第一个Tab;右:第二个Tab

重要代码与知识点

跳转页面传值

实现跳转页面传值一共有两个步骤:

  1. 我们先通过在第二个页面的公共API中设置属性
  2. 然后在第一个页面跳转到第二个页面之前将数据赋予第二个页面的这个公共属性实现传值。

1. 在第二个页面设置公共属性

1
2
3
4
5
6
7
#import "ViewController.h"

@interface TextAnylizeViewController : UIViewController

@property (nonatomic, retain) NSAttributedString *textToAnalyze;

@end

2. 在第一个页面跳转到第二个页面之前执行传值

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
//1. 首先判断segue的identifier
if ([segue.identifier isEqualToString:@"Analyze Text"]) {
//2. 然后判断目标控制器的类型
if ([segue.destinationViewController isKindOfClass:[TextAnylizeViewController class]]) {
//3. 在1和2都确定的情况下,实例化第二个页面
TextAnylizeViewController *analyzeVC = (TextAnylizeViewController*)segue.destinationViewController;
//4. 将第一个页面的字符串赋予第二个页面,用于第二个页面的分析
analyzeVC.textToAnalyze = self.body.textStorage;
}
}
}

获取一段字符中,具有某种属性的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (NSAttributedString *)charactersWithAttribute: (NSString *)attributedName
{
NSMutableAttributedString *characters = [[NSMutableAttributedString alloc] init];

NSUInteger index = 0;

while (index < [self.textToAnalyze length]) {

NSRange range;

//查找一段字符串中,具有某种相同属性的值
id value = [self.textToAnalyze attribute:attributedName atIndex:index effectiveRange:&range];

if (value) {
//如果值存在,获取具有该相同属性的字符串
[characters appendAttributedString:[self.textToAnalyze attributedSubstringFromRange:range]];

//将index移动到具有该相同属性的字符串的下一位
index = range.location + range.length;

}else{
index ++;
}
}

return characters;
}

搭建导航控制器(Navigation Controller)

导航控制器

搭建选项卡栏控制器(Tab Bar Controller)

选项卡栏控制器

最后的话


如果哪位小伙伴想拿到本文Demo的代码请不要客气,在评论里留言即可。
而且十分欢迎给笔者的代码和文笔抛出宝贵的意见和建议~

本文为笔者原创,如需转载,请事先与笔者交涉~

2016.7.12日更新:


笔者已经把目前为止整理的所有Demo(第二课到第十课)放入到了我的GitHub仓库里。分为英文注释版和中文注释版(英文注释要少一点,嘿嘿)想要的小伙伴可以果断下载~ 如果有不知道怎么下载的小伙伴请联系我~

————————————————- 2018年7月17日更新 ————————————————-

注意注意!!!

笔者在近期开通了个人公众号,主要分享编程,读书笔记,思考类的文章。

  • 编程类文章:包括笔者以前发布的精选技术文章,以及后续发布的技术文章(以原创为主),并且逐渐脱离 iOS 的内容,将侧重点会转移到提高编程能力的方向上。
  • 读书笔记类文章:分享编程类思考类心理类职场类书籍的读书笔记。
  • 思考类文章:分享笔者平时在技术上生活上的思考。

因为公众号每天发布的消息数有限制,所以到目前为止还没有将所有过去的精选文章都发布在公众号上,后续会逐步发布的。

而且因为各大博客平台的各种限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~

扫下方的公众号二维码并点击关注,期待与您的共同成长~

公众号:程序员维他命

坚持原创技术分享,您的支持将鼓励我继续创作!