斯坦福大學(xué)iOS開發(fā)公開課總結(jié)(八) :協(xié)議,block,動畫,俄羅斯方塊Demo

本節(jié)課介紹了協(xié)議,block,動畫的相關(guān)知識,最后結(jié)合了這些知識點展示了一個類似俄羅斯方塊的小游戲Demo。
總體來說本節(jié)課的內(nèi)容比較重要,稍微擺脫了UI層面的知識,對于初學(xué)者來說理解起來不是很容易,不過筆者會盡量詳細(xì)地講解給大家。

協(xié)議


關(guān)于協(xié)議所介紹的知識點比較簡單,而且實現(xiàn)起來相對容易,故不做詳細(xì)介紹,各位可以參考文檔或者相關(guān)博客即可。
在這里只強調(diào)一個知識點:

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

相同點:都表示了某個對象。
不同點
id obj表示obj是具體某一類的實例對象。
id<MyProtocol>obj只表示遵守了某協(xié)議的對象 。

因為有的時候我們并不需要確保某個對象一定是某個類的實例對象,而只需要它遵循了某個協(xié)議,這個時候就需要用第二行的寫法來確保這個對象確實遵循了<MyProtocol>。

Block


關(guān)于block的概念和語法在這里就不贅述了,因為有文檔和很多牛人已經(jīng)總結(jié)地很好了。
在這里只強調(diào)兩點關(guān)于block的使用注意事項。

修改block內(nèi)部變量的方案

如果我們要在block里將found值設(shè)為YES,就應(yīng)該在block外部添加__block關(guān)鍵字。

    __block BOOL found = NO;
    //通過__block關(guān)鍵字,將found從棧中移動到堆中保證其可以被修改;block結(jié)束后,將該變量復(fù)制一份到堆中,再放回棧上

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

        if ([targetString isEqualToString:obj]) {            

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

存儲循環(huán)的解決方案

只要block存在,block內(nèi)部消息中的每個對象都會被block的一個強指針指著。此時,如果這些對象里的某個或幾個對象也有指向該block的指針,就會造成存儲循環(huán)。

問題重現(xiàn):


    //這個block有強指針指向self,而self也通過myBlocks數(shù)組有強指針指向block

    [self.myBlocks addObject:^{    

        [self doSomething];

    }];

解決方案:創(chuàng)建弱類型的局部變量


    __weak ViewController *weakSelf = self; //創(chuàng)建弱類型的局部變量

    [self.myBlocks addObject:^{    

        [weakSelf doSomething];

    }];

Block的應(yīng)用

block可以直接保存在變量中,屬性中,字典和數(shù)組中。

具體使用環(huán)境:

  • 多線程:用于主線程,子線程的回調(diào)。
  • 枚舉:數(shù)組,字典的枚舉等。
  • 通知:某件事情發(fā)生后,信息的傳遞。
  • 錯誤時調(diào)用:“包住”錯誤發(fā)生后需要執(zhí)行的代碼。
  • 成功時調(diào)用:“包住”任務(wù)成功后需要執(zhí)行的代碼。
  • 動畫
  • 排序

通過View改變視圖的屬性來實現(xiàn)動畫


  • 改變frame
  • 改變transform
  • 改變alpha

具體通過UIView的類方法來改變

+ (void)animateWithDuration:(NSTimeInterval)duration   //動畫在這個屏幕上出現(xiàn)的時間
                                     delay:(NSTimeInterval)delay       //等待多長時間再執(zhí)行
                                  options:(UIViewAnimationOptions)options 
                             animations:(void (^)(void))animations  //在此代碼塊中修改frame,transform 和 alpha
                             completion:(void (^ __nullable)(BOOL finished))completion;

options參數(shù):

    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

通過給視圖添加物理效果實現(xiàn)動畫


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

  1. DynamicAnimator
  2. UIGravityBehavior
  3. 遵守<UIDynamicItem>協(xié)議的item(大部分情況是UIView)

DynamicAnimator:動力動畫

UIDynamicAnimator *animator =[ [ UIDynamicAnimator alloc] initWithReferenceView:aView]; //aview是動畫Views的頂級視圖

動力動畫的初始化需要給其添加要進(jìn)行動畫的頂級視圖,詳細(xì)內(nèi)容后面再介紹。

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; //設(shè)置錨點
- (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>協(xié)議的item(大部分情況是UIView)

只要是遵守了<UIDynamicItem>協(xié)議(動力項協(xié)議)的對象,都可以添加動力行為。

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

動力項協(xié)議的屬性:

@property (nonatomic, readwrite) CGPoint center;//動力項的中心

@property (nonatomic, readonly) CGRect bounds; //動力項的繪制區(qū)域,只讀,通過變換,居中,移動進(jìn)行修改

@property (nonatomic, readwrite) CGAffineTransform transform;//動力項的旋轉(zhuǎn)或縮放比例

若想與animator的動畫相抗?fàn)?,需要調(diào)用animator的以下方法:

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

Demo


Demo需求

  • 點擊屏幕后,在頂部隨機位置生成具有隨機色的正方形,正方形顯示后立即下落并停止。
  • 方塊排滿的行會自動被炸飛,而且?guī)赢嫛?/li>

Demo效果圖

左:炸飛前 | 右:炸飛后

重要代碼段

因為每個方塊的動作行為都是一致的,所以在這里自定義了一個UIDynamicBehavior類,給每個方塊增加相同的動作行為。

1. 自定義統(tǒng)一行為類: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];
         //設(shè)置重力加速度
        _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 是動畫實現(xiàn)的頂級視圖,它的子視圖是掉落的方塊
        _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];
}

目前小方塊下落碰到障礙物后會旋轉(zhuǎn),所以容易讓這些小方塊散落成堆。這樣一來,就不能計算好整行的排列情況,所以我們應(yīng)該讓小方塊們沒有旋轉(zhuǎn)的特性。

5.取消旋轉(zhuǎn)特性

在公用的behavior類DropItBehavior里增加一個UIDynamicItemBehavior實例,取消其旋轉(zhuǎn)特性。

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

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

這樣就能整齊排列小方塊了:

左:可旋轉(zhuǎn) | 右:不可旋轉(zhuǎn)

6. 動畫炸掉排滿的行

最好在方塊都靜止了之后再判斷是否有排滿的行,這里需要遵守協(xié)議<UIDynamicAnimatorDelegate>


/**
 *  監(jiān)聽動力動畫內(nèi)部的所有動畫停止后調(diào)用炸飛整行的方法
 *
 *  @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的數(shù)組
 */

- (void)animatedRemovingDrops:(NSArray *)dropsToRemove
{
    [UIView animateWithDuration:0.5 animations:^{
        
        for (UIView *drop in dropsToRemove) {
           
            //設(shè)定炸飛后終點的位置
            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)];

    }];
}

思考一下


關(guān)于通過給view添加物理效果的方法添加動畫,需要弄清楚DynamicAnimator,UIDynamicBehavior和遵守<UIDynamicItem>協(xié)議的item三者之間的關(guān)系。

通過對代碼的分析以及講師的講解,筆者將這三者以比喻的方法將他們的關(guān)系梳理了一下:

  • DynamicAnimator:代表了一個游樂場。
  • UIDynamicBehavior:代表了游樂場里的娛樂設(shè)施。
  • 遵守<UIDynamicItem>協(xié)議的item:代表了去游樂場玩兒的小孩。

我們從代碼看一下如何映射他們的關(guān)系:

DynamicAnimator

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

在這里,aView代表了一片空地,這句話的意思是我們把游樂場建在了這片空地上。

UIDynamicBehavior

 [self.animator addBehavior:_dropitBehavior];

在這里,代表了我們在這個游樂場里增加了某個娛樂設(shè)施。

遵守<UIDynamicItem>協(xié)議的item

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

在這里,代表了我們讓某個小孩來玩兒某個娛樂設(shè)施。

這樣就理清了:我們要讓一個小孩玩兒一個娛樂設(shè)施就應(yīng)該:

  1. 找一片空地建設(shè)游樂場。
  2. 在游樂場引進(jìn)娛樂設(shè)備。
  3. 孩子來玩兒這個娛樂設(shè)備。

筆者在開始看到這三者的相關(guān)代碼的時候略懵逼,不知道為什么會這么設(shè)計,但是用了“比喻法”之后,頓時豁然開朗了~

最后的話


如果哪位小伙伴想拿到本文Demo的代碼請不要客氣,在評論里留言即可。
而且十分歡迎給筆者的代碼和文筆拋出寶貴的意見和建議~

本文為筆者原創(chuàng),如需轉(zhuǎn)載,請事先與筆者交涉~

2016.7.12日更新:


筆者已經(jīng)把目前為止整理的所有Demo(第二課到第十課)放入到了我的GitHub倉庫里。分為英文注釋版和中文注釋版(英文注釋要少一點,嘿嘿)想要的小伙伴可以果斷下載~ 如果有不知道怎么下載的小伙伴請聯(lián)系我~

本文已在版權(quán)印備案,如需轉(zhuǎn)載請訪問版權(quán)印。48422928

獲取授權(quán)

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

注意注意!?。?/strong>

筆者在近期開通了個人公眾號,主要分享編程,讀書筆記,思考類的文章。

  • 編程類文章:包括筆者以前發(fā)布的精選技術(shù)文章,以及后續(xù)發(fā)布的技術(shù)文章(以原創(chuàng)為主),并且逐漸脫離 iOS 的內(nèi)容,將側(cè)重點會轉(zhuǎn)移到提高編程能力的方向上。
  • 讀書筆記類文章:分享編程類,思考類心理類,職場類書籍的讀書筆記。
  • 思考類文章:分享筆者平時在技術(shù)上,生活上的思考。

因為公眾號每天發(fā)布的消息數(shù)有限制,所以到目前為止還沒有將所有過去的精選文章都發(fā)布在公眾號上,后續(xù)會逐步發(fā)布的。

而且因為各大博客平臺的各種限制,后面還會在公眾號上發(fā)布一些短小精干,以小見大的干貨文章哦~

掃下方的公眾號二維碼并點擊關(guān)注,期待與您的共同成長~

公眾號:程序員維他命
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容