デフォルト引数を使って簡易メソッドを簡単に作る
複数引数を扱うメソッドの簡易メソッドをObjective-Cで用意するには
初めに Objective-C での事例を説明してから、Swift の話しに移った方が分かりやすいと思います。
Objective-C では以下の様にして、簡易メソッドの数だけメソッドを生やす必要がありました。
- (void)function { [self function:nil param2:nil]; } - (void)function:(id)param1 { [self function:param1 param2:nil]; } - (void)function:(id)param1 param2:(id)param2 { //肝心の処理 }
書くのも少し大変ですが、何よりコードを読む時に場合が大変です。
場合によっては、何度もジャンプをして肝心の処理が記述されているメソッドに
ようやくたどり着くなんてこともあります。
Swift におけるオプション引数
Swift では引数にデフォルト値を指定する事が出来ます。
デフォルト値を設定すると、その引数無しでその関数を実行してもエラーが起きないので
それだけで簡易メソッドとして扱う事が出来る様になりました。
以下を Playground などで実行してみると分かると思います。
func function(param1 : AnyObject? = nil, param2 : AnyObject? = nil) { if param1 != nil { println("param1") } if param2 != nil { println("param2") } } function() function(param1: 1) function(param1: 1, param2: 2)
UIScrollViewKeyboardDismissMode について
出たままのキーボードをスクロールされたら良い感じに閉じる
iOS 純正アプリの Messages みたいなキーボードの挙動をやりたいなー、と調べていました。
どういう挙動かというと、
- キーボードが出ている状態でスクロールダウンすると、それに伴ってキーボードも下がる
- ドラッグしながら下げるのをやめて上にあげるとキーボードも戻ってくる
と、大変インタラクティブな挙動になっているのです。
まぁここまでやらなくとも、せめてスクロールビューへのドラッグなどのジェスチャーを検知して
キーボードを dismiss したいと思いました。
iOS 7 以前
iOS 7 より前では、簡単に実装するとすればスクロールビューに以下のようなことを
していてフックしていたかなと思い出しました。
- UIGestureRecognizer をスクロールビューに付与してイベントを取得
- UIScrollViewDelegate を受け取り、スクロールイベントを取得
上記のイベントをフックして、
[textField resignFirstResponder];
を実行していたかと思います。
これでも良いんですが、例えばiOS 純正アプリの Messages のインタラクティブ
なことをやろうとすると非常に手間で独自実装が増えてしまいます。
iOS 7
ここからようやく本題です。
タイトルにもある UIScrollViewKeyboardDismissMode という
UIScrollView に keyboardDismissMode としてプロパティがあります。
デフォルトでは UIScrollViewKeyboardDismissModeNone
が設定されているため、
- インタラクティブしたいなら
UIScrollViewKeyboardDismissModeInteractive
- 単純にドラッグされた時にキーボードを消すだけなら
UIScrollViewKeyboardDismissModeOnDrag
を設定するだけです。
iOS 7 からは、これだけでスクロール時のキーボード表示/非表示を行う事ができます。
参考
UIView の Animation Block の中で行われている事
Animation Block
iOS 4 から UIView のアニメーションを簡単にするために、以下のアニメーションに関するクラスメソッドが UIView に追加されました。
iOS 開発に携わってる人ならみんな知ってると思います。
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations; + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion; + (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion;
上記以外にも iOS 7 からは以下のようなキーフレームや UI Dynamics を用いたアニメーションのためのメソッドも追加されています。
+ (void)addKeyframeWithRelativeStartTime:(double)frameStartTime relativeDuration:(double)frameDuration animations:(void (^)(void))animations; + (void)animateKeyframesWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(UIViewKeyframeAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion + (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion
これらのメソッドのおかげで、とても簡潔に UIView
をアニメーションさせることができるようになりました。
しかし、実際にあの Block の中ではどのような処理が走ってアニメーションが実行されているかが上手に隠蔽され、
あれだけで済んでしまったことによる Core Animation に対する認識不足が自分の中で目立ってきたので今回は調べてみました。
CAAnimation による明示的アニメーション
上記の UIView の簡易 API ができる以前はどのようにアニメーションを実装していたのか、
というそもそもの所に戻ってみるのが近道っぽいです。
以下のコードは Core Animation Programming Guide - Animating Layer Content の章にあるサンプルです。
CABasicAnimation* fadeAnim = [CABasicAnimation animationWithKeyPath:@"opacity"]; fadeAnim.fromValue = [NSNumber numberWithFloat:1.0]; fadeAnim.toValue = [NSNumber numberWithFloat:0.0]; fadeAnim.duration = 1.0; [theLayer addAnimation:fadeAnim forKey:@"opacity"]; // Change the actual data value in the layer to the final value. theLayer.opacity = 0.0;
CAAnimation
を作成し、それを CALayer
に addAnimation:forKey:
することでアニメーションを実行しています。
Animatable Property
少し脇道にそれますが、CALayer
には animatable property というのがあります。以下をみるとわかりますが、ほとんどがそれです。
Core Animation Programming Guide - Animatable Properties
では animatable property とは何なのでしょうか?
animatable property とはその名の通りアニメーション可能な値のことで、
その値を変更すると actionForKey:
が発火して、然るべき CAAction プロトコルに準拠したオブジェクトを返し、暗黙的アニメーションを実行するというプロパティです。
actionForKey:
が CAAction プロトコルに準拠したオブジェクトを返す流れは reference を見るのがわかりやすいです。
特別な定義が無ければ、animatable property と一緒に定義された CAAnimation
が返ります。
ということで、CAAnimation
による明示的アニメーションと animatable property が行う暗黙的アニメーション
について分かったところで UIView
の話に戻ります。
結論
上記でも書きましたが、スタンドアローンの CALayer
の animatable property が変更された場合は、CAAnimation
が発行されて暗黙的アニメーションが実行されます。
しかし UIView
が保持している CALayer に同じことをしても何も起こりません。ただ新しいフレームにアニメーション無しで移るだけです。
なぜかというと UIView
が自身が保持している layer のアニメーション機能をオフにしてるからなんですね。
これは Core Animation Programming Guide - How to Animate Layer-Backed Views にも書かれています。
The UIView class disables layer animations by default but reenables them inside animation blocks.
理由としては Core Text などと同様、 iPhone OS リリース当時のパフォーマンスが芳しくなかったからとかだと想像します。
どうやってオフにしてるかというと、CALayer
が CALayerDelegate
経由で UIView
に CAAnimation
を問い合わせる時に行っています。
で、それを簡単に有効にするために登場したのが、冒頭に書いた UIView Animation Block というわけですね。
上記の事柄は、以下のコードをみると一目瞭然だと思います。
NSLog(@“Animation Block 外: %@", [view actionForLayer:view.layer forKey:@"position"]); [UIView animateWithDuration:1.0 animations:^{ NSLog(@“Animation Block 内: %@", [view actionForLayer:view.layer forKey:@"position"]); }]; — 実行結果 Animation Block 外: <null> Animation Block 内: <CABasicAnimation: 0x8c2ff10>
まとめると、
UIView Animation Block の中で animatable property を変更した場合のみ、CAAnimation
が発行され暗黙的アニメーションが実行されている
ということでした。長々と失礼しました。
参考リンク
Push 通知内の Payload の内容を起動時にデバッグする
Payload
Push 通知には Payload と呼ばれるデータ領域があり、
そこにはシステムがユーザの警告するためのデータや、別用途で用いるためのカスタムデータなどが入っています。
iOS 側での実装
対象のアプリケーションが起動していない状態で、Notification Center 内の通知をタップするなどして起動すると、
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
が走り、launchOptions の中に Push 通知内の Payload のデータが入っているというわけです。
そのデータは以下の様にして取得する事ができます。
launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]
デバッグが困難
では実際に Payload 内におかしなデータが入っている可能性があり、
その内容をみながらデバッガを使ってデバッグしたいという場合があるとします。
しかし、
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
はアプリケーションの起動してすぐに走ってしまうので Xcode でビルドした時に呼ばれてしまい、
起動中に Push 通知を受け取った場合は以下の別のメソッドが呼ばれてしまうのです。
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
解決策
長々と書いてきましたが、以下の様にして簡単に解決出来ます。
Info タブの Launch トグルを Automatically から Wait for ~ to be launched menu に変えるだけです。
こうすることでアプリが実際に指などで起動されるまでデバッガが起動しなくなります。
- 出版社/メーカー: 早川製菓
- 発売日: 2013/12/02
- メディア: 食品&飲料
- この商品を含むブログ (1件) を見る
CADisplayLink について
ユースケース
CADisplayLink を実際に使う例と共にどんなクラスなのか簡単に紹介します。
例えば現在時刻を表示する場合。
画面に表示されている日時を定期的に更新する必要があります。
そのような場合に NSTimer で 0.01 秒など適当なインターバルを設定して更新、みたいなこと実装した経験ありませんか?
僕はあります。
それを解決するための表示されているビューを更新するためのイベントを取得するためのクラス、それが CADisplayLink です。
最近 facebook が OSS 化した pop や、長年 iOS の 2D ゲームフレームワークとして親しまれている cocos2d でも、もちろん使われていました。
CADisplayLink の使い方
CADisplayLink を以下の様にしてセットアップすることで、ディスプレイの更新タイミングをトリガーにしたイベント実行が可能になります。
///1 CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(update:)]; ///2 [link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
画面更新には CADisplayLink
前述したバッドケースとして NSTimer で更新、というのがありました。
これの何が悪いかというと、メインスレッドのロックなどが無ければ、画面の更新は大抵の場合 1/60 秒間隔で行われます。
なので、NSTimer でそれより細かい間隔で更新してもディスプレイには反映されません。
また画面の更新間隔はメインスレッドの具合によって変動するので、 CADisplayLink を用いて画面と関連づけて更新した方が良いというわけです。
ちなみに Building Paper を見て初めて知りましたが、大体メインスレッドで5ms以上処理がかかると、ドロップフレームが起こるらしいです。
facebookの”Building Paper”はすべてのiOSエンジニアがみるべき
CADisplayLink の考え方としてはゲーム開発などのフレームワークなどに良くある update 関数と近いというか同じだと思います。
画面上に描画する処理をユーザからの入力や、システム内のループで行っているものを CADisplayLink に置き換える事でアプリケーションをヌルヌル動かす事ができるようになるかもしれません。
アップル Apple Mini DisplayPort-Dual-Link DVIアダプタ MB571Z/A
- 出版社/メーカー: アップル
- 発売日: 2009/01/14
- メディア: Personal Computers
- この商品を含むブログを見る