本節(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 obj 和 id<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)動畫
添加物理效果主要需要三個元素:
- DynamicAnimator
- UIGravityBehavior
- 遵守<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;
}
這樣就能整齊排列小方塊了:

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)該:
- 找一片空地建設(shè)游樂場。
- 在游樂場引進(jìn)娛樂設(shè)備。
- 孩子來玩兒這個娛樂設(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
-------------------------------- 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)注,期待與您的共同成長~
