IOS的MVC

时间:2022-04-23 19:23:57

1 翻牌游戏

1.1 问题

根据苹果MVC设计模式的思想原则实现一个简单的翻牌游戏,功能如下:

1)界面上随机摆放12张背面朝上的纸牌,界面效果如图-1所示:

IOS的MVC

图- 1

2)点击纸牌可以使纸牌翻页,翻牌后进行数字和花色的匹配,如果数字一样得4分,花色一样得1分;

3)在界面的左下角有一个记录得分的标签,界面如图-2所示:

IOS的MVC

图- 2

1.2 方案

首先使用Xcode创建一个带有xib的项目,在xib界面拖放12个UIButton对象和一个计分的UILabel对象,因为纸牌可以点击,所以使用UIButton控件作为纸牌对象,并且在检查器中设置好纸牌的背景图片。

其次根据苹果IOS开发的MVC原则,将整个案例的类分为三个群组,即Model层——用来保存游戏的纸牌数据和分数计算,View层——保存游戏的界面(xib文件)以及Controller层——控制程序的流程,协调View层和Model层。

然后创建TRCard类(纸牌类)用来管理纸牌的数据,TRDeck类(牌桌类)管理发牌的规则,TRCardGame类(游戏类)管理整个游戏的逻辑,这三个类都属于Model层,分别给这三个类增加属性和方法,实现各自管理的数据和逻辑。

最后在TRCardGameController类里面实现Model层和View层的通信逻辑。

1.3 步骤

实现此案例需要按照如下步骤进行。

步骤一:搭建游戏界面

首先在xib界面拖放12个UIButton对象和一个计分的UILabel对象,因为纸牌可以点击这是使用UIButton对象作为纸牌。

其次给纸牌设置背景图片,选中纸牌的背景图片拖放到Xcode导航栏的View群组里面,会弹出如图-3所示对话框,在Copy items if needed选项前的复选框打钩,表明将图片拷贝到项目中:

IOS的MVC

图-3

点击Finish按钮之后,可以在导航栏的View群组里面看见添加进来的两张图片,名字分别为cardback.png和cardfront.png,分别是纸牌正面图片和背面图片,如图-4所示:

IOS的MVC

图-4

然后在右边栏的第四个检查器里面设置纸牌的背景图,初始界面将按钮的背景图设置为纸牌的背面图片cardback.png,如图-5所示:

IOS的MVC

图-5

步骤二:创建TRCard类、TRDeck类和TRCardGame类

创建Model层的三个类,TRCard,TRDeck和TRGame,这三个类全都继承至NSObject,如图-6所示:

IOS的MVC

图-6

其次在TRCard类里面声明NSString类型的用来表示纸牌内容的属性content,而每张牌由级别和花色组成,因此再声明两个属性rank和suit,分别用来表示级别和花色,在这里级别使用NSUInteger类型,花色使用特殊的字符表示,因此是NSString类型,代码如下所示:

  1. //这张牌的内容, 如:"♣A"
  2. @property (nonatomic, strong, readonly)NSString *content;
  3. //纸牌花色♠♥♣♦
  4. @property (nonatomic, strong)NSString *suit;
  5. //纸牌级别
  6. @property (nonatomic) NSUInteger rank;

然后再声明两个BOOL类型的属性用来表示纸牌的两个状态,chosen表示是否被选中状态,matched表示是否被匹配状态,代码如下所示:

  1. @property (nonatomic, getter=isChosen) BOOL chosen;
  2. @property (nonatomic, getter=isMatched) BOOL matched;

在这里定义属性使用了getter关键字,这是一种开发习惯,将BOOL类型属性的getter方法进行重新命名,表达更清楚明确,在这里自动生成的两个属性的getter方法名为isChosen和isMatched。

实际的开发中为了提高代码的效率,根据程序的需要通常会重写属性的setter和getter方法,属性suit只能接受♠♥♣♦这四种花色,赋值其他字符是非法的,为了保护suit属性,可以在suit的setter方法里面增加以下限制代码,如下所示:

  1. + (NSArray *)validSuits
  2. {
  3. return @[@"♠", @"♥", @"♣", @"♦"];
  4. }
  5. - (void)setSuit:(NSString *)suit
  6. {
  7. //判断传入的参数是否合法
  8. if([[TRCardvalidSuits] containsObject:suit]){
  9. _suit = suit;
  10. }
  11. }

以上代码中validSuits方法可以在TRCard.h文件中公开方便外部使用,否则外部可能不知道哪些是合法字符。同样的为了保护suit属性在getter方法里面也可以增加一些限制的代码,如下所示:

  1. -(NSString *)suit
  2. {
  3. //判断_suit实例变量是否为空,如果为空则返回?,保证_suit不会为空
  4. return _suit ? _suit : @"?";
  5. }

此时需要注意,重写完setter和getter方法之后系统不会再自动生成实例变量_suit,因此需要使用@synthesize关键字来生成实例变量_suit,代码如下所示:

  1. @synthesize suit = _suit;

同样为了保护rank属性,也可以将rank的setter方法和getter方法重写,代码如下所示:

  1. - (void)setRank:(NSUInteger)rank
  2. {
  3. //纸牌一共有13个级别
  4. if(rank <= 13){
  5. _rank = rank;
  6. }
  7. }

纸牌的内容是由花色和级别组成,因此重写content的getter方法即可,每次调用getter方法时即可获取到纸牌的显示内容,代码如下所示:

  1. + (NSArray *)randStrins
  2. {
  3. //纸牌13个级别对应的字符串,由小到大放入数组
  4. return @[@"?", @"A", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"10", @"J", @"Q", @"K"];
  5. }
  6. - (NSString *)content
  7. {
  8. NSString *rankString = [TRCardrandStrins][self.rank];
  9. return [self.suitstringByAppendingString:rankString];
  10. }

最后在TRCard类里面增加一个公开的方法,用于获取纸牌的最大级别,方便其他对象使用,代码如下所示:

  1. //返回级别的最大值
  2. + (NSUInteger)maxRank
  3. {
  4. return [[TRCardrandStrins] count] - 1;
  5. }

步骤三:TRDeck类添加属性和方法

TRDeck类主要是管理发牌的规则,既然是发牌,就需要拥有一个纸牌数组的属性,因此声明一个NSMutableArray类型的私有属性cards,使用懒汉模式(延迟加载)给实例变量_cards进行初始化,代码如下所示:

  1. //声明属性
  2. @property (nonatomic, strong) NSMutableArray *cards;
  3. //重写setter方法初始化_card实例变量
  4. - (NSMutableArray *)cards
  5. {
  6. if(!_cards)_cards = [[NSMutableArrayalloc]init];
  7. return _cards;
  8. }

然后在TRDeck的初始化方法里面按照纸牌的规则给_card数组添加纸牌对象,一共52个纸牌对象,代码如下所示:

  1. - (instancetype)init
  2. {
  3. self = [super init];
  4. if (self) {
  5. for (NSString *suit in [TRCardvalidSuits]) {
  6. for(NSUInteger rank = 1; rank<=[TRCardmaxRank]; rank++){
  7. //创建纸牌对象
  8. TRCard *card = [[TRCardalloc]init];
  9. card.suit = suit;
  10. card.rank = rank;
  11. //将纸牌对象添加的纸牌数组中
  12. [self.cardsaddObject:card];
  13. }
  14. }
  15. }
  16. return self;
  17. }

最后实现随机发牌的方法randomCard,此方法需要在TRDeck.h文件中公开,代码如下所示:

  1. - (TRCard *)randomCard
  2. {
  3. //使用随机函数,随机出一个数组下标,
  4. unsignedint index = arc4random() % self.cards.count;
  5. //根据随机出的数组下标从数组中获取到纸牌对象
  6. TRCard *card = self.cards[index];
  7. //从纸牌数值将刚才获取的纸牌对象移除
  8. [self.cardsremoveObjectAtIndex:index];
  9. return card;
  10. }

步骤四:TRCardGame类添加属性和方法

TRCardGame类是用来管理整个游戏的逻辑,分析本案例主要需要实现纸牌匹配和计算分数的逻辑,因此首先声明一个NSInteger的属性score,此时需要注意分数只能由TRCardGame类进行计算和修改,外部对该属性只能读取不能修改,因此在TRCardGame.h文件中声明一个只读的score属性,在TRCardGame.m文件中声明一个可读写的score属性,代码如下所示:

  1. //TRCardGame.h文件中
  2. @property (nonatomic, readonly) NSInteger score;
  3. //TRCardGame.m文件中
  4. @property (nonatomic, readwrite) NSInteger score;

其次TRCardGame需要随机产生12张纸牌,所以需要声明一个NSMutableArray类型的属性cards,同样在setter方法中进行实例变量_cards初始化,代码如下所示:

  1. //声明属性
  2. @property (nonatomic, strong) NSMutableArray *cards;
  3. //重写setter方法初始化_card实例变量
  4. - (NSMutableArray *)cards
  5. {
  6. if(!_cards)_cards = [[NSMutableArrayalloc]init];
  7. return _cards;
  8. }

然后自定义一个TRCardGame类的初始化方法initWithCardCount:usingDeck:,此方法用来初始化TRCardGame对象,此方法中使用一个TRDeck对象随机出12张纸牌,此时需要注意初始化方法通常都是需要公开在.h文件中进行声明,代码如下所示:

  1. - (instancetype)initWithCardCount:(NSUInteger)count usingDeck:(TRDeck *)deck
  2. {
  3. self = [super init];
  4. if (self) {
  5. for(inti=0; i<count; i++){
  6. TRCard *card = [deck randomCard];
  7. [self.cardsaddObject:card];
  8. }
  9. }
  10. return self;
  11. }

最后实现纸牌的匹配逻辑,定义一个动态方法chooseCardAtIndex:,当用户选中某张牌是调用此方法,其中TRCard对象的匹配判断应有TRCard类提供,同样此方法需要在.h文件中公开,代码如下所示:

  1. TRCardGame类中的代码:
  2. //根据下标返回指定扑克牌
  3. - (TRCard *)cardAtIndex:(NSUInteger)index
  4. {
  5. return index<self.cards.count ? self.cards[index] : nil;
  6. }
  7. //用户选中了一张牌
  8. - (void)chooseCardAtIndex:(NSUInteger)index
  9. {
  10. //进行匹配
  11. TRCard *card = [self cardAtIndex:index];
  12. if(![card isMatched]){
  13. if([card isChosen]){
  14. card.chosen = NO;//再翻回去
  15. }else{//没有匹配,也没有在正面
  16. //匹配其他翻过来的牌
  17. for (TRCard *otherCard in self.cards) {
  18. if([otherCardisChosen] && ![otherCardisMatched]){
  19. int score = [card match:otherCard];
  20. if(score){//匹配成功
  21. self.score += score;
  22. card.matched = YES;
  23. otherCard.matched = YES;
  24. }else{//匹配失败
  25. otherCard.chosen = NO;
  26. }
  27. }
  28. }
  29. //把牌翻过来
  30. card.chosen = YES;
  31. }
  32. }
  33. }
  34. TRCard类中的代码:
  35. - (int)match:(TRCard *)otherCard
  36. {
  37. int score = 0;
  38. if(self.rank == otherCard.rank) score = 4;
  39. else if(self.suit == otherCard.suit) score = 1;
  40. return score;
  41. }

步骤五:通过TRCardGameController实现View和Model层的通信

第一步已经搭建好了界面,此时需要将xib中的对象关联到TRCardGameController中,首先关联计分UILabel对象,以拉线的方式关联成TRCardGameController的私有属性scoreLabel,代码如下所示:

  1. @property (weak, nonatomic) IBOutletUILabel *scoreLabel;

其次关联12个纸牌对象,选中一张纸牌按住control键,往TRCardGameController.m文件的类扩展中拖,在弹出的对话框Connection选项中选择Outlet Collection,如图-7所示:

IOS的MVC

图-7

释放鼠标会自动生成一个NSArray类型的属性cardButtons,该数组里面的元素指向xib中的一个对象,选中代码前的是新圆圈,依次关联xib上的其他11张纸牌,这样_cardButtons数组里面的元素指向12个xib创建的纸牌对象,如图-8所示:

IOS的MVC

图-8

然后将12个纸牌对象以拉线的方式关联同一个IBAction方法touchCardButton:,每当选择纸牌时需要进行纸牌匹配的逻辑判断计算得分,这件事情需要TRCardGame对象去实现,因此TRCardGameController类需要拥有一个TRCardGame类的私有属性以及一个TRDeck类的私有属性,声明属性并且进行初始化,代码如下所示:

  1. @property (nonatomic, strong) TRDeck *deck;
  2. @property (nonatomic, strong) TRCardGame *game;
  3. - (TRDeck *)deck
  4. {
  5. if(!_deck)_deck = [[TRDeckalloc]init];
  6. return _deck;
  7. }
  8. - (TRCardGame *)game
  9. {
  10. if(!_game)_game = [[TRCardGamealloc]initWithCardCount:self.cardButtons.countusingDeck:self.deck];
  11. return _game;
  12. }

然后在touchCardButton:方法里通过TRCardGame对进行纸牌的匹配逻辑计算,代码如下所示:

  1. //点击按钮计算纸牌的匹配逻辑
  2. - (IBAction)touchCardButton:(UIButton *)sender
  3. {
  4. NSUIntegerchooseCardIndex = [self.cardButtonsindexOfObject:sender];
  5. [self.gamechooseCardAtIndex:chooseCardIndex];
  6. }

最后通过TRCardGame对象得出的匹配的结果更新界面,代码如下所示:

  1. //更新整个界面
  2. - (void)updateUI
  3. {
  4. for (UIButton *cardButton in self.cardButtons) {
  5. NSUInteger index = [self.cardButtonsindexOfObject:cardButton];
  6. TRCard *card = [self.gamecardAtIndex:index];
  7. [cardButtonsetTitle:[self titleForCard:card] forState:UIControlStateNormal];
  8. [cardButtonsetBackgroundImage:[self imageForCard:card] forState:UIControlStateNormal];
  9. cardButton.enabled = ![card isMatched];
  10. self.scoreLabel.text = [NSStringstringWithFormat:@"Score:%d", self.game.score];
  11. }
  12. }
  13. //根据纸牌选中的状态更改纸牌背景图片
  14. - (UIImage *)imageForCard:(TRCard *)card
  15. {
  16. if([card isChosen]){
  17. return [UIImageimageNamed:@"cardfront.png"];
  18. }else{
  19. return [UIImageimageNamed:@"cardback.png"];
  20. }
  21. }
  22. //根据纸牌选中的状态更改纸牌的title
  23. - (NSString *)titleForCard:(TRCard *)card
  24. {
  25. return [card isChosen] ? card.content : @"";
  26. }
  27. //点击按钮更新界面
  28. - (IBAction)touchCardButton:(UIButton *)sender
  29. {
  30. NSUIntegerchooseCardIndex = [self.cardButtonsindexOfObject:sender];
  31. [self.gamechooseCardAtIndex:chooseCardIndex];
  32. [selfupdateUI];
  33. }

1.4 完整代码

本案例中,TRCardGameViewController.m文件中的完整代码如下所示:

 
  1. #import "TRCardGameViewController.h"
  2. #import "TRCardGame.h"
  3. @interfaceTRCardGameViewController ()
  4. @property (nonatomic, strong) TRDeck *deck;
  5. @property (nonatomic, strong) TRCardGame *game;
  6. @property (strong, nonatomic) IBOutletCollection(UIButton) NSArray *cardButtons;
  7. @property (weak, nonatomic) IBOutletUILabel *scoreLabel;
  8. @end
  9. @implementationTRCardGameViewController
  10. - (TRDeck *)deck
  11. {
  12. if(!_deck)_deck = [[TRDeckalloc]init];
  13. return _deck;
  14. }
  15. - (TRCardGame *)game
  16. {
  17. if(!_game)_game = [[TRCardGamealloc]initWithCardCount:self.cardButtons.countusingDeck:self.deck];
  18. return _game;
  19. }
  20. - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil
  21. {
  22. self = [super initWithNibName:nibNameOrNilbundle:nibBundleOrNil];
  23. if (self) {
  24. // Custom initialization
  25. }
  26. return self;
  27. }
  28. - (IBAction)touchCardButton:(UIButton *)sender
  29. {
  30. NSUIntegerchooseCardIndex = [self.cardButtonsindexOfObject:sender];
  31. [self.gamechooseCardAtIndex:chooseCardIndex];
  32. [selfupdateUI];
  33. }
  34. - (void)updateUI
  35. {
  36. for (UIButton *cardButton in self.cardButtons) {
  37. NSUInteger index = [self.cardButtonsindexOfObject:cardButton];
  38. TRCard *card = [self.gamecardAtIndex:index];
  39. [cardButtonsetTitle:[self titleForCard:card] forState:UIControlStateNormal];
  40. [cardButtonsetBackgroundImage:[self imageForCard:card] forState:UIControlStateNormal];
  41. cardButton.enabled = ![card isMatched];
  42. self.scoreLabel.text = [NSStringstringWithFormat:@"Score:%d", self.game.score];
  43. }
  44. }
  45. - (UIImage *)imageForCard:(TRCard *)card
  46. {
  47. if([card isChosen]){
  48. return [UIImageimageNamed:@"cardfront.png"];
  49. }else{
  50. return [UIImageimageNamed:@"cardback.png"];
  51. }
  52. }
  53. - (NSString *)titleForCard:(TRCard *)card
  54. {
  55. return [card isChosen] ? card.content : @"";
  56. }
  57. @end

本案例中,TRCard.h文件中的完整代码如下所示:

 
  1. #import<Foundation/Foundation.h>
  2. //扑克牌类
  3. @interfaceTRCard : NSObject
  4. @property (nonatomic, getter=isChosen) BOOL chosen;//被选中
  5. @property (nonatomic, getter=isMatched) BOOL matched;//被匹配
  6. @property (nonatomic, strong, readonly)NSString *content;//这张牌的内容, 如:"♣️A"
  7. @property (nonatomic, strong)NSString *suit;//花色♠️♥️♣️♦️
  8. @property (nonatomic) NSUInteger rank;//级别
  9. //和另外的一个扑克牌匹配,返回得分
  10. - (int)match:(TRCard *)otherCard;
  11. //返回合法的花色
  12. + (NSArray *)validSuits;
  13. //返回级别的最大值
  14. + (NSUInteger)maxRank;
  15. @end

本案例中,TRCard.m文件中的完整代码如下所示:

 
  1. #import "TRCard.h"
  2. @implementationTRCard
  3. @synthesize suit = _suit;
  4. - (int)match:(TRCard *)otherCard
  5. {
  6. int score = 0;
  7. if(self.rank == otherCard.rank) score = 4;
  8. else if(self.suit == otherCard.suit) score = 1;
  9. return score;
  10. }
  11. + (NSArray *)validSuits
  12. {
  13. return @[@"♠️", @"♥️", @"♣️", @"♦️"];
  14. }
  15. - (void)setSuit:(NSString *)suit
  16. {
  17. if([[TRCardvalidSuits] containsObject:suit]){
  18. _suit = suit;
  19. }
  20. }
  21. -(NSString *)suit
  22. {
  23. return _suit ? _suit : @"?";
  24. }
  25. - (void)setRank:(NSUInteger)rank
  26. {
  27. if(rank <= 13){
  28. _rank = rank;
  29. }
  30. }
  31. + (NSArray *)randStrins
  32. {
  33. return @[@"?", @"A", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9", @"10", @"J", @"Q", @"K"];
  34. }
  35. - (NSString *)content
  36. {
  37. NSString *rankString = [TRCardrandStrins][self.rank];
  38. return [self.suitstringByAppendingString:rankString];
  39. }
  40. //返回级别的最大值
  41. + (NSUInteger)maxRank
  42. {
  43. return [[TRCardrandStrins] count] - 1;
  44. }
  45. @end

本案例中,TRDeck.h文件中的完整代码如下所示:

 
  1. #import<Foundation/Foundation.h>
  2. #import "TRCard.h"
  3. //牌桌类
  4. @interfaceTRDeck : NSObject
  5. - (TRCard *)randomCard;
  6. @end

本案例中,TRDeck.m文件中的完整代码如下所示:

 
  1. #import "TRDeck.h"
  2. @interfaceTRDeck ()
  3. @property (nonatomic, strong) NSMutableArray *cards;
  4. @end
  5. @implementationTRDeck
  6. - (NSMutableArray *)cards
  7. {
  8. if(!_cards)_cards = [[NSMutableArrayalloc]init];
  9. return _cards;
  10. }
  11. - (instancetype)init
  12. {
  13. self = [super init];
  14. if (self) {
  15. for (NSString *suit in [TRCardvalidSuits]) {
  16. for(NSUInteger rank = 1; rank<=[TRCardmaxRank]; rank++){
  17. //创建纸牌对象
  18. TRCard *card = [[TRCardalloc]init];
  19. card.suit = suit;
  20. card.rank = rank;
  21. //将纸牌对象添加的纸牌数组中
  22. [self.cardsaddObject:card];
  23. }
  24. }
  25. }
  26. return self;
  27. }
  28. //随机发牌
  29. - (TRCard *)randomCard
  30. {
  31. unsignedint index = arc4random() % self.cards.count;
  32. TRCard *card = self.cards[index];
  33. [self.cardsremoveObjectAtIndex:index];
  34. return card;
  35. }
  36. @end

本案例中,TRCardGame.h文件中的完整代码如下所示:

 
  1. #import<Foundation/Foundation.h>
  2. #import "TRCard.h"
  3. #import "TRDeck.h"
  4. //游戏类
  5. @interfaceTRCardGame : NSObject
  6. - (instancetype)initWithCardCount:(NSUInteger)count usingDeck:(TRDeck *)deck;
  7. //用户选中了一张牌
  8. - (void)chooseCardAtIndex:(NSUInteger)index;
  9. //根据下标返回指定扑克牌
  10. - (TRCard *)cardAtIndex:(NSUInteger)index;
  11. @property (nonatomic, readonly) NSInteger score;//分数
  12. @end

本案例中,TRCardGame.m文件中的完整代码如下所示:

 
  1. #import "TRCardGame.h"
  2. @interfaceTRCardGame ()
  3. @property (nonatomic, strong)NSMutableArray *cards;
  4. @property (nonatomic, readwrite) NSInteger score;//分数
  5. @end
  6. @implementationTRCardGame
  7. - (NSMutableArray *)cards
  8. {
  9. if (!_cards) {
  10. _cards = [[NSMutableArrayalloc]init];
  11. }
  12. return _cards;
  13. }
  14. - (instancetype)initWithCardCount:(NSUInteger)count usingDeck:(TRDeck *)deck
  15. {
  16. self = [super init];
  17. if (self) {
  18. for(inti=0; i<count; i++){
  19. TRCard *card = [deck randomCard];
  20. [self.cardsaddObject:card];
  21. }
  22. }
  23. return self;
  24. }
  25. //用户选中了一张牌
  26. - (void)chooseCardAtIndex:(NSUInteger)index
  27. {
  28. //进行匹配
  29. TRCard *card = [self cardAtIndex:index];
  30. if(![card isMatched]){
  31. if([card isChosen]){
  32. card.chosen = NO;//再翻回去
  33. }else{//没有匹配,也没有在正面
  34. //匹配其他翻过来的牌
  35. for (TRCard *otherCard in self.cards) {
  36. if([otherCardisChosen] && ![otherCardisMatched]){
  37. int score = [card match:otherCard];
  38. if(score){//匹配成功
  39. self.score += score;
  40. card.matched = YES;
  41. otherCard.matched = YES;
  42. }else{//匹配失败
  43. otherCard.chosen = NO;
  44. }
  45. }
  46. }
  47. //把牌翻过来
  48. card.chosen = YES;
  49. }
  50. }
  51. }
  52. //根据下标返回指定扑克牌
  53. - (TRCard *)cardAtIndex:(NSUInteger)index
  54. {
  55. return index<self.cards.count ? self.cards[index] : nil;
  56. }
  57. @end