斯坦福大学iOS公开课笔记(8)--代理、block、动画

这一节课主要讲了代理、block和动画,并且在课程的最后部分编写了一个类似俄罗斯方块的游戏 Dropit!来使大家更加理解这些知识。代理和block都是平时开发时需要用到的很重要的知识,这节课可能会比较难理解一些,不过内容都是十分重要的。

协议 Protocol

声明一个协议

@interface一样,我们在声明一个协议的时候也是说明他的变量和方法,只不过在声明的时候把关键字换成了@protocol

但是协议只是写他的声明部分,而没有实现部分,所以@protocol没有对应的@implementation

注意:在协议中的方法默认是必须实现的,但是可以通过@optional来声明它之后的方法为可选的。同样的也可以使用@required来声明他之后的方法是必须实现的。

和类差不多,我们也可以在声明的时候增加一个 <xyzzy> 来表示他继承了xyzzy中的所有方法,所以实现的时候也要实现xyzzy中的全部方法。当然我们也可以同时继承多个协议<xyzzy,NSObject>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@protocol Foo<Xyzzy,NSObject>
- (void)someMethod;
@opational //之后的方法为可选择实现的
- (void)methodWithArgument:(BOOL)argument;
@required //之后的方法必须实现
@property (readonly) int readonlyProperty;
@property NSString *readwriteProperty;
- (int)methodThatReturnSomething;
@end

Protocol放在哪里?

  • 放在自己的头文件中

    比如Foo协议的头文件就是Foo.h。

  • 放在某个相关类的头文件中

    比如UIScrollViewDelegate可以放在使用UIScrollView的文件中。

Protocol有什么用?

Protocol可以让你和编译器都知道声明的这个对象遵循哪个协议,意味着他一定实现了必须实现的方法。

Protocol主要用来做什么

  • 第一个用途就是作为委托Delegate和数据源DataSource。就是用于当MVC中视图像控制器发送消息的时候就会用到他们。

  • 其他地方比如动画当中。

block

block就是一段代码块,可以嵌入到其他代码中,进行参数的传递,存储在数组中。

block经常以^开头,然后会制定一个返回类型,然后是一对大括号{}中间的内容就是block的内容。

__block

block中的变量都是是readonly只读的,所以在block中我们没有办法修改他的值。这时候我们需要借助到__block关键字。使用了__block关键字之后,系统会自动生成一段代码,将stoppedEarly从栈中移动到堆中,这样就可以在block中使用了,然后,在block结束的时候再将信息复制会堆中,再放回到栈上。

1
2
3
4
5
6
7
8
9
10
11
12
__block BOOL stoppedEarly = NO;
double stopValue = 53.5;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop){
NSLog(@"value for key %@ is %@",key,value);
if([@"ENOUGH" isEqualToString:key] || [value doubleValue] == stopValue)
{
*stop = YES;
stoppedEarly = YES; //这里可以修改外部的变量
}
}];

__weak

因为每次向block中的对象发送消息时,系统就会创建一个指向该对象的强指针,该指针会一直保持到block超出范围以后。所以只要block存在,系统中的对象就会被一个强指针指着。这样就会照成一个问题,会照成循环引用。比如下面一段代码:

1
2
3
[self.myBlocks addObject:^{
[self doSomething];
}];

block会有一个强指针指向self,而且重点是self也会有一个强指针指向block,现在他们都互相强引用,谁也不会先释放谁,这就照成了循环引用。

解决他们的办法就是使用局部变量,当block作用的部分结束之后,局部变量不再被引用,他就会释放,这样就解决了循环引用的问题。

但是局部变量也是强指针类型的,这时候我们可以使用__weak修饰符来修饰他,让他变成弱类型。弱类型不会在堆中保存该对象,如果没有元素指向他,他就会释放。

1
2
3
4
5
__weak MyClass *weakSelf = self;
[self.myBlocks addObject:^{
[self doSomething];
}];

在使用ARC的情况下,大多数只有这种情况下会造成循环引用。

Animation 动画

UIView Animation

UIView Animation是UIView的一个类方法,用来改变

  • frame(位置)
  • transfrom(缩放比例,旋转情况)
  • alpha(透明度)

当我们对他添加一个动画,他立即就会变换成目标的样式,只不过显示的时候会显示出一个慢慢变化的动画。比如我将一个view的frame从(10,10)变为(100,100),那么系统会在执行到这里时立刻做出这个变化,然后在视图上慢慢改变frame来达到一个动画的效果。

1
2
3
4
5
6
7
8
9
[UIView animateWithDuration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
options:(UIViewAnimationOptions)options
animations:^{
... //需要改变的值
}
completion:^(BOOL finished) {
... //完成动画后做的事情
}];
  • duration表示需要多少时间来执行。
  • delay表示等待多久开始执行。
  • options动画的运行状态,iOS中提供了很多种options,他是一个枚举值,具体的值可以在文档中查看。
  • animations他是一个block,没有参数也没有返回值,可以在这修改需要的值。
  • completion也是一个block, 当动画完成之后调用这个block。这里的finished表示动画是否完成,因为当出发这个方法的时候,实际上的值已经改变了,但是在执行动画的过程中有可能会被打断,比如突然改变了alpha,这样的话在调用completion的时候就可以通过finish来判断是否动画被打断来改变他的状态。

当我想要修改整个视图的状态的时候,我们可以用到这个类方法

1
2
3
4
5
+ (void)transitionWithView:(UIView *)view
duration:(NSTimeInterval)duration
options:(UIViewAnimationOptions)options
animations:(void (^ __nullable)(void))animations
completion:(void (^ __nullable)(BOOL finished))completion;
  • view你想要改变的目标view,比如playingCardView。
    其他的参数与之前的参数基本一致,只是options用的是不同的options。

Dynamic Animation

物理引擎动画,他主要是用作对视图进行物理效果的动画,比如重力、碰撞、弹性等等。
关于物理引擎的更多使用可以点击这里看我的另外一篇博客。

Demo Dropit

最后的部分是一个小游戏的demo,这个小游戏类似于简化版的俄罗斯方块。要求点击了之后掉落一个正方形的小方块。当小方块填充满一整行之后“炸掉”这一行。这个demo结合了Dynamic AnimationUIView Animation

logo

因为这节课没有设置一个边界,所以当铺满整个屏幕之后的判断会失效导致并没有显示出一个爆炸的效果,在下一节课中,我们会学习Auto Layout这个功能来限制边框,从而修复判断失效的问题。

添加点击掉落手势

首先我们给页面添加一个手势tap,并且在点击之后响应随机出现一个方块。

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
- (IBAction)tap:(UITapGestureRecognizer *)sender
{
[self drop];
}
- (void)drop
{
CGRect frame;
frame.origin = CGPointZero;
frame.size = DROP_SIZE;
//设置随机出现的横坐标
int x = (arc4random()%(int)self.gameView.bounds.size.width) / DROP_SIZE.width;
frame.origin.x = x * DROP_SIZE.width;
UIView *dropView = [[UIView alloc] initWithFrame:frame];
dropView.backgroundColor = [self randColor];
[self.gameView addSubview:dropView]; //设置随机方块颜色
}
- (UIColor *)randColor
{
switch (arc4random()%5)
{
case 0: return [UIColor greenColor];
case 1: return [UIColor blueColor];
case 2: return [UIColor orangeColor];
case 3: return [UIColor redColor];
case 4: return [UIColor purpleColor];
default:
break;
}
return [UIColor blackColor];
}
logo

设置重力和碰撞

需要掉落的view已经有了,接下来我们需要给整个页面添加一个重力,和一个碰撞的属性,在课程中一开始这里是分开写的,后来新建了一个类来调用他,这边我们就直接新建一个类然后调用他。

首先先创建一个继承UIDynamicBehavior的类DropitBehavior
然后给他添加两个方法

1
2
3
4
5
6
@interface DropitBehavior : UIDynamicBehavior
- (void)addItem:(id <UIDynamicItem>) item;
- (void)removeItem:(id <UIDynamicItem>) item;
@end

然后添加碰撞和重力方法

1
2
3
4
5
6
@interface DropitBehavior()
@property (nonatomic, strong) UIGravityBehavior *gravity;
@property (nonatomic, strong) UICollisionBehavior *collision;
@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
45
46
47
48
49
50
51
52
53
54
@implementation DropitBehavior
- (UIGravityBehavior *)gravity
{
if(!_gravity)
{
_gravity = [[UIGravityBehavior alloc] init];
_gravity.magnitude = 0.9;
}
return _gravity;
}
- (UICollisionBehavior *)collision
{
if(!_collision)
{
_collision = [[UICollisionBehavior alloc] init];
_collision.translatesReferenceBoundsIntoBoundary = YES;
}
return _collision;
}
- (void)addItem:(id<UIDynamicItem>)item
{
[self.gravity addItem:item];
[self.collision addItem:item];
}
- (void)removeItem:(id<UIDynamicItem>)item
{
[self.gravity removeItem:item];
[self.collision removeItem:item];
}
- (instancetype)init
{
self = [super init];
if(self)
{
[self addChildBehavior:self.gravity];
[self addChildBehavior:self.collision];
}
return self;
}
@end

这样我们的类就封装好了。接下来在控制器里我们实现这个动画并将DropitBehavior添加进来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (UIDynamicAnimator *)animator
{
if(!_animator)
{
_animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.gameView];
}
return _animator;
}
- (DropitBehavior *)dropitBehavior
{
if(!_dropitBehavior)
{
_dropitBehavior = [[DropitBehavior alloc] init];
[self.animator addBehavior:_dropitBehavior];
}
return _dropitBehavior;
}

最后需要在drop方法里,给生成的view添加dropitBehavior属性。

1
[self.dropitBehavior addItem:dropView];

这样就基本实现了我们需要的功能。

logo

###设置炸掉一整行的动画

炸掉一整行的动画其实就是改变这一整行中所有view的位置,让他的横坐标位置随机,纵坐标位置为负数即可。

这里快速写了一段判断是否排满整行的代码,原理就是从屏幕的最下边开始,检测每一个应该出现方块的中心位置是否有方块。如果都有则表示这一行排满了,则执行爆炸动画。

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
- (BOOL)removeCompletedRows
{
NSMutableArray *dropToRemove = [[NSMutableArray alloc] init];
for(CGFloat y = self.gameView.bounds.size.height-DROP_SIZE.height/2 ; y > 0 ; y -= DROP_SIZE.height)
{
BOOL rowIsComplete = YES;
NSMutableArray *dropsFound = [[NSMutableArray alloc] init];
for(CGFloat x = DROP_SIZE.width/2 ; x <= self.gameView.bounds.size.width-DROP_SIZE.width/2 ; x += DROP_SIZE.width)
{
UIView *hitView = [self.gameView hitTest:CGPointMake(x, y) withEvent:NULL];
if([hitView superview] == self.gameView)
{
[dropsFound addObject:hitView];
}
else
{
rowIsComplete = NO;
break;
}
}
if(![dropsFound count])
break;
if(rowIsComplete)
{
[dropToRemove addObjectsFromArray:dropsFound];
}
}
if([dropToRemove count])
{
for(UIView *drop in dropToRemove)
{
[self.dropitBehavior removeItem:drop];
}
[self animateRemovingDrops:dropToRemove];
}
return NO;
}

爆炸的动画用到的是UIView Animation来改变位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)animateRemovingDrops:(NSArray *)dropsToRemove
{
[UIView animateWithDuration:1.0
animations:^{
for(UIView *dropView in dropsToRemove)
{
int x = (arc4random()%(int)(self.gameView.bounds.size.width*5)) - (int)self.gameView.bounds.size.width*2;
int y = self.gameView.bounds.size.height;
dropView.center = CGPointMake(x, -y);
}
} completion:^(BOOL finished) {
[dropsToRemove makeObjectsPerformSelector:@selector(removeFromSuperview)];
}];
}
logo

以上就是这节课的内容,这个小游戏还会在下节课继续完成,完成之后我也会补全这一部分的代码。