斯坦福大学IOS7开发课程3
课程3主要内容是继续之前的纸牌游戏。上节课实现的纸牌游戏App只是实现了简单的纸牌翻转,这节课使其变得真正可玩。
主要分为三个部分:
- 先回顾上节课编写的App
- 对上节课的App改进
- 使游戏变得可玩
上节课实现的纸牌翻转App
- 将背景颜色设置为绿色 - 打开 - Main.storyboard,选中View后,在属性栏里将- Background的颜色设置为绿色。
 
- 添加纸牌正反面的图片 - 在网上获取纸牌正反面图片,比如: 
 
 
   选中Assets.xcassets,将两张图片拖至其中。
 
- 添加按钮,并将按钮的背景图片改为纸牌正面图片(CardFront), 文字改为“♠️A”
 
- 添加一个 - Label,显示纸牌翻转的次数
- 为按钮添加“Touch Up Inside”事件,命名为 - touchCardButton
- 为 - Label在- ViewController.m的- @interface ViewController() @end之间添加“Referencing Outlets“,命名为- flipsLabel。
 
- 另外,在 - ViewController.m的- @interface ViewController() @end之间添加int属性- flipCount
- 在 - ViewController.m中实现touchCardButton事件函数- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13- - (IBAction)touchCardButton:(UIButton *)sender { 
 if ([sender.currentTitle length]) {
 [sender setBackgroundImage:[UIImage imageNamed:@"CardBack"] forState:UIControlStateNormal];
 [sender setTitle:@"" forState:UIControlStateNormal];
 } else {
 Card *card = [self.deck drawRandomCard];
 if(card) {
 [sender setBackgroundImage:[UIImage imageNamed:@"CardFront"] forState:UIControlStateNormal];
 [sender setTitle:@"A♣️" forState:UIControlStateNormal];
 }
 }
 self.flipCount++;
 }
- 重构属性 - flipCount的- setter函数。- 1 
 2
 3
 4
 5- - (void)setFlipCount:(int)flipCount 
 {
 _flipCount = flipCount;
 self.flipsLabel.text = [NSString stringWithFormat:@"Flips: %d",self.flipCount];
 }
此时App完成,代码如下:
| 1 | // ViewController.h | 
| 1 | // ViewController.m | 
最终效果如下:
 
对该App改进
上节课完成的App只能实现纸牌的翻转,每次翻转显示的纸牌都是同一张。那么如何让每次翻转,随机显示牌堆中的一张纸牌呢?其实也很简单,方法如下:
- 在 - ViewController.m中引入- Deck.h
- 在 - ViewController.m的- @interface ViewController() @end之间添加- Deck*属性- deck
- 在 - ViewController.m中引入- PlayingCardDeck.h
- 修改属性 - deck的- getter,当- _deck为空时,通过- [[PlayingCardDeck alloc]init]创建牌堆,然后再返回Deck。- 此时 - ViewController.m的代码如下:- 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
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49- // ViewController.m 
 #import "ViewController.h"
 #import "Deck.h"
 #import "PlayingCardDeck.h"
 @interface ViewController ()
 @property (weak, nonatomic) IBOutlet UILabel *flipsLabel;
 @property (nonatomic) int flipCount;
 @property (strong, nonatomic) Deck *deck;
 @end
 @implementation ViewController
 - (void)viewDidLoad {
 [super viewDidLoad];
 // Do any additional setup after loading the view.
 }
 - (Deck *)deck
 {
 if(!_deck)
 _deck = [self createDeck];
 return _deck;
 }
 - (Deck *)createDeck
 {
 return [[PlayingCardDeck alloc]init];
 }
 - (IBAction)touchCardButton:(UIButton *)sender {
 if ([sender.currentTitle length]) {
 [sender setBackgroundImage:[UIImage imageNamed:@"CardBack"] forState:UIControlStateNormal];
 [sender setTitle:@"" forState:UIControlStateNormal];
 } else {
 [sender setBackgroundImage:[UIImage imageNamed:@"CardFront"] forState:UIControlStateNormal];
 [sender setTitle:@"A♣️" forState:UIControlStateNormal];
 }
 self.flipCount++;
 }
 - (void)setFlipCount:(int)flipCount
 {
 _flipCount = flipCount;
 self.flipsLabel.text = [NSString stringWithFormat:@"Flips: %d",self.flipCount];
 }
 @end
- 在 - touchCardButton函数中,利用- deck的- drawRandomCard函数随机抽取一张纸牌。
- 将随机抽取出的Card的内容显示出来。需要注意的是,牌堆里的牌抽完以后要停止抽牌。 
完成后ViewController.m的代码如下:
| 1 | // ViewController.m | 
效果如下:
 
使该游戏变得真正可玩
接下来我们让游戏变得真正可玩儿。在MVC开发模型中,游戏的逻辑属于Model,Model和UI是完全独立的,所以在编写过程中不需要考虑UI。
Model开发
首先添加一个新的类CardMatchingGame,这个类就是MVC模型中的Model,在编码CardMathingGame的过程中,需要思考需要哪些公开的API。
- 首先是构造函数,因为需要传进一些参数,比如牌的张数,所以新建构造函数initWithCardCount,指定构造函数(designated initializer)
- 公开的属性 score,但同时我们不希望别人能够随意修改socre属性,因此需要在公开API中将其设置为readonly,同时在.m文件中的私密API中声明为readwrite。其实readwrite用的不多,因为默认情况下就是readwrite,只有在公开API只读的时候才会用到。
- 允许用户通过index选中纸牌chooseCardAtIndex
- 通过index从牌堆中获取Card,CardAtIndex
| 1 | // CardMatchingGame.h | 
接下来开始在CardMatchingGame.m中开始实现:
- 利用关键字 - readwrite使得属性score在- CardMatchingGame.m中可写。- 1 
 2
 3- @interface CardMatchingGame() 
 @property (nonatomic, readwrite) NSInteger score;
 @end
- 新建属 - cards,用于存储游戏中的纸牌。- 1 
 2
 3
 4- @interface CardMatchingGame() 
 @property (nonatomic, readwrite) NSInteger score;
 @property (nonatomic, strong) NSMutableArray *cards; // of Card
 @end
- 修改属性 - cards的- getter函数- 属性的默认初始值为nil,当属性 - cards的指针为nil时,创建一个- NSMutableArray- 1 
 2
 3
 4
 5
 6
 7- -(NSMutableArray *)cards 
 {
 if(!_cards) {
 _cards = [[NSMutableArray alloc] init];
 }
 return _cards;
 }
- 实现构造函数 - initWithCardCount- 这个构造函数是designated initializer,也就是说使用这个类的用户必须调用这个构造函数,否则类无法被正确的初始化。使用构造函数init会返回nil。这个信息要传递给用户,所以以注释的形式写在公开API中 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16- -(instancetype) initWithCardCount:(NSUInteger)count usingDeck:(Deck *)deck 
 {
 self = [super init];
 if(self) {
 for(int i = 0;i < count;i++) {
 Card *card = [deck drawRandomCard];
 if(card) {
 [self.cards addObject:card];
 } else {
 self = nil;
 break;
 }
 }
 }
 return self;
 }
- 实现函数 - cardAtIndex。- 1 
 2
 3
 4- - (Card *)cardAtIndex:(NSUInteger)index 
 {
 return index < [self.cards count] ? self.cards[index] : nil;
 }
- 实现函数 - chooseCardAtIndex- 这个函数是游戏的关键。表示某个card被选中后的处理方式,包括匹配过程 - 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
 28
 29
 30
 31
 32
 33
 34- static const int PENALTY_SCORE = 1; 
 static const int COST_OF_CHOOSE = 1;
 -(void)chooseCardAtIndex:(NSUInteger)index
 {
 Card *card = [self cardAtIndex:index];
 if(!card.isMatched) {
 // if choosen card is not matched
 if(card.isChosen) { // if the card is already chosen
 card.chosen = NO; // filp the card back
 } else { // match it against another card
 // here, only match two cards, but may make it match multiple cards in later
 for(Card *otherCard in self.cards) {
 if(otherCard.isChosen && !otherCard.isMatched) { // Bingo, find the card we want to match
 int matchScore = [card match:@[otherCard]]; // try to match it
 if(matchScore == 0) { // not match
 self.score -= PENALTY_SCORE; // penalty of un match
 otherCard.chosen = NO; // flip the other card back
 } else { // match
 self.score += matchScore;
 card.matched = YES;
 otherCard.matched = YES;
 }
 break;
 }
 }
 card.chosen = YES;
 self.score -= COST_OF_CHOOSE;
 }
 } else {
 // if choosen card is already matched
 // do nothing
 }
 }
UI开发
到目前,完成了App中Model的实现,接下来是实现App的UI。
- 在UI中创建多张纸牌,可以通过复制粘贴的方式完成 
- 创建OutletCollections,添加所有纸牌。 - 创建方法,右键点击某个button,按住“New Referencing Outlet Collections“,拖到 - ViewController.m中。- 添加其他button的方法,按住Ctrl键,点击button并拖到 - ViewController.m中
 
- 在 - ViewController.m中引入- CardMatchingGame.h
- 新增 - CardMatchingGame的属性- game- @property (strong, nonatomic) CardMatchingGame *game;
- 重写属性 - game的- getter- 1 
 2
 3
 4
 5
 6- - (CardMatchingGame *)game{ 
 if(!_game) {
 _game = [[CardMatchingGame alloc] initWithCardCount:[self.cardButtons count] usingDeck:[self createDeck]];
 }
 return _game;
 }
- 在 - ViewController.m中创建函数- titleForCard- 1 
 2
 3
 4- - (NSString *)titleForCard:(Card *)card 
 {
 return card.isChosen ? card.contents : @"";
 }
- 在 - ViewController.m中创建函数- backgroundImageForCard- 1 
 2
 3
 4- - (UIImage *)backgroundImageForCard:(Card *)card 
 {
 return [UIImage imageNamed:card.isChosen ? @"CardFront" : @"CardBack"]; // CardFront & CardBack are name of card image
 }
- 在 - ViewController.m中创建函数- updateUI用于同步Model和UI。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10- - (void)updateUI 
 {
 for (UIButton *cardButton in self.cardButtons) {
 int cardIndex = [self.cardButtons indexOfObject:cardButton];
 Card *card = [self.game cardAtIndex:cardIndex];
 [cardButton setTitle:[self titleForCard:card] forState:UIControlStateNormal];
 [cardButton setBackgroundImage:[self backgroundImageForCard:card] forState:UIControlStateNormal];
 cardButton.enabled = !card.isMatched;
 }
 }
- 创建Touch Up Inside事件,命名为touchCardButton,并将所有button加入其中,方法类似于步骤2. 
- .m文件中实现- touchCardButton- 1 
 2
 3
 4
 5- - (IBAction)touchCardButton:(UIButton *)sender { 
 int cardIndex = [self.cardButtons indexOfObject:sender];
 [self.game chooseCardAtIndex:cardIndex];
 [self updateUI];
 }
到这个时候App已经可以运行了,运行效果如下:
 
可以正常运行,但是两张黑桃却没有正常匹配,问题出在哪里?
问题在我们在课程2中实现的Card类中的匹配函数:
| 1 | // Card.m | 
这显然不是一个完整的匹配逻辑。
所以需要在PlayingCard类的.m文件中重构match函数。
| 1 | - (int)match:(NSArray *)otherCards | 
现在完成了匹配规则,最后一个任务是添加一个Label用于展示分数。
- 在UI中增加一个Label 
- 为Label增加一个Outlet 
- 在 - updateUI中更新score Label。- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- - (void)updateUI 
 {
 for (UIButton *cardButton in self.cardButtons) {
 int cardIndex = [self.cardButtons indexOfObject:cardButton];
 Card *card = [self.game cardAtIndex:cardIndex];
 [cardButton setTitle:[self titleForCard:card] forState:UIControlStateNormal];
 [cardButton setBackgroundImage:[self backgroundImageForCard:card] forState:UIControlStateNormal];
 cardButton.enabled = !card.isMatched;
 }
 self.scoreLabel.text = [NSString stringWithFormat:@"Score: %d", self.game.score];
 }
 
至此,一个可玩的纸牌匹配游戏App已经完成,大家可以试着让App可以同时匹配三张及以上的纸牌。