斯坦福大学iOS开发公开课总结(八) :协议,block,动画,俄罗斯方块Demo

本节课介绍了协议,block,动画的相关知识,最后结合了这些知识点展示了一个类似俄罗斯方块的小游戏Demo。
总体来说本节课的内容比较重要,稍微摆脱了UI层面的知识,对于初学者来说理解起来不是很容易,不过笔者会尽量详细地讲解给大家。

协议


关于协议所介绍的知识点比较简单,而且实现起来相对容易,故不做详细介绍,各位可以参考文档或者相关博客即可。
在这里只强调一个知识点:

id objid<MyProtocol>obj的相同点和不同点:

相同点:都表示了某个对象。
不同点
id obj表示obj是具体某一类的实例对象。
id<MyProtocol>obj只表示遵守了某协议的对象 。

因为有的时候我们并不需要确保某个对象一定是某个类的实例对象,而只需要它遵循了某个协议,这个时候就需要用第二行的写法来确保这个对象确实遵循了<MyProtocol>。

Block


关于block的概念和语法在这里就不赘述了,因为有文档和很多牛人已经总结地很好了。
在这里只强调两点关于block的使用注意事项。

修改block内部变量的方案

如果我们要在block里将found值设为YES,就应该在block外部添加__block关键字。

    __block BOOL found = NO;
    //通过__block关键字,将found从栈中移动到堆中保证其可以被修改;block结束后,将该变量复制一份到堆中,再放回栈上

    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop){        

        if ([targetString isEqualToString:obj]) {            

            *stop = YES; //停止
            found = YES;
        }        
    }];

存储循环的解决方案

只要block存在,block内部消息中的每个对象都会被block的一个强指针指着。此时,如果这些对象里的某个或几个对象也有指向该block的指针,就会造成存储循环。

问题重现:


    //这个block有强指针指向self,而self也通过myBlocks数组有强指针指向block

    [self.myBlocks addObject:^{    

        [self doSomething];

    }];

解决方案:创建弱类型的局部变量


    __weak ViewController *weakSelf = self; //创建弱类型的局部变量

    [self.myBlocks addObject:^{    

        [weakSelf doSomething];

    }];

Block的应用

block可以直接保存在变量中,属性中,字典和数组中。

具体使用环境:

  • 多线程:用于主线程,子线程的回调。
  • 枚举:数组,字典的枚举等。
  • 通知:某件事情发生后,信息的传递。
  • 错误时调用:“包住”错误发生后需要执行的代码。
  • 成功时调用:“包住”任务成功后需要执行的代码。
  • 动画
  • 排序

通过View改变视图的属性来实现动画


  • 改变frame
  • 改变transform
  • 改变alpha

具体通过UIView的类方法来改变

+ (void)animateWithDuration:(NSTimeInterval)duration   //动画在这个屏幕上出现的时间
                                     delay:(NSTimeInterval)delay       //等待多长时间再执行
                                  options:(UIViewAnimationOptions)options 
                             animations:(void (^)(void))animations  //在此代码块中修改frame,transform 和 alpha
                             completion:(void (^ __nullable)(BOOL finished))completion;

options参数:

    UIViewAnimationOptionLayoutSubviews            = 1 <<  0,
    UIViewAnimationOptionAllowUserInteraction      = 1 <<  1, // turn on user interaction while animating
    UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2, // start all views from current value, not initial value
    UIViewAnimationOptionRepeat                    = 1 <<  3, // repeat animation indefinitely
    UIViewAnimationOptionAutoreverse               = 1 <<  4, // if repeat, run animation back and forth
    UIViewAnimationOptionOverrideInheritedDuration = 1 <<  5, // ignore nested duration
    UIViewAnimationOptionOverrideInheritedCurve    = 1 <<  6, // ignore nested curve
    UIViewAnimationOptionAllowAnimatedContent      = 1 <<  7, // animate contents (applies to transitions only)
    UIViewAnimationOptionShowHideTransitionViews   = 1 <<  8, // flip to/from hidden state instead of adding/removing
    UIViewAnimationOptionOverrideInheritedOptions  = 1 <<  9, // do not inherit any options or animation type

通过给视图添加物理效果实现动画


添加物理效果主要需要三个元素:

  1. DynamicAnimator
  2. UIGravityBehavior
  3. 遵守<UIDynamicItem>协议的item(大部分情况是UIView)

DynamicAnimator:动力动画

UIDynamicAnimator *animator =[ [ UIDynamicAnimator alloc] initWithReferenceView:aView]; //aview是动画Views的顶级视图

动力动画的初始化需要给其添加要进行动画的顶级视图,详细内容后面再介绍。

UIDynamicBehavior:动力行为

动力行为分为重力动力行为,碰撞行为等具体的行为。
这个类有很多子类:

1. UIGravityBehavior:重力行为


@property (readwrite, nonatomic) CGFloat angle;//重力方向

@property (readwrite, nonatomic) CGFloat magnitude; //重力加速度值

2. UICollisionBehavior:碰撞行为


@property (nonatomic, readwrite) UICollisionBehaviorMode collisionMode;//互相碰撞弹开还是只是从边界碰撞弹开

@property (nonatomic, readwrite) BOOL translatesReferenceBoundsIntoBoundary; //是否是有弹性的边界

3. UIAttachmentBehavior :吸附行为

@property (readwrite, nonatomic) CGPoint anchorPoint; //设置锚点
- (instancetype)initWithItem:(id <UIDynamicItem>)item attachedToAnchor:(CGPoint)point;//将动力项吸附在锚点上
- (instancetype)initWithItem:(id <UIDynamicItem>)item1 attachedToItem:(id <UIDynamicItem>)item2;//吸附两个动力项

4. UISnapBehavior:速甩行为

- (instancetype)initWithItem:(id <UIDynamicItem>)item snapToPoint:(CGPoint)point NS_DESIGNATED_INITIALIZER;

5. UIPushBehavior:推动行为

@property (nonatomic, readonly) UIPushBehaviorMode mode;

@property (readwrite, nonatomic) CGFloat magnitude;//推力

@property (readwrite, nonatomic) CGVector pushDirection;//推动方向

6. UIDynamicItemBehavior:动力项行为

@property (readwrite, nonatomic) CGFloat elasticity; // Usually between 0 (inelastic) and 1 (collide elastically) 

@property (readwrite, nonatomic) CGFloat friction; // 0 being no friction between objects slide along each other

@property (readwrite, nonatomic) CGFloat density; // 1 by default

@property (readwrite, nonatomic) CGFloat resistance; // 0: no velocity damping

- (CGPoint)linearVelocityForItem:(id <UIDynamicItem>)item;//线速度
- (CGFloat)angularVelocityForItem:(id <UIDynamicItem>)item;//角速度

遵守<UIDynamicItem>协议的item(大部分情况是UIView)

只要是遵守了<UIDynamicItem>协议(动力项协议)的对象,都可以添加动力行为。

id<UIDynamicItem>item1 = ....;
id<UIDynamicItem>item2 = ....;
[gravity addItem:itme2];

动力项协议的属性:

@property (nonatomic, readwrite) CGPoint center;//动力项的中心

@property (nonatomic, readonly) CGRect bounds; //动力项的绘制区域,只读,通过变换,居中,移动进行修改

@property (nonatomic, readwrite) CGAffineTransform transform;//动力项的旋转或缩放比例

若想与animator的动画相抗争,需要调用animator的以下方法:

- (void)updateItemUsingCurrentState:(id <UIDynamicItem>)item;

Demo


Demo需求

  • 点击屏幕后,在顶部随机位置生成具有随机色的正方形,正方形显示后立即下落并停止。
  • 方块排满的行会自动被炸飞,而且带动画。

Demo效果图

左:炸飞前 | 右:炸飞后

重要代码段

因为每个方块的动作行为都是一致的,所以在这里自定义了一个UIDynamicBehavior类,给每个方块增加相同的动作行为。

1. 自定义统一行为类:DropItBehavior

- (instancetype)init
{
    self = [super init];    
   //重写初始化方法,同时增加重力和碰撞行为
    [self addChildBehavior:self.gravity];
    [self addChildBehavior:self.collider];
    return self;
}

//同时增加重力和碰撞行为
- (void)addItem:(id<UIDynamicItem>)item
{
    [self.gravity addItem:item];
    [self.collider addItem:item];
}

//同时移除重力和碰撞行为

- (void)removeItem:(id<UIDynamicItem>)item
{
    [self.gravity removeItem:item];
    [self.collider removeItem:item];
}

- (UIGravityBehavior *)gravity
{

    if (!_gravity) {

        _gravity = [[UIGravityBehavior alloc] init];
         //设置重力加速度
        _gravity.magnitude = 1.9;
    }
    return _gravity;
}

- (UICollisionBehavior *)collider
{
    if (!_collider) {
        _collider = [[UICollisionBehavior alloc] init];
        //触碰边缘弹性 
        _collider.translatesReferenceBoundsIntoBoundary = YES;
    }
    return _collider;
}

2. 初始化animator

- (UIDynamicAnimator *)animator
{
    if (!_animator) {
        //self.gameView 是动画实现的顶级视图,它的子视图是掉落的方块
        _animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.gameView];
    }
    return _animator;
}

3. 给UIDynamicAnimator添加行为


- (DropItBehavior *)dropitBehavior
{
    if (!_dropitBehavior) {
         _dropitBehavior = [[DropItBehavior alloc] init];
        [self.animator addBehavior:_dropitBehavior];
    }
    return _dropitBehavior;
}

4. 生成随机方块并让其下落

/**
 *  生成随机方块并下落
 */
- (void)drop
{
    //1. 随机位置

    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];
  
    //2. 随机颜色
    dropView.backgroundColor = [self randomColor];
    [self.gameView addSubview:dropView];
    
    //3. 添加下落
    [self.dropitBehavior addItem:dropView];
}

目前小方块下落碰到障碍物后会旋转,所以容易让这些小方块散落成堆。这样一来,就不能计算好整行的排列情况,所以我们应该让小方块们没有旋转的特性。

5.取消旋转特性

在公用的behavior类DropItBehavior里增加一个UIDynamicItemBehavior实例,取消其旋转特性。

- (UIDynamicItemBehavior *)animationOptions
{
    if (!_animationOptions) {

        _animationOptions = [[UIDynamicItemBehavior alloc] init];
        _animationOptions.allowsRotation = NO;        
    }
    return _animationOptions;
}

这样就能整齐排列小方块了:

左:可旋转 | 右:不可旋转

6. 动画炸掉排满的行

最好在方块都静止了之后再判断是否有排满的行,这里需要遵守协议<UIDynamicAnimatorDelegate>


/**
 *  监听动力动画内部的所有动画停止后调用炸飞整行的方法
 *
 *  @param animator 动力动画
 */
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator

{
    [self removeCompleteRows];
}

下面来看一下炸飞整行的方法:


/**
 *  炸飞整行的方法:包括查看是否存在整行的算法和炸飞整行的动画
 */
- (void)removeCompleteRows
{
    NSMutableArray *dropsToRemove = [[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) {
            
            //移动(x,y)获取这个点所在的view
            UIView *hitView = [self.gameView hitTest:CGPointMake(x, y) withEvent:NULL];

            if ([hitView superview] == self.gameView) {
               
                //如果获取的view的父视图是gameView,就说明它是方块
                [dropsFound addObject:hitView];
                
            }else{

                //否则这个行肯定是不完整的
                rowIsComplete = NO;
                break;
            }
        }

        if (![dropsFound count]) break;
        if (rowIsComplete)[dropsToRemove addObjectsFromArray:dropsFound];
  
    }    

    //如果有排满的行,则炸掉它
    if ([dropsToRemove count]){
        for (UIView *drop in dropsToRemove){
            [self.dropitBehavior removeItem:drop];
        }
        [self animatedRemovingDrops:dropsToRemove];
    }
}

/**
 *  炸飞整行
 *
 *  @param dropsToRemove 需要炸飞的View的数组
 */

- (void)animatedRemovingDrops:(NSArray *)dropsToRemove
{
    [UIView animateWithDuration:0.5 animations:^{
        
        for (UIView *drop 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;
            drop.center = CGPointMake(x,-y);

        }

    } completion:^(BOOL finished) {

        [dropsToRemove makeObjectsPerformSelector:@selector(removeFromSuperview)];

    }];
}

思考一下


关于通过给view添加物理效果的方法添加动画,需要弄清楚DynamicAnimator,UIDynamicBehavior和遵守<UIDynamicItem>协议的item三者之间的关系。

通过对代码的分析以及讲师的讲解,笔者将这三者以比喻的方法将他们的关系梳理了一下:

  • DynamicAnimator:代表了一个游乐场。
  • UIDynamicBehavior:代表了游乐场里的娱乐设施。
  • 遵守<UIDynamicItem>协议的item:代表了去游乐场玩儿的小孩。

我们从代码看一下如何映射他们的关系:

DynamicAnimator

UIDynamicAnimator *animator =[ [ UIDynamicAnimator alloc] initWithReferenceView:aView]; 

在这里,aView代表了一片空地,这句话的意思是我们把游乐场建在了这片空地上。

UIDynamicBehavior

 [self.animator addBehavior:_dropitBehavior];

在这里,代表了我们在这个游乐场里增加了某个娱乐设施。

遵守<UIDynamicItem>协议的item

- (void)addItem:(id<UIDynamicItem>)item
{
    [self.gravity addItem:item];
    [self.collider addItem:item];
}

在这里,代表了我们让某个小孩来玩儿某个娱乐设施。

这样就理清了:我们要让一个小孩玩儿一个娱乐设施就应该:

  1. 找一片空地建设游乐场。
  2. 在游乐场引进娱乐设备。
  3. 孩子来玩儿这个娱乐设备。

笔者在开始看到这三者的相关代码的时候略懵逼,不知道为什么会这么设计,但是用了“比喻法”之后,顿时豁然开朗了~

最后的话


如果哪位小伙伴想拿到本文Demo的代码请不要客气,在评论里留言即可。
而且十分欢迎给笔者的代码和文笔抛出宝贵的意见和建议~

本文为笔者原创,如需转载,请事先与笔者交涉~

2016.7.12日更新:


笔者已经把目前为止整理的所有Demo(第二课到第十课)放入到了我的GitHub仓库里。分为英文注释版和中文注释版(英文注释要少一点,嘿嘿)想要的小伙伴可以果断下载~ 如果有不知道怎么下载的小伙伴请联系我~

本文已在版权印备案,如需转载请访问版权印。48422928

获取授权

-------------------------------- 2018年7月17日更新 --------------------------------

注意注意!!!

笔者在近期开通了个人公众号,主要分享编程,读书笔记,思考类的文章。

  • 编程类文章:包括笔者以前发布的精选技术文章,以及后续发布的技术文章(以原创为主),并且逐渐脱离 iOS 的内容,将侧重点会转移到提高编程能力的方向上。
  • 读书笔记类文章:分享编程类思考类心理类职场类书籍的读书笔记。
  • 思考类文章:分享笔者平时在技术上生活上的思考。

因为公众号每天发布的消息数有限制,所以到目前为止还没有将所有过去的精选文章都发布在公众号上,后续会逐步发布的。

而且因为各大博客平台的各种限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~

扫下方的公众号二维码并点击关注,期待与您的共同成长~

公众号:程序员维他命
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容