jarinosuke blog

about software engineering, mostly about iOS

デフォルト引数を使って簡易メソッドを簡単に作る

複数引数を扱うメソッドの簡易メソッド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 という

APIiOS 7 から追加されています。

UIScrollView に keyboardDismissMode としてプロパティがあります。

デフォルトでは UIScrollViewKeyboardDismissModeNone が設定されているため、

  • インタラクティブしたいなら UIScrollViewKeyboardDismissModeInteractive
  • 単純にドラッグされた時にキーボードを消すだけなら UIScrollViewKeyboardDismissModeOnDrag

を設定するだけです。

iOS 7 からは、これだけでスクロール時のキーボード表示/非表示を行う事ができます。

参考

Text Programming Guide for iOS - Managing the Keyboard

Dismissing the keyboard in a UIScrollView

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 を作成し、それを CALayeraddAnimation: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 リリース当時のパフォーマンスが芳しくなかったからとかだと想像します。

どうやってオフにしてるかというと、CALayerCALayerDelegate 経由で UIViewCAAnimation を問い合わせる時に行っています。

で、それを簡単に有効にするために登場したのが、冒頭に書いた 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 が発行され暗黙的アニメーションが実行されている

ということでした。長々と失礼しました。

参考リンク

Multiple Animations

View Layer Synergy

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

解決策

長々と書いてきましたが、以下の様にして簡単に解決出来ます。

Edit Scheme から対象の Scheme を選び、

Info タブの Launch トグルを Automatically から Wait for ~ to be launched menu に変えるだけです。

f:id:jarinosuke0808:20140618181624p:plain

こうすることでアプリが実際に指などで起動されるまでデバッガが起動しなくなります。

早川製菓 ふなっしー 梨汁ブシャーキャンディ 80g×6袋

早川製菓 ふなっしー 梨汁ブシャーキャンディ 80g×6袋

CADisplayLink について

ユースケース

CADisplayLink を実際に使う例と共にどんなクラスなのか簡単に紹介します。

例えば現在時刻を表示する場合。

画面に表示されている日時を定期的に更新する必要があります。

そのような場合に NSTimer で 0.01 秒など適当なインターバルを設定して更新、みたいなこと実装した経験ありませんか?

僕はあります。

それを解決するための表示されているビューを更新するためのイベントを取得するためのクラス、それが CADisplayLink です。

最近 facebookOSS 化した pop や、長年 iOS の 2D ゲームフレームワークとして親しまれている cocos2d でも、もちろん使われていました。

facebook/pop

cocos2d/cocos2d-iphone

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

アップル Apple Mini DisplayPort-Dual-Link DVIアダプタ MB571Z/A