斯坦福大学IOS7开发课程2

斯坦福大学IOS7开发课程2

这节课就着手开始实现”Card Matching Game”,纸牌匹配游戏。

具体来说就是实现涉及的各种类,以及XCode的简单使用。

Card类

下面代码是课程1结束时的Card类,包括Card.hCard.m

1
2
3
4
5
6
7
8
9
// Card.h
#import <Foundation/Foundation>
@interface Card : NSObject
@property (strong nonatomic) NSString *contents;

@property (nonatomic, getter=isChosen) BOOL chosen;
@property (nonatomic, getter=isMatched) BOOL matched;
- (int)match:(NSArray *)otherCards;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Card.m
#import "Card.h"
@interface Card()
@end
@implementation Card

- (int)match:(NSArray *)otherCards
{
int score = 0;
for(Card *card in otherCards) {
if([card.contents isEqualToString:self.contents])
score = 1;
}
}
@end

Deck类

这节课首先添加另外一个类:整幅牌Deck,下面是上节课学的类的基本结构

1
2
3
4
5
// Deck.h
#import <Foundation/Foundation.h>

@interface Deck : NSObject
@end
1
2
3
4
5
6
// Deck.m
#import "Deck.h"
@interface Deck()
@end
@implementation Deck
@end

接下来为Deck类添加两个基础的方法:

  • 向Deck中添加牌的addCard,其中atTop表示是否将新的牌放在牌堆的顶部。
  • 另一个是从Deck中随机抽取牌的drawRandomCard
1
2
3
4
5
6
7
8
9
// Deck.h
#import <Foundation/Foundation.h>
#import "Card.h"

@interface Deck : NSObject
- (void)addCard:(Card *)card atTop:(BOOL)atTop;

- (Card *)drawRandomCard;
@end

如果希望addCard的参数atTop可选参数,在Objective-C中唯一的方法就是声明一个新的函数,这个函数也叫addCard,但是没有参数atTop。这两个addCard是两个不同的函数,相互之间并没有关联。

1
2
3
4
5
6
7
8
9
10
// Deck.h
#import <Foundation/Foundation.h>
#import "Card.h"

@interface Deck : NSObject
- (void)addCard:(Card *)card atTop:(BOOL)atTop;
- (void)addCard:(Card *)card;

- (Card *)drawRandomCard;
@end

为了存储牌堆中的牌,需要在私密API中添加一个属性,类型是可变数组NSMutableArray。mutable意味着可以向数组中添加或删除数据,而普通的NSArray是不能修改的,一旦被建立,不能添加数据也不能删除。

在Objective-C中声明数组没办法指定数据类型。

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
// Deck.m
#import "Deck.h"
@interface Deck()
@property (strong, nonatomic) NSMutableArray *cards; // of Card
@end
@implementation Deck

- (void)addCard:(Card *)card atTop:(BOOL)atTop
{
if (atTop) {
[self.cards insertObject:card atIndex:0];
} else {
[self.cards addObject:card];
}
}

- (void)addCard:(Card *)card
{
[self addCard:card atTop:NO];
}

- (Card *)drawRandomCard
{

}
@end

根据上节课说到构建属性时自动生成的getter和setter,新建一个Deck类后,其所有的变量和属性都会被自动初始化为0/nil,属性cards是一个空指针,因此调用addCard时虽然不会导致程序的崩溃,但是也不能正常工作。

那么怎么解决这个问题呢?方法是重写属性的getter函数,在getter中添加一个判断结构,属性cards自动生成的getter是:

1
2
3
4
- (NSMutableArray *)cards
{
return _cards;
}

为了在其中添加一个判断逻辑,需要手动重写这个getter:

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
// Deck.m
#import "Deck.h"
@interface Deck()
@property (strong, nonatomic) NSMutableArray *cards; // of Card
@end
@implementation Deck

// 为了处理cards为空指针的情况,手动重写getter
- (NSMutableArray *)cards
{
if(!_cards) _cards = [[NSMutableArray alloc] init];
return _cards;
}

- (void)addCard:(Card *)card atTop:(BOOL)atTop
{
if (atTop) {
[self.cards insertObject:card atIndex:0];
} else {
[self.cards addObject:card];
}
}

- (void)addCard:(Card *)card
{
[self addCard:card atTop:NO];
}

- (Card *)drawRandomCard
{

}
@end

接下来实现drawRandomCard

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
// Deck.m
#import "Deck.h"
@interface Deck()
@property (strong, nonatomic) NSMutableArray *cards; // of Card
@end
@implementation Deck

- ( NSMutableArray *)cards
{
if(!_cards) _cards = [[NSMutableArray alloc] init];
return _cards;
}

- (void)addCard:(Card *)card atTop:(BOOL)atTop
{
if (atTop) {
[self.cards insertObject:card atIndex:0];
} else {
[self.cards addObject:card];
}
}

- (void)addCard:(Card *)card
{
[self addCard:card atTop:NO];
}

- (Card *)drawRandomCard
{
Card *randomCard = nil;

if([self.cards count]) {
unsigned index = arc4random() % [self.cards count];
randomCard = self.cards[index];
[self.cards removeObjectAtIndex:index];
}

return randomCard;
}
@end

至此,Deck类已经完成。

PlayingCard类

接下来再添加一个类:PlayingCard,同样的,基本结构如下:

1
2
3
4
5
// PlayingCard.h
#import "Card.h"
@interface PlayingCard : Card // 终于有个类的父类不是NSObject了

@end
1
2
3
4
5
// PlayingCard.m
#import "PlayingCard.h"
@implementation PlayingCard

@end

给类添加两个属性suitrank,前者表示牌的花色“桃(hearts)”、“(方片)diamons”、“(梅花)clubs”,后者表示1到13。另外在.m文件中,重写父类属性content的getter。

1
2
3
4
5
6
7
8
// PlayingCard.h
#import "Card.h"
@interface PlayingCard : Card

@property (strong, nonatomic) NSString *suit;
@property (nonatomic) NSUInteger rank;

@end
1
2
3
4
5
6
7
8
// PlayingCard.m
#import "PlayingCard.h"
@implementation PlayingCard
- (NSString *)contents
{
return [NSString stringWithFormat:@"%d%@",self.rank, self.suit];
}
@end

注意到,字符串前面有个@,这表示把字符串变成一个字符串类。其中%@表示一个对象,当然可以是字符串。

重写父类属性contents的getter之后,获取contents内容会返回“数字 + 花色”,比如“3红桃”、“1梅花”、“13方片”等。但是在纸牌中,我们一般会把1说成A,11说成J等……,为了符合这个习惯,上面的contents重写可以改成下面的代码:

1
2
3
4
5
6
7
8
9
10
// PlayingCard.m
#import "PlayingCard.h"
@implementation PlayingCard
- (NSString *)contents
{
NSArray *rankStrings = @[@"?",@"A",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10",@"J",@"Q",@"K"];
// 将0-13表示为?,1,2,...,J,Q,K
return [rankStrings[self.rank] stringByAppendingString:self.suit];
}
@end

将rank的0设置为”?“是因为objective-C默认将rank初始化为0,”?“表示这是未知的,没有经过设置的。那如果花色没经过设置也会显示”?“就更好了,解决方法也是重写suit属性的getter函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// PlayingCard.m
#import "PlayingCard.h"
@implementation PlayingCard
- (NSString *)contents
{
NSArray *rankStrings = @[@"?",@"A",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10",@"J",@"Q",@"K"];
// 将0-13表示为?,1,2,...,J,Q,K
return [rankStrings[self.rank] stringByAppendingString:self.suit];
}

- (NSString *)suit
{
return _suit?_suit:@"?";
}
@end

suit的选择应该只有四种,为了防止suit被设置为其他值,还要重写suit属性的setter:setSuit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// PlayingCard.m
#import "PlayingCard.h"
@implementation PlayingCard
- (NSString *)contents
{
NSArray *rankStrings = @[@"?",@"A",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10",@"J",@"Q",@"K"];
// 将0-13表示为?,1,2,...,J,Q,K
return [rankStrings[self.rank] stringByAppendingString:self.suit];
}
@synthesize suit = _suit;
// 因为重构了suit的getter和setter,所以要手写@synthesize
- (void)setSuit:(NSString *)suit
{
if([@[@"♥️",@"♦️",@"♠️",@"♣️"] containsObject:suit]) {
_suit = suit;
}
}

- (NSString *)suit
{
return _suit?_suit:@"?";
}
@end

值得注意的是,setSuit中的第一个@表示创建新的数组,在这里,每次判断都会新建这个数组。为了性能和代码简介,可以新建函数来判断setter收到的suit是否有效。实际上,这种改变对性能的提升是极其有限的。

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
// PlayingCard.m
#import "PlayingCard.h"
@implementation PlayingCard
- (NSString *)contents
{
NSArray *rankStrings = @[@"?",@"A",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10",@"J",@"Q",@"K"];
// 将0-13表示为?,1,2,...,J,Q,K
return [rankStrings[self.rank] stringByAppendingString:self.suit];
}
@synthesize suit = _suit;
// 因为重构了suit的getter和setter,所以要手写@synthesize

+ (NSArray *)validSuits
{
return @[@"♥️",@"♦️",@"♠️",@"♣️"];
}
- (void)setSuit:(NSString *)suit
{
if([[PlayingCard validSuits] containsObject:suit]) {
// 类的函数的调用方法。
_suit = suit;
}
}

- (NSString *)suit
{
return _suit?_suit:@"?";
}
@end

这里在函数实现前面第一次出现了+符号,这个符号表示这个函数是类的函数(而不是对象的函数)。

一般只在两种情况下使用类的函数:

  1. 工具函数(utility method),比如这里的validSuits

  2. 创建类的函数,比如stringWithFormat

    对rank属性做同样的检查和优化,并添加一个公开APImaxRank返回rank的最大值,比如现在是13。

1
2
3
4
5
6
7
8
9
10
11
// PlayingCard.h
#import "Card.h"
@interface PlayingCard : Card

@property (strong, nonatomic) NSString *suit;
@property (nonatomic) NSUInteger rank;

+ (NSArray *)validSuits;
+ (NSUInteger)maxRank;

@end
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
// PlayingCard.m
#import "PlayingCard.h"
@implementation PlayingCard
- (NSString *)contents
{
NSArray *rankStrings = [PlayingCard rankStrings];
// 将0-13表示为?,1,2,...,J,Q,K
return [rankStrings[self.rank] stringByAppendingString:self.suit];
}
@synthesize suit = _suit;
// 因为重构了suit的getter和setter,所以要手写@synthesize

+ (NSArray *)validSuits
{
return @[@"♥️",@"♦️",@"♠️",@"♣️"];
}
- (void)setSuit:(NSString *)suit
{
if([[PlayingCard validSuits] containsObject:suit]) {
// 类的函数的调用方法。
_suit = suit;
}
}

- (NSString *)suit
{
return _suit?_suit:@"?";
}

+ (NSArray *)rankStrings
{
return @[@"?",@"A",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10",@"J",@"Q",@"K"];
}
+ (NSUInteger)maxRank
{
return [[self rankStrings] count]-1;
}
- (void)setRank:(NSUInteger)rank
{
if(rank <= [PlayingCard maxRank]) {
_rank = rank;
}
}
@end

PlayingCardDeck类

接下来可以开始玩牌了,创建一个新的类PlayingCardDeck,继承自Deck类,但是需要重写构造函数

1
2
3
4
5
// PlayingCardDeck.h
#import "Deck.h"

@interface PlayingCardDeck : Deck
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// PlayingCardDeck.m
#import "PlayingCardDeck.h"
#import "PlayingCard.h"

@implementation PlayingCardDeck
- (instancetype)init // instancetype类只在init中使用
{
self = [super init];
// 调用父类的构造函数
if(self) {
// 父类正常完成构造,继续子类的构造函数
// 如果父类无法完成构造,将不执行子类的构造代码
for (NSString *suit in [PlayingCard validSuits]) {
for (NSUInteger rank = 1; rank <= [PlayingCard maxRank]; rank++) {
PlayingCard *card = [[PlayingCard allo] init];
card.rank = rank;
card.suit = suit;
[self addCard:card];
}
}
}
return self;
}
@end

XCode的简单使用

课程剩余的部分就是以纸牌游戏为例,简单介绍XCode的使用,开发一个简单的App,App内容是显示纸牌,点击纸牌将其翻转。

由于课程介绍的是XCode 5,笔者记笔记的时候已经是XCode 12.4了,有了不小的变化,参考笔者另一篇笔记(Objective-C IOS开发之HelloWorld),应该也不难完成,就不再具体介绍实现了。

相较于HelloWorld这个App,课程中实现的App另外涉及了以下知识点:

  • 图像添加

    直接将图片拖到Assets.xcassets文件夹中

  • button背景图片的设置

    点击button,在属性中点击Background下拉菜单,就会显示上一步添加的图片选项,以及一些原始icon。

  • Action中的sender其实就是触发事件的View对象

  • 类的添加

    添加类的方法,XCode 12中其实就是新建Cocoa Touch Class