XCTestExpectationの「API violation」エラーの解決策
XCTestExpectation
XCTestで非同期通信処理をテストしたいときに使えるクラスです。
XCTestExpectation *expectation = [self expectationWithDescription:@"successfully load image"]; [self.imageLoader getRequest:@"http://dummy.jpg/" success:^(UIImage* image){ XCTAssertNotNil(image); [expectation fulfill]; } failure:^(NSError *error){ XCTFail(@"image should be loaded."); }]; [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { XCTFail(@"time over."); }];
expectationを定義したあと、waitForExpectationsWithTimeout:handler:で指定した秒数の間、通信の処理を待つことができます。
上記の例だと5.0秒経ってしまうと「time over」となりエラー。
無事非同期通信が終わり、success:に結果が返ってきたら「fulfill」することで、非同期通信処理を完了させています。
API violationエラー
すべてのUnit Testを流しているときに、非同期通信まわりでよく下記のようなエラーに遭遇しました。
CRASH: API violation - multiple calls made to -[XCTestExpectation fulfill].
どうやら複数回「fulfill」を呼ぼうとしているようなエラーメッセージです。
CRASH: API violation - called -[XCTestExpectation fulfill] after the wait context has ended.
こういうのもありました。waitが終わってるのにfulfillを呼ぶといったエラーです。
どういう場合に起こるのか
1つ目は、たとえば画像を一度に5枚取得しようとして、返り先が同じメソッドだったりすると、「fulfill」が何度も呼ばれることがあります。こういう場合に起こり得ます。
2つ目は、waitで指定した時間以上経ったあとに「fulfill」を呼ぶと発生します。どうやら、waitで指定した時間が経つと、expectationがnilになるようです。
これらのエラーに遭遇したときに厄介だったのが、起こったり起こらなかったり、起こるテストがころころ変わったりするところです。一体どこが悪いのか探すのに苦労しました。
解決策
@property (nonatomic, weak) XCTestExpectation *imageLoaderExpectation;
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"successfully load image"];
このようにexpectationを宣言するときに、「__weak」を指定してあげると直りました。fulfillが一度呼ばれたタイミングやwaitの時間が経ったときに、expectationがうまく解放され、2回呼ばれたりnilのまま残ったりといったことがなくなるのだと思います。
UIGravityBehaviorとUICollisionBehaviorでバウンドする開閉メニューをつくる
UIGravityBehavior
UIViewに重力を与えることができます。重力を与える方向や強さの調整も可能。
UICollisionBehavior
UIViewが衝突したときの挙動を与えられます。ここのラインにきたらバウンドさせるという動きができます。
事前準備
@property (nonatomic, strong) UIDynamicAnimator *animator; @property (nonatomic, strong) UIView *menuView;
UIDynamicAnimatorのプロパティを作っておきます。このプロパティがアニメーションを制御してくれます。
menuViewにはボタンをおしたら上から落ちてきて一定のラインでバウンドして止まる動きをつけます。
落下させたいViewに重力を与える
UIGravityBehavior *gravityBeahvior = [[UIGravityBehavior alloc] initWithItems:@[self.menuView]]; gravityBeahvior.magnitude = 3.0; [self.animator addBehavior:gravityBeahvior];
落下させたいViewをinitWithItemsで指定し、UIGravityBehaviorを作成します。
「magnitude」で力加減を調整できます。
「animator」に「addBehavior」すれば重力を与えるビヘイビアの登録が完了です。
ちなみにこれだけだと、下にすーっと落下していきます。
UIGravityBehaviorには「angle」というプロパティがあり、ここでどの方向に重力を与えるか指定できます。
バウンドさせる場所を指定する
CGFloat collisionLine = 150; UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.menuView]]; [collisionBehavior addBoundaryWithIdentifier:@"bottom" fromPoint:CGPointMake(0, collisionLine) toPoint:CGPointMake(self.view.bounds.size.weight, collisionLine)]; [self.animator addBehavior:collisionBehavior];
「collisionLine」で衝突させる位置を指定します。
上記の設定だと(0, 150)と(320, 150)をつなぐラインで衝突させることができます。
高さを調整して、Tabbarの真上とかに設定すると、上から落ちてきたメニューがTabbarのちょうど真上でバウンドして止まるようになります。
dynamicAnimatorDidPause でアニメーションが終わった瞬間をキャッチ
「UIDynamicAnimatorDelegate」に以下のふたつのメソッドがあります。
- dynamicAnimatorWillResume
- アニメーションが再開するときに呼ばれる
- dynamicAnimatorDidPause
- アニメーションが終わったときに呼ばれる
基本的にビヘイビアをaddしたら、それをremoveするまでずっとビヘイビアが有効です。
上記の例だと、指定したラインでバウンドして止まったらアニメーションが一旦終了になり、「dynamicAnimatorDidPause」が呼ばれますが、またself.menuViewが上のほうに移動すると落下します(常に重力がかかっているといるので)。こういった、もう一度アニメーションが再開するときに呼ばれるのが「dynamicAnimatorWillResume」です。
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator{ [self.animator removeAllBehaviors]; }
1回ビヘイビアの動作が終了したら繰り返さなくてもいいやっていうときは、ビヘイビアを忘れずにremoveしておきます。
また動作させたいときにビヘイビアをaddすれば同様に動きます。
まとめ
UIGravityBehaviorでUIViewに重力を与え、UICollisionBehaviorで指定した位置で衝突させることができます。メニューの動きにちょっとした仕掛けを施したいときに簡単につかえるのでぜひ。
UIViewをドラッグした後、元の位置に戻す
UIViewをドラッグさせる
touchesBeganやtouchesMovedでもできそうですが、今回は「UIPanGestureRecognizer」を使ってみました。
self.snapView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width/2, self.view.bounds.size.height/2)]; self.snapView.center = self.view.center; self.snapView.backgroundColor = [UIColor blueColor]; [self.view addSubview:self.snapView]; UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dragged:)]; [self.snapView addGestureRecognizer:panGesture];
Viewを作って画面の真ん中に配置させます。
そしてUIPanGestureRecognizerでジェスチャーをViewに登録します。
ジェスチャー(今回はドラッグ)されたときの処理をactionに指定。
-(void)dragged:(UIPanGestureRecognizer*) sender{ if(sender.state == UIGestureRecognizerStateEnded){ [self snap]; }else{ UIView *draggedView = sender.view; CGPoint delta = [sender translationInView:draggedView]; CGPoint movedPoint = CGPointMake(draggedView.center.x + delta.x, draggedView.center.y + delta.y); draggedView.center = movedPoint; [sender setTranslation:CGPointZero inView:draggedView]; } }
「translationInView:」で元の位置との相対位置を取得します。
そこから移動先の真ん中の座標を割り出し、Viewの位置を変更します。
これで、Viewをドラッグする処理ができました。
特定の位置に吸い付かせる
「UISnapBehavior」を使って、元いた位置に戻してみます。
-(void)snap{ [self.animator removeBehavior:self.snapBehavior]; CGPoint centerPoint = CGPointMake(self.view.center.x, self.view.center.y); self.snapBehavior = [[UISnapBehavior alloc] initWithItem:self.snapView snapToPoint:centerPoint]; [self.animator addBehavior:self.snapBehavior]; }
「snapToPoint」で指定した場所に吸い付くように移動します。
さきほどのジェスチャーにて、「UIGestureRecognizerStateEnded」でドラッグが終わったあとに上記のSnapの処理がはしるようにすれば完成。
UIPushBehaviorでViewに力を与える
UIPushBehavior
iOS7から追加されたUIKit Dynamicsに含まれるビヘイビアのひとつです。
UIKit Dynamics には重力をあたえたり、ViewとViewが衝突した際の動作であったり、Viewが指定した位置に吸い付いたりするアニメーションを作ることができます。
ゲーム用のSprite Kitとは違い、UIKit用の2次元物理エンジンとなります。
UIPushBehavior
「UIPushBehavior」を使ってみたので、その使い方を紹介します。
UIPushBehaviorを使うと、Viewに力を加えることができます。
事前準備
UIKit Dynamics を使うための事前準備です。
ヘッダーにこれらを宣言しておきます。
@property (nonatomic, strong) UIDynamicAnimator *animator; @property (nonatomic, strong) UIPushBehavior *push;
ボディには初期設定および力を与える対象のViewを作っておきます。
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view]; UIView *test = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)]; test.backgroundColor = [UIColor redColor]; [self.view addSubview:test];
等速運動
self.push = [[UIPushBehavior alloc] initWithItems:@[test] mode:UIPushBehaviorModeInstantaneous]; [self.push setPushDirection:CGVectorMake(1.0, 0.0)]; [self.animator addBehavior:self.push];
「UIPushBehaviorModeInstantaneous」は一瞬力を加えるモードです。
「setPushDirection」でどの方向にどれくらい力を加えるか指定できます。
上記の場合だと、X軸に1.0, Y軸に0.0の力が与えられます。
最後に、UIDynamicAnimatorオブジェクトにビヘイビアを登録してあげれば動きますよ。
等加速度運動
self.push = [[UIPushBehavior alloc] initWithItems:@[test] mode:UIPushBehaviorModeContinuous]; [self.push setPushDirection:CGVectorMake(1.0, 0.0)]; [self.animator addBehavior:self.push];
「UIPushBehaviorModeContinuous」を指定すると、延々と指定した力が加えられていきます。
したがって上記の場合だと、X軸方向にどんどん速くなって動きます。
UIKit Dynamics Catalog
Appleがサンプルコードを公開しています。とても参考になるのでぜひ。
ボタンを押すとボタンがバウンドするようなものもあります。
https://developer.apple.com/LIBRARY/IOS/samplecode/DynamicsCatalog/Introduction/Intro.html
Swiftの文字列操作まとめ
空文字のチェック
「isEmpty」を使えばOK。空ならtrueが返ってきます。
var empty = "" var empty2 = String() //Empty. if empty.isEmpty{ println("Empty.") }else{ println("Not Empty.") } //Empty. if empty2.isEmpty{ println("Empty.") }else{ println("Not Empty.") }
文字列の連結
「+」でつなげてあげるだけ。
NSMutableString使ったり、[NSString stringWithFormat:@"%@%@", str1, str2] こんなことしなくていい!
var greeting = "Hello " let name = "Tom" greeting + name //Hello Tom
文字列を一文字ずつ取り出す
for in で取り出すと一文字ずつ取り出せます。楽ちん。
for char in "Hello!"{ println(char) }
print文内で変数を使う
バックスラッシュとカッコで囲んであげると変数が差し込めます。
日本語キーボード使ってるかたは、option + ¥マークでバックスラッシュ入力できます。
let name2 = "Tom" println("Hello \(name2).") //Hello Tom. println("1たす2は\(1+2)") //1たす2は3
文字数をカウントする
「countElements」というメソッドに文字列を渡してあげると、文字数が返ってきます(Int)。
let longMessage = "こんにちは。今日は天気がいいです。" countElements(longMessage) //17
文字列が等しいかどうか比較する
「==」で比較できます。
var str1 = "こんにちは" var str2 = "こんばんは" if str1 == str2 { println("同じ文字列") }else{ println("違う文字列") //こちら。 } str2 = "こんにちは" if str1 == str2 { println("同じ文字列") //こちら。 }else{ println("違う文字列") }
文頭/文末に特定の文字列が含まれているか確認する
「hasPrefix」を使うと文頭に含まれているかどうかをチェックできます。
「hasSuffix」を使うと文末に含まれているかどうかをチェックできます。
var greetings = ["おはよう", "こんにちは", "こんばんは"] for message in greetings { if message.hasPrefix("こん") { println(message) //こんにちは, こんばんは } if message.hasSuffix("ちは"){ println(message) //こんにちは } }
アルファベットを大文字/小文字にする
「uppercaseString」で大文字に、「lowercaseString」で小文字にできます。
let lowerStr = "hello" lowerStr.uppercaseString //HELLO let upperStr = "HELLO" upperStr.lowercaseString //hello
いろいろ便利になってますね。
Swiftでは絵文字を変数として扱える
(スクショはNDA的にダメみたいなので消しました。また後日のっけます)
公式ドキュメントにもありましたが、絵文字を変数として使えます。なんだか面白い。
Swiftの「Optional Value」の使い方
Optional Value
Swiftのコードを読んでいると変数の横に「?」や「!」がついていることがあります。
この「?」がついているものが「Optional Value」になります。
Optional Value の宣言
var optionalValue:String? = "オプショナルです"
型の横に「?」をつけるだけです。
これでnilも代入可能なString型のオブジェクト完成です。
var normalValue:String = "オプショナルじゃない" normalValue = nil //error var optionalValue:String? = "オプショナルです" optionalValue = nil //OK
実際は、Optional<Int>のような型宣言になりますが、「?」をつけるだけでOKです。
nilを許容していないオブジェクトに対してnilを代入しようと、「Could not find an overload for '__conversion' that accepts the supplied arguments」というエラーがでます。
Optional Value を代入するときの注意
Optionalじゃない変数にOptional Valueを代入しようとすると、エラーになります。
let number = "123" let convertedNumber = number.toInt() var intNumber:Int = convertedNumber //エラー
「Value of optional type 'Int' not unwrapped; did you mean to use '!' or '?' ?」というエラーがでます。
「toInt()」でString型をInt型に変換できます。たとえば上記のような"123"はInt型に正しく変換できますが、"Hello, world"などの文字列はInt型に変換できませんよね。そんな場合はnilとして変換されます。
つまり、toInt()で変換された変数は「nilであるかもしれない」のでOptional Valueとなります。
Optional Valueを代入したいときは、代入先をOptionalにしてあげればOKです。
let number = "123" let convertedNumber = number.toInt() var intNumber:Int? = convertedNumber //OK
nilを許容しない変数にOptional Valueを代入したいときはどうすればよいでしょうか。
そこで、オブジェクトが必ずnilじゃない保証があるという前提で、「このオブジェクトはnilではありませんよ」と明示的に宣言するために「!」を使います。
let number = "123" let convertedNumber = number.toInt() var intNumber:Int = convertedNumber! //OK convertedNumberはnilじゃない!
実際はImplicitlyUnwrappedOptional<Int>型になっているようです。
Optionalかどうかを判定する
Optionalかどうかを判定して条件分岐したいときは下記のようにするとよいです。
let number = "123" let convertedNumber = number.toInt() if let actualNumber = convertedNumber { println("\(actualNumber)はnilじゃないです。") }else{ println("nilでした。") }