本节课知识点内容不多,主要是延续了上一节课翻单张纸牌的游戏(详情请见:斯坦福大学iOS开发公开课总结(二) :翻纸牌Demo),将一张纸牌扩展到一个多张纸牌并进行配对和打分的小游戏。
本节课的内容虽然简单,但是十分重要,讲师强调了MVC的设计原则并实际运用到了代码中,本文就Demo的具体代码来讲解本节课提到的知识点。
设计需求
- 显示多张纸牌,点击任意一张牌可以翻牌。
- 两张牌都显示正面后可以进行配对:
- 花色匹配得1分;数字匹配得4分,匹配后,两张牌切换为不可点击状态(置灰)。
- 都不匹配扣2分。
- 每次翻牌都减一分。
- 每次翻牌都要更新分数。
效果图:
左:初始界面 ;右:游戏中界面
重要代码段与知识点
模型类:CardMatchingGame
1. 在公共接口设置只读属性
//CardMatchingGame.h
@property (nonatomic, readonly) NSInteger score;
//CardMatchingGame.m
@property (nonatomic, readwrite) NSInteger score;
在.h文件中将分数属性设置为只读,并在.m文件中设定该属性为读写,以便在内部计算。
原因:不希望其他类更改此属性,只能获取该属性。通俗一点地说:“你们就拿我给你算好的分数就好了,你们是不能更改它的!”
2. 指定初始化器:Designated initializer
- (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;
}
有些时候,我们需要在类实例化的时候就要求对象持有某些数据,这就需要设计指定初始化器,因为原始的初始化方法
-(instancetype)init
方法是无法让实例对象持有非零数据的(初始化后,基本数据类型属性=0;对象属性=nil)。
在这段代码里,该模型类通过数量
count
和一堆纸牌deck
中拿到了自己持有的数组self.cards
。
举个🌰 :想要从一个有52张牌的堆里抽取了12张牌来作为自己的一堆纸牌的话,就要设置Deck为具有52张牌的数组;而设置count为12即可。
3. 设定常量
#define MISMATCH_PENALTY 2 //简单的替换,不具有数据类型
static const int MISMATCH_PENALTY = 2; //非简单替换,具有数据类型
控制器类:CardMathcingGameViewController
1. 接收来自View的点击事件并更新UI
/**
* 接收用户的点击事件
*
* @param sender 点击的按钮对象
*/
- (IBAction)touchCardButton:(UIButton *)sender {
//1. 找到界面中所点击的按钮index
NSInteger cardIndex = [self.cardButtons indexOfObject:sender];
//2. 找到模型中相同index的纸牌数据,并判断是否匹配,计算分数
[self.game chooseCardAtIndex: cardIndex];
//3. 更新UI
[self updateUI];
}
/**
* 更新UI
*/
- (void)updateUI
{
//1. 更新view上所有牌面
for (UIButton *cardButton in self.cardButtons) {
//1.1 找到界面中的一张纸牌(按照枚举的顺序)
NSInteger cardIndex = [self.cardButtons indexOfObject:cardButton];
//1.2 找到模型中对应的纸牌数据
Card *card = [self.game cardAtIndex:cardIndex];
//1.3 根据数据更新纸牌的UI和可点击性
[cardButton setTitle:[self titleForCard:card] forState:UIControlStateNormal];
[cardButton setBackgroundImage: [self backgroundImageForCard:card] forState:UIControlStateNormal];
cardButton.enabled = !card.isMatched;
}
//2. 更新分数
self.scoreLabel.text = [NSString stringWithFormat:@"Score: %ld", (long)self.game.score];
}
在这里,我们可以很容易看到MVC的工作流程:
- 在View里发生了点击事件并通知给了Controller。
- Controller告诉Model发生了点击。
- Model根据点击事件更新自己,然后将更新后的自己告诉Controller。
- Controller根据更新后的模型去更新UI。
这里笔者有一张自己画的图,略逗逼,掩面贴出,独乐乐不如众乐乐~
MVC流程图.png零散知识点
1. 在数组里传入其包含的对象返回其所在序号
NSInteger cardIndex = [self.cardButtons indexOfObject:cardButton];
2. 在数组中找到第一个元素
//应该使用:
PlayingCard *otherCard = [otherCards firstObject];
//不应该使用:
PlayingCard *otherCard = otherCards[0];
//不应该使用:
PlayingCard *otherCard = [otherCards objectAtIndex:0];
应该使用第一种情况。
因为如果数组为空,那么如果使用第一种情况会返回nil,而向nil发送消息是不会造成崩溃的。
但是如果使用后两种方法,一旦数组为空,就会立刻造成程序崩溃!而且同样适用与取数组的最后一个元素的情况。
在数组中找到最后一个元素:
PlayingCard *otherCard = [otherCards lastObject];
3. 在数组中是否包含某元素
if ([ [PlayingCard ValidSuits] containsObject:suit]) {
_suit = suit;
}
containsObject:是NSArray的方法,返回布尔值,用来判断是否包含某个元素。
4. JPG格式图片的读取
- (UIImage *)backgroundImageForCard: (Card *)card
{
//默认是png,如果是jpg需要加上.jpg的后缀
return [UIImage imageNamed:card.isChosen? @"cardFront.jpg":@"CardBack.png"];
}
如果使用
imageNamed:
方法,仅传入jpg格式的文件名是无法显示出图片的,应该讲后缀.jpg
拼接后传入才可以哦~而相同情况下,若要显示png
格式的图片的话是不需要另外加后缀的。
最后的话:
如果哪位小伙伴想拿到此Demo的代码请不要客气,在评论里留言即可。
而且十分欢迎给笔者的代码和文笔抛出宝贵的意见和建议~
本文为笔者原创,如需转载,请事先与笔者交涉~
2016.7.12日更新:
笔者已经把目前为止整理的所有Demo(第二课到第十课)放入到了我的GitHub仓库里。分为英文注释版和中文注释版(英文注释要少一点,嘿嘿)想要的小伙伴可以果断下载~ 如果有不知道怎么下载的小伙伴请联系我~
本文已在版权印备案,如需转载请访问版权印。48422928
网友评论
首先是概念的问题:matching card应该是在chosenCards中完成的。而不应该是在遍历一遍全局牌堆。
再次效率也低下:每次都遍历全局,如果不是12牌,而是100牌呢?
最后就是不能一般化解决N-cards matching。
学习