NSBlogger

意識高いブログ

WatchKit AppからiPhone Appを起動する方法

Watch App側の実装

[WKInterfaceController openParentApplication:@{} reply:^(NSDictionary *replyInfo, NSError *error) {}];

WKInterfaceControllerのopenParentApplication:reply:メソッドを使うことでiPhone側の親アプリを起動することができます。現在のところ、親アプリのみ起動が可能です。
一つ目の引数に渡したいデータを入れればOK。

iPhone App側の実装

- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply

Watch Kit App からiPhone側の親アプリが起動されたとき、application:handleWatchKitExtensionRequest:reply:メソッドが最初に呼ばれます。渡ってきたデータをごにょごにょできますね。
URLスキームを設定していれば、特定のページへ遷移させることも可能です。

iOS Simulatorの調子が悪くなったときにやること

iOS Simulatorが調子悪いとき

たまにXcodeでアプリを実行したとき、iOS Simulatorにアプリのインストールができなくなることがあります。
f:id:Kamekiti:20150213222509p:plain
Unable to run app in Simulator. An error was encountered while running (Domain = LaunchServicesError, Code = 0)」
たとえばこういうのですね。
一体何が悪いのかわかりません。
とにかくエラーにぶち当たったことだけが警告として表示されます。

iOS Simulatorの機嫌を取り戻す

iOS Simulatorを再起動するとたいがい直りますが、それでもうまくいかないことがあったりします。そんなときは下記の手順を踏むとiOS Simulatorの機嫌がよくなります。

  1. Xcodeの「Window」→「Organizer」→「Projects」→「Derived Data」を「Delete」
  2. Xcodeの「Product」→「Clean」
  3. XcodeおよびiOS Simulatorを終了
  4. Xcodeを再起動してプロジェクトを実行

Xcodeが調子悪くて、コードは間違っていないのにビルドエラーを吐きまくるときも有効です。
iOS Simulator、iOS7 あたりからやけに不安定ですよねー。

AFNetworkingでメモリリークが起こったときの対処法

AFNetworking

Objective C のライブラリで超ど定番なHttpClientです。いまさらながら使い始めました。AFNetworking/AFNetworking · GitHub

メモリリーク

f:id:Kamekiti:20150209233708p:plain
AFNetworkingのラッパーを作って既存のAPIとの通信をすべて置き換えました。一応メモリリークを見ておこうと思ってInstrumentsで測定したら、上記の通りメモリリークが多発してました。

解決策

@property (nonatomic, strong) AFHTTPSessionManager *sessionManager;

[self.sessionManager invalidateSessionCancelingTasks:YES];

当然なのですが、AFHTTPSessionManagerAFURLSessionManagerでinvalidateSessionCancelingTasks:を呼んでセッションを無効化しておく必要がありました。明示的に呼んであげないと、AFHTTPSessionManagerが解放されないためメモリリークになっていました。

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