2014年12月26日金曜日

Carthage で Code Sign Identity および Provisioning Proifleを直接指定してビルドする方法

誰得情報なのでメモだけ残しておきます。
手元のMacに複数のCode Sign Identityがあってビルドが失敗する人向けです。

akisute Test$ carthage update
*** Fetching SwiftState
*** Fetching SwiftTask
*** Checking out SwiftState at "1.1.1"
*** Checking out SwiftTask at "2.4.0"
*** xcodebuild output can be found in /var/folders/gk/205sh3lx1qdfrwtxcb_tj97m0000gp/T/carthage-xcodebuild.uzBMvq.log
*** Building scheme "SwiftState-iOS" in SwiftState.xcodeproj
*** Building scheme "SwiftState-OSX" in SwiftState.xcodeproj
*** Building scheme "SwiftTask-iOS" in SwiftTask.xcworkspace
*** Building scheme "SwiftTask-OSX" in SwiftTask.xcworkspace
** BUILD FAILED **


The following build commands failed:
Check dependencies

(1 failure)

akisute Test$ tail -n 10 /var/folders/gk/205sh3lx1qdfrwtxcb_tj97m0000gp/T/carthage-xcodebuild.uzBMvq.log
** BUILD SUCCEEDED **

Build settings from command line:
    SDKROOT = macosx10.10

=== BUILD TARGET SwiftState-iOS OF PROJECT SwiftState WITH CONFIGURATION Release ===

Check dependencies

Code Sign error: Multiple matching codesigning identities found: Multiple codesigning identities (i.e. certificate and private key pairs) matching “iPhone Developer” were found.



こういうときは環境変数を使って、
JP11688 kaa$ CODE_SIGN_IDENTITY="iPhone Developer: Masashi Ono" carthage update
*** Fetching SwiftState
*** Fetching SwiftTask
*** Checking out SwiftState at "1.1.1"
*** Checking out SwiftTask at "2.4.0"
*** xcodebuild output can be found in /var/folders/gk/205sh3lx1qdfrwtxcb_tj97m0000gp/T/carthage-xcodebuild.5z3CLW.log
*** Building scheme "SwiftState-iOS" in SwiftState.xcodeproj
*** Building scheme "SwiftState-OSX" in SwiftState.xcodeproj
*** Building scheme "SwiftTask-iOS" in SwiftTask.xcworkspace
*** Building scheme "SwiftTask-OSX" in SwiftTask.xcworkspace


または、
JP11688 kaa$ PROVISIONING_PROFILE="XXXX-XXXX-XXXX-XXXX" carthage update
*** Fetching SwiftState
*** Fetching SwiftTask
*** Checking out SwiftState at "1.1.1"
*** Checking out SwiftTask at "2.4.0"
*** xcodebuild output can be found in /var/folders/gk/205sh3lx1qdfrwtxcb_tj97m0000gp/T/carthage-xcodebuild.5z3CLW.log
*** Building scheme "SwiftState-iOS" in SwiftState.xcodeproj
*** Building scheme "SwiftState-OSX" in SwiftState.xcodeproj
*** Building scheme "SwiftTask-iOS" in SwiftTask.xcworkspace
*** Building scheme "SwiftTask-OSX" in SwiftTask.xcworkspace

参考: http://stackoverflow.com/questions/9264727/code-sign-identity-parameter-for-xcodebuild-xcode4
参考: https://github.com/Carthage/Carthage/issues/235

Container View Controllerを作ってみよう

今日は冬休みの工作ということで、iOSのContainer View Controllerを作ってみようと思います。

Container View Controllerとは

一言で言うと、他のUIViewControllerを包含して表示するUIViewControllerのことです。どのように包含して表示するかによって、たとえばUINavigationControllerやUITabBarController、UIPageViewControllerのような実装があります。

iOS 5からはこのContainer View Controllerを自作する事が可能になりましたが、実装が面倒なのと大体の場合においてUIKitが用意しているContainer View Controllerを使うかcocoapodsあたりからそれっぽいライブラリを拾ってくれば解決するためかあまり具体的な実装方法が話題になっていないようです。今回たまたま作る機会があったのでその時の内容をメモしておこうかと思います。

Container View ControllerへのView Controllerの追加

Container View Controllerを作るには、UIViewControllerを継承したクラスを作成して、そこに- (void)addViewController:(BOOL)animatedのような管理対象のView Controllerを追加するメソッドを作ってやればよいです。
ここで、Container View ControllerにView Controllerを追加する上で基本的にやらなくてはならないことは以下の5ステップにわかれます。
  1. addChildViewController:
    • このタイミングでContainer View Controllerに対象のView Controllerが格納されます。ただしViewはまだ表示されません。
  2. didMoveToParentViewController:
    • Container View Controllerに対象のView Controllerが格納されたことを通知します。
  3. beginAppearanceTransition:animated:
    • これからViewが表示されることをContainer View Controllerおよび対象のView Controllerに通知します。viewWillAppearに相当します。
  4. addSubview:
    • 実際にViewを表示します。必要に応じてアニメーションもつけます。
  5. endAppearanceTransition
    • Viewの表示が完了したことをContainer View Controllerおよび対象のView Controllerに通知します。viewDidAppearに相当します。

実際のサンプルコードはこちら。


Container View ControllerからのView Controllerの削除

View Controllerに追加した時に実施したことを逆順に実施してやればOKです。- (void)removeViewController:(BOOL)animatedのようなメソッドを作ってやって、そこに実装を書けばよいでしょう。
サンプルコードにするとこんな感じになります。


shouldAutomaticallyForwardAppearanceMethodsの設定

先に答えだけいうと、何もしないでいいです。このメソッドの存在そのものを忘れて構いません。

iOS 6から追加されたメソッドで、overrideして使用します。このメソッドがYESを返すときは、Container View Controller自身が他のContainer View Controllerに追加されるなどして表示されviewWillAppear/viewDidAppearが呼び出されるタイミングで自動的にchildViewControllersに対してもviewWillAppear/viewDidAppearを呼び出します。NOの場合は自動的に呼び出されないため手動でchildViewControllersの表示状態を管理し、適時beginAppearanceTransition:animated:を呼び出す必要があります。

デフォルトはYESで、基本的にはデフォルトのまま使えば問題ありません。NOを返したいケースは、たとえばContainer View Controllerが画面に表示されてから一瞬遅れてchildViewControllersをアニメーションしながら表示したいなどの要件がある場合に限られるでしょう。

2014年12月23日火曜日

ReactiveCocoa を Swift から使ってみた

FRP(Functional Reactive Programming)なるものが流行っているらしいので、私もたまには流行に乗っかってみることにしました。手始めにReactiveCocoaをSwiftで一日ほど使ってみました。

導入

こちらのブログにまとまっていますので、そちらを参照していただければ良いかと。基本的にはCocoaPodsで一発です。
http://tnakamura.hatenablog.com/entry/2014/11/15/how_to_use_reactivecocoa_in_swift

ドキュメント

基本的にはプロジェクトのGitHubにしっかりドキュメントがあるのでそれを見ればだいたい大丈夫かなと思います。
https://github.com/ReactiveCocoa/ReactiveCocoa
https://github.com/ReactiveCocoa/ReactiveCocoa/tree/master/Documentation
APIの使い方がわからなければヘッダファイルを見れば相当詳細にコメントがついているのでそれでほぼ問題無いです。それでもわからなければ結構ググればかなりヒットします。熱狂的なファンがいるようです。

概念

こちらのページに非常に詳細に書いてあるのでお勧めです。こちらを見るだけでなんとなく概念がつかめてReactiveCocoaは何をするフレームワークなのかがわかって良いと思います。以下主要なクラスに対して私が理解した内容です(間違ってたらゴメンナサイ)。

RACStream

連続した値を表すすべての既定概念。連続した値というのは今現在すぐに返せる値も通信や計算などによって将来的に返される値も含む。関数型言語で言うところのモナドらしいけどモナドはさっぱり。

RACSequence

RACStreamの実装の一つで、Pull-Baseなもの。すなわちユーザが値を要求して、それに対して値を返すオブジェクト。関数型言語でいうところのリストとかシーケンス。

RACSignal

RACStreamの実装の一つで、Push-Baseなもの。すなわちシステムが何らかのイベントやタイミングに応じて値を返すオブジェクト。もちろん値が返ってくるのは直後かもしれないし遠い未来かもしれない。

RACSubscription

RACSignalに対するコールバックみたいなもの。Promiseパターンのthenとかcatchとかfinallyみたいなもの。

RACCommand

UIBarButtonItemとかUIButtonなどのユーザーインタラクションを表すクラスみたいです。まぁ要するにIBActionみたいなもののようです。加えてRACSignalを使ってenabledの状態をコントロールしたりsenderをfilter/map/reduce/その他いろいろ加工可能。

RACTuple

引数とか返り値とかでよく使われるタプル。Swiftのtupleとは違うので注意。

だいたいこれぐらいわかっていたらコードが書けました。

実験

単純なメモアプリを作って実験しようと思いとりあえずこんなテーブルビューを作ってみました。

そしたら問題が出るわ出るわ。

マクロが使えない

Swiftからだと便利なマクロが使えないので非常に困ります。さらに次に挙げる一部の問題はマクロがないと解決できません。

配列の内容変化に対してRACSignalを取れない

オブジェクトの変化を監視するRACSignalがKVOを元に実装されていて、KVOがArrayに対して使用できないので当然なんですが、これのせいでいきなりReactiveCocoaの世界からいつものCocoaの世界に引き戻されます。
良い対処法はないようです。一応Objective-Cならマクロとか使って対応可能なように見えますがSwiftではどうしようもありませんでした。
https://github.com/ReactiveCocoa/ReactiveCocoa/issues/500
https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1197

delegateパターンをRACSignalに変換するのが厄介

RACSubjectという自分で自由自在に状態を操作できるRACSignalのサブクラスを使ってシグナルをコントロールする方法がまずお手軽です。

またはこちらのブログで紹介されているrac_signalsForSelectorなどを使う方法もあります。
http://spin.atomicobject.com/2014/02/03/objective-c-delegate-pattern/

内部的に黒魔法が多い

先ほどのrac_signalsForSelectorもそうですが、内部で平然とmethod swizzlingを使い放題使いまくっていたりするのでちょっと怖いです。


まとめ

Objective-CでFRPの勉強をするにはちょうどいいんじゃないでしょうか。

2014年12月19日金曜日

SwiftからCやObjective-Cのライブラリを扱うときのテクニック数点

Objective-C Bridging Headerを利用することで、Swiftは既存のいかなるC/Objective-Cコードのシンボルでも呼び出すことが可能になっています。しかしながら場合によってはSwift単体では素直に書きづらいハマりどころがあります。C/Objective-Cのラッパーを作り、Objective-C Bridging Header経由でSwiftから呼び出せば全ての問題は解決できるのですが、面倒くさいですしやはりSwift単体で何とかしたいですよね。そこでここでは素直に書きづらいハマりどころと、それを何とかしてSwift単体で解決する方法をご紹介します。

※以下の情報は2014/12/19現在のものです。Swiftは言語仕様の変化が激しいので予期せず変更されている場合があります、ご了承ください。

1. ARCに管理されていないObjective-Cオブジェクトを扱う

例えばKeychainを扱うAPIなどで、ARCに管理されていないObjective-Cオブジェクトを扱うことがあります。このような場合にはUnmanaged型を使用します。

Unmanaged型はメソッド経由でretain, release, autoreleaseを行うことができるほか、takeUnretainedValue()またはtakeRetainedValue()メソッド経由でT型のSwiftオブジェクトを取り出すことができます。

2. C言語のポインタを扱う

生のC言語のポインタを扱う場合、たいていのケースではUnsafePointer型を使うようにCのAPIがSwiftのAPIに変換されます。このとき、Swift上ではUnsafePointer型を要求するのに、C言語のAPI的にはNULLを渡したい場合には、nilを渡すことができないので、代わりにUnsafePointer.null()を渡すことができます。

UnsafePointer以外にもCOpaquePointer型やCFunctionPointer型に変換されるAPIもありますが、この場合も同様にCOpaquePointer.null()やCFunctionPointer.null()をAPIに渡してやればうまくいきます。

3. cStringUsingEncoding()メソッドの罠に注意する

SwiftにはSwiftネイティブのString型とCocoaのNSString型が存在します。基本的にはこの2つは自動的にうまい具合にブリッジされるためプログラマは違いを意識する必要がありませんが、実はcStringUsingEncoding()メソッドを使う場合にはこれが重大な問題になってきます。String.cStringUsingEncoding()は[CChar]?を、NSString.cStringUsingEncoding()はUnsafePointerを返すのです。さらにコンパイラは文字列をStringとして解釈するのを優先するため、先ほど述べたC言語のAPIに渡す際に型が合わないという理由でエラーになりがちです。

対策として上記の通り明示的にNSStringにキャストすることをおすすめします。

4. enum値のOR結合を何とかする

すみません、なんともなりませんでした(´・_・`)
例えば以下のようなコードを書くこと自体は可能なのですが、適切なenum値を得ることができません。optionsはnilになってしまいます。

どうしてもOR結合が必要なenum値が存在する場合は、現状C/Objective-Cでラッパーを作りObjective-C Bridging Header経由でSwiftから呼び出すしかないようです。


2014年9月30日火曜日

GitHub:Enterprise 環境下では CocoaPods を 0.34.0 以上にアップデートしてはいけません

タイトルの地点で結構出落ちですが、本日少々厄介な問題にぶち当たってしまったので共有いたします。

発生条件

CocoaPods 0.34.0以上を使用し、GitHub:EnterpriseのgitリポジトリをPodSpecないしSourceリポジトリとして使用している場合に発生します。

発生する問題

CocoaPodsのgitを利用するアクションが全く成功しなくなります。具体的にはpod install, pod updateなどが全く通りません。

問題の原因

公式ブログにあるとおり、0.34.0以上からCocoaPodsは高速にgitリポジトリをcloneするため--depth 1 (shallow clone) オプションを採用するようになっています。しかしながらGitHub:Enterprise側の技術的問題なのか、GitHub:Enterprise内のgitリポジトリに対するhttpないしhttps経由でのshallow cloneが成功しないため永遠にpod installやpod updateが終わりません。

対処方法

根本的にはCocoaPodsかGitHub:Enterpriseかいずれかが修正されるのを待つしかありませんが、CocoaPods側としてはあまり積極的に対応する気がないようなので、GitHub:Enterprise側に対応を期待するしかないでしょうか。もちろん全てのGitHub:Enterprise環境下で発生するとは限りませんが、いずれにせよ注意する必要があります。

2014/10/01追記

GitHub:Enterpriseに問い合わせてみたところ、こちらは既知の問題であり現在修正中で、次のリリース時に修正されるそうです。次のリリースが社内環境にデプロイされるまでの辛抱ですね。

こちらからは以上です。

2014年9月8日月曜日

Swift から Core Data を操作するときはこの2点だけ気をつけよう (Xcode 6 beta 7編)

将来的にXcode側の対応が変わる可能性が極めて高いので暫定ですが、Xcode 6 beta 7でSwiftからCore Dataを触った時に注意するポイント2点まとめです。この2点にだけ気をつければSwiftでもCore Dataは案外あっさり動きますのでご安心ください!

1. プロパティの設定の仕方


  • @NSManagedを使うこと
  • Int, BoolではなくNSNumberを使うこと。StringはStringでOK
  • Many関連にはNSSetを指定すること



2. entityNameの与え方


  • コード上ではクラス名だけを与える
  • Model Editor上ではモジュール名.クラス名(完全修飾名)を与える



Model Editorではこのようにモジュール名.クラス名の形で設定する必要があります


あとは普段Objective-Cで使っている時と同じように使えばOKです。一応、簡単なSwiftからCore Dataを操作するラッパライブラリを書いてみたので、もし宜しければ見てみてください。
https://github.com/akisute/Dove

【ヤヴァい】リリース直前の Swift の仕様が早くも悲惨なことになってる

正式版リリースまで後一ヶ月と噂されるXcode 6と新言語Swiftですが、リリース一ヶ月前にも関わらずその仕様が早くも悲惨との声がごく一部から上がっているようです!!



public private(set)って結局どっちなの!?

beta 5から追加されたアクセス制限指定子ですが、その仕様に疑問の声が!


なるほど、public private(set) var numberはキモいですね〜。ちなみにこれはgetterがpublicでsetterがprivateな変数の宣言のようです。なんかもっといい文法はなかったんでしょうか・・・



一行closureを使うときに注意!思わぬ落とし穴が!




これはSwiftの言語仕様上、実行コードが一行しかないclosureは暗黙的にreturnするものとみなされてしまうからのようです。これはちょっとひどいですね〜〜!



中には良い一面も・・・?



Nil Coalescing Operatorというのは ?? 演算子のことです。これは A ?? B のように使い、AがNot NilならAを、そうでなければBを返すという演算子です。なかなか便利に使えますよ!ちょっとはSwiftも見なおしたかも!?



おわりに

某まとめっぽくしたらなんかイマイチな感じになりました(´・_・`)
あとAppleふざけんな

Apple Push Notification で送られてきた通知を通知センターから消すたった一つの方法

いや、それがたった一つしか方法ないのはさすがにどうかと思うのですが。

皆さんもiOSのアプリを使っていて、通知を受け取った後にアプリを開いても通知センターから通知が消えず地味にイライラする現象に見舞われたことがあるのではないかと思います。私もお恥ずかしながらつい最近知ったのですが、実はこれアプリ側で何も対応しないと通知センターから通知は消えません。それどころかなんと通知センターからプッシュ通知を消すためのAPIも一切提供されていません。つまり普通にアプリを作ると通知センターから通知が消えないのです。Appleふざけんな

※注記: ここで私が消せない通知と呼んでいるものはApple Push Notification経由でのRemote Notiifcationです。UILocalNotificationは自由自在にアプリ側から消せるので、適切なタイミングで消してないのは単なる実装者の怠慢となります。

しかしながらTwitterやFacebookアプリはきちんとアプリが開かれたタイミングで通知センターから通知が消えるようになっています。不思議です。そこでちょっと調べてみました。

解決策

以下のリンク先にあるように、一度applicationIconBadgeNumberを1以上の数字に設定してから、0に戻すと、全てのApple Push Notification経由での通知が消えるようです。

http://stackoverflow.com/questions/8682051/ios-application-how-to-clear-notifications
http://stackoverflow.com/questions/9925854/remove-single-remote-notification-from-notification-center

iOS 5~8のすべてのバージョンで動作することを確認しています。少々ダサいですがこの方法でしのいでいきましょう!

※注記: 個別に通知を消す方法はありません。なおiOS 8より、ユーザーが直接通知センターから通知をタップしてアプリが起動した場合のみ、タップされた通知が自動的に消えるように仕様が変更されています。相変わらずアプリから明示的に消すことはできません。Appleふざけんな

2014年8月25日月曜日

Silent Push / Background Fetch 時の fetchCompletionHandler に渡す引数ごとの挙動の違いを調べてみた

2014/09/10追記: 追加調査によって判明した事項を追記しています。


iOS 7から追加されたPush通知によるバックグラウンド処理機能 (Silent Push) および定期的なバックグラウンドフェッチ機能 (Background Fetch) ですが、これらの機能はバックグラウンド処理が完了したタイミングでいずれもfetchCompletionHandlerにUIBackgroundFetchResult型の値を渡すような設計になっています。

ちょっと調べれば、大体どこの解説サイトにも以下のように説明があります。


  • UIBackgroundFetchResultNewData
    • 新しいコンテンツの取得に成功し、新しいデータが存在した場合
  • UIBackgroundFetchResultNoData
    • 新しいコンテンツの取得には成功したが、新しいデータが用意されていなかった場合
  • UIBackgroundFetchResultFailed
    • 通信エラーなどの理由でコンテンツの取得そのものに失敗した場合


問題は、これらの値をfetchCompletionHandlerに渡したら「何が起きるか」を解説しているサイトや文献がほとんど存在しません。そこでこのたび独自に調査してみました。

UIBackgroundFetchResultの値が影響する項目

いろいろ調べてみた結果、以下の参考文献によりUIBackgroundFetchResultの値は2つの項目に影響することがわかりました。
http://www.objc.io/issue-5/multitasking.html
https://codeiq.jp/magazine/2013/12/3022/

ひとつは、以降のSilent PushおよびBackground Fetchの挙動に対する影響です。OSが返された値を元に、アプリが使用したプロセス時間・電力消費などを判断し、以降のSilent PushやBackground Fetchの頻度に反映させるというものです。ただしどのような判断が行われどのように頻度に対して影響するかは完全にブラックボックスとなっており文献がありません。

もう一つはApp Switcher(ホームボタンをダブルタップした際に表示されるアプリ切り替え画面)のサムネイル画像、スナップショットを更新するために使用しているようです。値が渡されたタイミングで必要に応じてアプリのスナップショットを再描画するために、なんとアプリがバックグラウンドにいるにも関わらずその場でUIの再描画が行われます。この際通常のフォアグラウンドにアプリが存在する場合とまったく同様に、UIViewやUIViewControllerのメソッドが呼ばれます。例えばviewWillAppear, viewDidAppear,  didMoveToSuperViewなどが呼び出されます。

従いましてUIBackgroundFetchResultを渡す際には必ずメインスレッドで行う必要があります。NSURLSessionのdelegateを受け取ったスレッドから直接値を渡すとバックグラウンドでひっそりクラッシュします。ひっそりクラッシュする程度ならまだしもですが、これらのUI処理の中にバックグラウンドで呼び出されることを考慮していない処理が入っていると数々の問題を引き起こします。例えばviewDidAppearのタイミングでユーザさんがその画面を見たものと判断してデータやフラグを更新するですとか、Google Analyticsに統計情報を送っているですとか、そういうことをやっていると一気に大問題になります。

長くなりましたが、以上を踏まえまして、各値ごとにどのような影響があるかを実際に実機でテストして調べました。

UIBackgroundFetchResultNewDataを渡した時

スナップショットの更新が発生します。
Silent Pushの受取可能頻度に対して影響はないようです。一日4回程度、3〜6時間程度の間隔が空いていれば、全く問題なく受取可能です。ただし10分〜15分間隔で受取を行うとレートリミットがかかりはじめ、1〜2日以内に受信不可能になります。一度受信不能になったデバイスが再度受信可能になるかどうかはわかりませんが、数日間程度置いていても効果がなかったので、再度受信ためには最悪デバイスの完全なワイプとリセットが必要になるかもしれません。
Background Fetchの発生頻度に対しても影響はないようです。

UIBackgroundFetchResultNoDataを渡した時


スナップショットの更新が発生します。
三ヶ月程度様子を見ているところ、Silent Pushの受取り頻度に対してNewDataの場合とほぼ同様に影響はなさそうです。したがってNewDataを返す場合と何が違うのか現在のところわかっていません。

以下、旧記述です。
スナップショットの更新は発生しません。スナップショットの更新に伴って大問題が発生する場合には一番簡単な解決策になります。
ただし毎回毎回UIBackgroundFetchResultNoDataばかりを返却していると、Silent Pushの受取可能頻度およびBackground Fetchの発生頻度に対してペナルティがかかる恐れがあります。何度試行してもデータが取れないので、システムが再試行頻度を下げるのではないかという推測ですが、実際に観測できたわけではありませんので、なにか情報が入り次第また詳しくお伝えします。

UIBackgroundFetchResultFailedを渡した時

スナップショットの更新は発生しません。
こちらもUIBackgroundFetchResultNoDataと同様、UIBackgroundFetchResultFailedばかりを返却していると、Silent Pushの受取可能頻度およびBackground Fetchの発生頻度に対してペナルティがかかる恐れがあります。何度試行してもデータが取れないので、システムが再試行頻度を下げるのではないかという推測ですが、実際に観測できたわけではありませんので、なにか情報が入り次第また詳しくお伝えします。


2014年8月12日火曜日

Apple Push Notification (APN) 使用時の delegate の挙動について、 iOS 7以降 / iOS 6以前の差をまとめた

iOS 7以降とiOS 6以前で、俗にいうリモートPush通知の受け取り方と受け取った際の挙動がまるで違っているので、最近リモートPush通知を実装した時につまづいた箇所をまとめてみました。

使用するdelegate methodの違い

iOS 7以降


  • いかなる種類のPush通知においてもapplication:didReceiveRemoteNotification:fetchCompletionHandler:を使用します。
  • application:didReceiveRemoteNotification:fetchCompletionHandler:とapplication:didReceiveRemoteNotification:が両方実装されている場合も、application:didReceiveRemoteNotification:fetchCompletionHandler:しか呼び出されません。application:didReceiveRemoteNotification:fetchCompletionHandler:のみ考えればOKです。
  • ただし、application:didReceiveRemoteNotification:が実装されており、application:didReceiveRemoteNotification:fetchCompletionHandler:が存在しない場合は、iOS 6以前同様の挙動になります。
  • 以下、長ったらしいのでapplication:didReceiveRemoteNotification:fetchCompletionHandler:を新メソッドと呼称します。

iOS 6以前


  • application:didReceiveRemoteNotification:を使用します。
  • 新メソッドが実装してあっても一切反応しません。
  • 以下、長ったらしいのでapplication:didReceiveRemoteNotification:を旧メソッドと呼称します。

挙動の違いまとめ

以下に表にしてまとめました。ここで、
iOS 7以降とは新メソッドを使用する場合、iOS 6以前とは旧メソッドを使用する場合のことを指します。


iOS 6以前iOS 7以降
初回起動application:didFinishLaunchingWithOptions:に
UIApplicationLaunchOptionsRemoteNotificationKeyが付いて呼び出される。
旧メソッドは呼び出されない。
application:didFinishLaunchingWithOptions:に
UIApplicationLaunchOptionsRemoteNotificationKeyが付いて呼び出される。
その後、UIApplicationStateActiveの状態で、新メソッドも呼び出される。
handlerについては、content-available指定が1の場合のみ付いてくる(未確認)。
起動中UIApplicationStateActiveの状態で、旧メソッドが呼び出される。UIApplicationStateActiveの状態で、新メソッドが呼び出される。
handlerについては、content-available指定が1の場合のみ付いてくる(未確認)。
未起動UIApplicationStateInactiveの状態で、旧メソッドが呼び出される。UIApplicationStateInactiveの状態で、新メソッドが呼び出される。
handlerについては、content-available指定が1の場合のみ付いてくる(未確認)。
データ取得不可能。UIApplicationStateBackgroundの状態で、新メソッドが呼び出される。
handlerは必ず付いてくる。
この通知がアラートや通知センターに表示された場合、ユーザーがそれらとインタラクションしたらさらにもう一回UIApplicationStateInactiveの状態で新メソッドが呼び出される。

バックグラウンドでの通知受け取りとデータ取得

iOS 7から、Push通知のペイロードのapsオブジェクトにcontent-availableというプロパティが新たに追加されました。content-availableプロパティの値を数字の1に設定すると、ユーザーのインタラクションを介さずにバックグラウンドでアプリケーションが起き上がることができ、サーバから最新のデータの取得を行ったりする事が可能です。詳しくは「Push通知 バックグラウンド iOS7 content-available」とかその辺の単語を適当に組み合わせてググってください。山ほど解説しているページがあります。

サイレント通知が届かないときの処方箋

さて、上記バックグラウンドでの通知受け取りとデータ取得について、余り解説が見当たらなかった点についてまとめました。

iOS 7以降のPush通知は、apsオブジェクトのalertプロパティの有無とcontent-availableプロパティの有無によって挙動が変わります。具体的には以下の表のようになります。

content-availableがあるcontent-availableがない
alertがある通常通知
同時にバックグラウンドデータ取得も可能
通常通知
alertがないサイレント通知
バックグラウンドデータ取得のみ可能
送信できない

content-availableがない場合はiOS 6以前のPush通知と全く同じなので説明を割愛します。次にcontent-availableとalert両方が指定されている場合ですが、これはiOS 6以前の通常通知と基本は同じで、異なる点は通知を受け取った瞬間にバックグラウンドでアプリが立ち上がってデータの取得ができるという点だけです。したがいましてちょっと上記に記載しましたが、通知を受け取った瞬間と、ユーザーがインタラクションした瞬間で2回新メソッドが呼び出されるので、必ず新メソッドの中でUIApplicationStateを見て処理を分岐するようにしてください。

問題になってくるのがcontent-availableだけが指定されている通知、サイレント通知についてです。ほかのサイトでもよく紹介されていますが、この通知を使ってバックグラウンドでアプリを起動させて最新のデータをダウンロードしたりすることができます。以下にサイレント通知のペイロードの例を示します。

上が通常のサイレント通知ですが、下のようにサイレント通知と同時にバッジの数を更新することもできます。

さてこのサイレント通知ですが、とにかく「届かない!」というトラブルを耳にします。実際私が試した際にもなかなか届かないで弱りました。そこで調査したところ、サイレント通知のペイロードを工夫することできちんと通知が到達するようになりました。以下に到達率を改善したサイレント通知のペイロードの例を示します。

ご覧のように、空のsound要素を追加することで到達率が劇的に向上します。また2つ目の例のように、priorityというプロパティを10に設定するとこれまた到達率が劇的に向上します。どうやらこのpriorityプロパティはalertプロパティかsoundプロパティが設定されている場合はデフォルト10に、設定されていないサイレント通知の場合はデフォルト5に設定されているようで、それが原因で到達しない事が多いということがわかりました。
参考記事: http://stackoverflow.com/questions/19239737/silent-push-notification-in-ios-7-does-not-work

それから、Xcode上からプロジェクトのCapabilitiesの設定をするとき、Background fetchとRemote notificationsの両方の指定がおそらく必須と思われます。中にはRemote notificationsだけ設定しておけば良いと解説しているところもありますが、どうやらRemote notificationsは通知を受け取る箇所までだけで、肝心のバックグラウンドでのデータの取得はBackground fetchがないと実行できないのではないかと思われます。

UINavigationController.interactivePopGestureRecognizer の挙動を魔改造して変えてみる

注意: 今回の記事は久しぶりの誰得記事です。こちらで紹介されている手法を用いるにはプライベートAPIを操作する必要があるため、おそらく審査に通りません。じこせきにんでお願いします。

iOS 7からUINavigationControllerにinteractivePopGestureRecognizerというプロパティが新たに追加されました。これは「ナビゲーションコントローラの左端から右にスワイプすると前の画面に戻る」機能を司るgesture recognizerで、同じくiOS 7から追加されたUIScreenEdgePanGestureRecognizer(画面端からのみ反応するPan Gesture Recognizer)とUIViewControllerInteractiveTransitioning(ViewController間の画面遷移時に、ユーザーのインタラクションを追加する。例えば画面半分までスワイプしたら50%画面遷移するとか)の仕組みで実装されています。

ここで、例えばこの「ナビゲーションコントローラの左端から右にスワイプすると前の画面に戻る」機能について、見た目はそのままがいいんだけど左端からしか反応しないのが気に入らないから画面の中央でも反応するようにしたいという要望があったとします。自前で実装してもよいのですが、やはりApple純正の動きが良いですよね。

そこで以下のページで紹介されている黒魔術を使ってみたいと思います。
http://blommegard.se/blog/2014/01/31/a-take-on-custom-transitions-with-uinavigationcontroller/
こちらのページにUINavigationController内部でどのようにinteractivePopGestureRecognizerが実装されているかという解説があります。

  • 先述の通り、UIScreenEdgePanGestureRecognizerのインスタンスが使用されている
  • targetは_UINavigationInteractiveTransition型のオブジェクト、調査の結果_cachedInteractionControllerという隠しプロパティがそれに相当することがわかった
  • actionは@selector(handleNavigationTransition:)

ここまでわかったので、あとはtarget/actionを自分で生成したUIGestureRecognizerにセットしてやれば、標準の動きをジャックすることができるというわけです。今回はUIPanGestureRecognizerを使用しました。



このコードを実行してみると確かに画面中央からでもいつものスワイプジェスチャで戻る挙動が可能になっています。やりましたね\(^o^)/

2014年6月25日水曜日

App Extension や Widgets に App Container を使わないで簡単にデータを渡す方法

iOS 8から利用できるApp Extensionはアプリ本体とは別のプロセスとして動作するため、そのままでは簡単にデータを渡すことができません。公式のドキュメントではApp Containerという仕組みを使う方法が推奨されていますが、この方法はiOS Dev Center上でApp IDの設定が必要になったりするなど面倒がつきまといます。そこでより簡単にApp Extensionにデータを渡す方法がないか調べてみました。

方法1: UIPasteboardを使う方法

一番おすすめなのがこちらです。UIPasteboardというのは要するにコピペ用のクリップボードみたいなものなのですが、こちらシステムが用意している共通のもの以外にも自分のTeam IDでコードサインされたアプリ間すべてで共用できるPasteboardを作成する機能があります。この機能を使用すると非常に簡単にデータをやりとりすることができます。面倒な設定も不要ですし、persistentプロパティを設定することでディスクへの永続化も面倒を見てくれます。

以下にサンプルコードを掲載します。本体アプリのUIApplicationDelegate内でクリップボードを作成してデータを突っ込み、App ExtensionのWidgetViewController内でその値を使用するサンプルです。

注意点として、UIPasteboardのnameはあなたの作成したアプリ全体(同じTeam IDでコードサインされたアプリ全体)で共有になります。ですのでWidgetですとかTestのような単純な名前ではなく、com.akisute.MyApp.Widgetのような一意になる名前をつけることをおすすめします。

UIPasteboardについての詳しい使い方については、以下の記事をご覧ください。

方法2: Keychainを使う方法(未確認)

未確認なので確定情報ではありませんが、Keychainには複数のアプリ間で値を共有する機能がありますので、そちらを使えばおそらくアプリ本体とApp Extensionの間で値を共有する事が可能になります。詳しくは以下の記事をご覧ください。

2014年6月9日月曜日

既存の Objective-C のメソッド引数の Swift 上での扱われ方を調べてみた

前置き

こちらの記事には2014/06/09現在、公式にはリリースされていないiOS8プレリリースドキュメントへのリンクが含まれます。iOS8にて新しく追加された内容には一切触れておらずAppleとのNDA規約にも違反するものではないという認識ですが、場合により予告なく削除する可能性があります。予めご了承ください。

本題

iOS8プレリリースドキュメントを眺めていて気になったのですが、ほとんどのCocoaのメソッドの引数に!がついています。例えばNSKeyValueObservingプロトコルのaddObserver:forKeyPath:options:context:メソッドのシグネチャは以下のようになっています。

func addObserver(_ anObserverNSObject!,
      forKeyPath keyPathString!,
         options optionsNSKeyValueObservingOptions,
         context contextCMutableVoidPointer)

第1引数と第2引数に!がついているのがわかると思います。
更にもうひとつ気になったのが、このメソッドの第3・第4引数です。これらは元々0やNULLポインタを渡すことができる引数だったのですが、見ての通り引数が?で宣言されておらず、そのままnilを渡してしまうとエラーになってしまうように見えます。

直感的に考えると、

  • !がついている引数は呼び出し時に強制的にunwrapされるので、nilを渡してはいけない、必須引数なのではないか?
  • !がついていない第3第4引数はnilが渡せないのではないか?

という風に思うのですが、実際には

  • !がついている引数にnilを渡すことができる。さらにnilを渡してもランタイムでクラッシュしない。
    • 今回の例のaddObserver:forKeyPath:の場合はクラッシュしますが、これは元々のAPIがそうだったからで、例えば他のCocoaのAPIで!がついているものでnilを渡しても問題ないものが多数存在します。
  • !がついていないNSKeyValueObservingOptions, CMutableVoidPointerにnilを渡してもコンパイルエラーにすらならず、何の問題もなくそのまま動作する。

このような私の理解とは正反対の挙動をします。大変気になったので調べてみることにしました。

!がついている引数の謎

これについてはそもそも私のSwiftに対する理解が完全に間違っていました。正しい理解は以下のとおりです。

  • 無印型 - nilを渡すことができない。nilになる瞬間が一瞬もない。これこそがnilを渡せない必須引数である。
  • ?型 - Optional型であり、nilを扱うことができる。これについては問題ない。
  • !型 - ImplicitlyUnwrappedOptionalという特殊なOptional型であり、Optional型なのでnilを扱うことができる。すなわち!が付いている引数については任意引数でありnilを渡すことができる。
    • !も?も演算子でも命令でもなく型であり、!や?をつけることで対象を型に包んでいる、と考えれば良いと思います。
    • !と?の違いはメンバにアクセスした際に暗黙的かつ強制的に元の型にアンラップされるか、そのままOptionalとして扱われるかの違いだけです。

ではなぜCocoaのAPIは引数で?ではなく!を使ってnilを受け取れるようにしているのでしょうか?これは引数を受け取った後に、その引数にアクセスした場合の挙動がおそらく影響しているのではないかと思われます。以下の例を見てください。

こちらのコードですが、safeSwim()メソッドが旧Objective-Cと完全に同じ挙動を示します。これは以下の様な理由によるものです。

  • self.name(!型)が?型によってラップされる。
  • self.name?の.descriptionにアクセスする。このとき?型によってラップされているので、self.nameがnilであれば何も起こらず、self.nameがnilでなければまず?型からアンラップされ、次に強制的に!型からアンラップされ、その結果通常通りdescriptionが呼び出される。
  • 通常だと?型の返り値は?型にラップされてしまうが、self.nameが!型なので強制的にアンラップされ?型ではなく素のString型を返すことができる
    • ここだけよく理解できてないです・・・

間違っていたらすみません(´・_・`)

とにかくこれこそが旧Objective-CのAPIについて!で型が表現されている理由ではないかと考えています。

!がついていないのにnilが渡せる引数の謎

こちらの謎はカラクリがわかってしまえば簡単です。前回の記事をご覧になった方はお気づきになったかもしれませんが、これらの型はAppleが__convertion()メソッドをNilTypeに対して追加しているため、nilを問題なく扱うことができます。

まとめ


  • !が付いている引数は?と同様にnilを渡しても良い引数である。nilを渡せない必須型は何もついていない引数のみである。
  • !はImplicitlyUnwrappedOptionalという特殊なOptional型であり、アクセス時にOptionalではなく強制的に元の型を返す点がOptionalと異なる、と考えれば理解しやすい。
  • 変数をすべて!で定義し、メンバアクセス時に常に?をつけるようにすると、どの変数にもnilを渡すことができ、実行時にnilがあればそこで実行がスキップされ、さらに返り値もOptionalではない通常の型にできるため、旧Objective-Cとほぼ同じ挙動になる。
    • ただしSwiftで新しく用意されているAPIを見る限りnilを扱う必要がある箇所については可能な限り!ではなく?を引数や返り値に使っているように見えるため、新しくSwiftで書く箇所については!を乱用するべきではないと思われる。
  • Cocoaの用意している型のうち、元々nilやNULLや0を渡すことができた型については、NilTypeに__conversion()メソッドが追加されているので、そのままnilを渡すことができる。

Swift で __conversion メソッドを使ってカスタムの型変換を定義する方法

2014/10/21追記:
Xcode 6.0 beta 6以降、__conversion()を使った暗黙的なas演算子を用いた型変換はサポートされていません。Xcode 6.1(Swift 1.1)現在、暗黙的な型変換を行う手段はないため、型変換を行いたい場合はイニシャライザを定義する方法を取るのが通例として良いと思います。

class 変換対象の型 {
  init(_ obj: 変換元の型:) -> 変換対象の型 {
    return 適当に変換対象の型を返す
  }
}



Swiftではas演算子を使ったり、型の定義されている変数・定数へ代入したり、メソッド呼び出しの引数にオブジェクトを渡す際に型変換が行われますが、デフォルトでは対応していない型変換があったりします。例えばStringはasを使ってもIntに変換することはできません。

また、SwiftではnilはNilTypeという型のシングルトンとして実装されており、nilを渡すとNilTypeから当該型への型変換が行われるようです。?(Optional)型や!(ImplicitlyUnwrappedOptional)型はNilTypeからの型変換に対応しているからnilが代入でき、それ以外の型は対応していないためコンパイルエラーになるというのがカラクリのようです。

この型変換ですが、__conversion()というメソッドを変換元の型に実装することで、任意の型変換を自分で実装する事ができます。以下のようになります。
class 変換元の型 {
  //@conversion属性は付けても付けなくても大丈夫みたいですが、一応つけます
  @conversion func __conversion() -> 変換対象の型 {
    return 適当に変換対象の型を返す
  }
}
以下にNilTypeを使ったnilからカスタムクラスへの型変換サンプルを示します。こうすればnilを引数にとってもエラーになりません。

ご覧のとおり、extensionとの組み合わせで既存のクラスも含むどのような型からどのような型への型変換にも対応可能になりますので色々はかどります。ぜひご活用ください。

2014年6月6日金曜日

Swift の enum型を for-in でイテレーションする方法

例えばJavaのEnum型などはそのまま以下のようにイテレーションすることが可能なのですが、

なぜかSwiftのenum型はそのままではイテレーションすることができません。対策としてGeneratorという仕組みが標準ライブラリに用意されてますので、それを使ってenumをイテレーションできるようにします。

具体的には、Generatorを継承したクラスを作成して next() -> Element? を実装してください。ElementはAnyObjectのtypealiasなので実際には好きな型を返していただければOKです。あとはSequenceOf<T>型でGeneratorをラップしてあげればOKです。next()メソッドがnilを返すまでSequenceOf<T>はイテレーションを続けてくれます。

以下にサンプルコードを示します。

Generator内部でyieldが使えれば便利なんですが、おそらくyield構文は無さそうです。Enum型を一覧したい場合以外にもGeneratorは便利に使えますのでぜひお試しください。

Swift を使ってみてがっかりした点まとめ



数日間iOS8/Xcode6/Swiftな環境で色々試してみて、Swiftを使っていて思ったよりがっかりした点が多かったのでちょっとまとめてみようと思います。

動的な処理がSwiftだけでは一切できない

[NSObject performSelector:]の類と、NSInvocationがSwiftからは一切呼び出せません。使おうとすると怒られます。objc/runtime.hは試していませんが、同様に直接Swift経由では呼び出せず間にObjective-Cをかます必要があるのではないかと思われます。

@optionalなprotocolが限定的にしか使用できない

具体的には@objc属性を付けないと使えません。しかしながらこのような後方互換性のためだけに存在する属性をいつまでもAppleがサポートするかは疑問が残るというのと、もう一つ以下の様な問題があります。

@objc属性のついたSwiftの型はただのObjective-Cクラスになる


こういう問題があるのであまり使いたいとは思えません。ちなみになのですがCocoaのクラスはほぼすべて@objc属性が付いているため、それを継承して使うことになるアプリでは事実上Swiftの本来の能力を出せないのではないかと思っていますが、実際のところはわかっていません・・・

メモリ管理が相変わらず必要

Swiftのメモリ管理はGCではなくARCでありただの参照カウント方式にすぎないため、Swiftでも循環参照が発生しないようにプログラマが明示的に参照の種類を指定しなければなりません。その上Objective-Cでも存在したstrong, weakに加えunownedという新しい種類のメモリ管理が追加されています。これはweakは参照が消滅するとnilにするという挙動であるためOptional型を使わなければならないのに対し、unownedは参照が消滅してもnilにならない代わりに通常の型がそのまま使えるというもののようです。

closureでselfをキャプチャするときの循環参照対策が相変わらず必要

いちばんがっかりしたのがこれです。Swiftはdelegateよりもclosureを使ったcallbackのほうが言語構造上向いているためclosureを大量に使うことになると思うのですが、このときselfがclosureを強参照し、closureがselfをキャプチャするようなコードを書いてしまうと、循環参照になるためメモリが開放されなくなるという問題がObjective-Cから引き続き発生します。対策としてclosure capture listと呼ばれる新たな構文が追加されています。closureの先頭、引数宣言の前に[unowned self]のような構文を追加することで、selfをunownedとしてキャプチャすることができます。

以下に使用前・使用後の例を示します。

Objective-CのweakSelfよりはマシに思えますが、とはいえこの辺りはコンパイラが自動的に対応してほしいところです(´・_・`)

2014年5月26日月曜日

Android で 画面の回転や状態の復元まで考えた Fragment の使い方のガイドライン(自分用メモ)

Fragment を使った画面を作る際に、どのように作ればうまい具合に画面の回転や状態の復元を扱えるかという自分用のメモです。

最初にまとめ

  • 基本方針として可能な限りすべての管理を当該ActivityのFragmentManagerに任せると楽
  • ActivityのフィールドとしてFragmentを保持するのはバッドノウハウな気がする
  • onCreateとonDestroyが呼び出されたからといってインスタンスが生成破棄されているとは限らない、これらはFragmentManagerのタイミング次第
最終的に実装したコードは以下のような感じになりました。

今回の発端

ActionBarのタブに2つのFragmentを格納し、片方はListView, もう片方はGoogle MapsのMapViewを突っ込むようなUIを作っていたのですが、Androidは素人なもので普通に作っていると画面回転とタブの切替時にうまいこと状態を復元するのがなかなか手こずってしまいました。というわけで良いプラクティスを考えてみることにしました。

Activity と Fragmentのライフサイクルを復習

Activityは画面回転時に一度破棄されてしまいます。このような場合はActivityのFragmentManagerが現在管理しているFragment(バックスタックに入っているものが含まれるかどうかは未検証)については、Activity破壊時にFragmentManager経由で自動的にonSaveInstanceが呼び出され、Activity復旧時に自動的にonCreateとonCreateView経由で復元が試みられます。

これとは別に、Activityは破棄されないがFragmentは破棄されるケース、例えばActionBarのタブを切り替えたりNavigationDrawerを選択するなどして同一のActivity上で別の画面Fragmentに遷移する場合もあります。

上記いずれの場合も、可能な限りテキストビューの入力内容やリストのスクロール位置、地図のカメラ位置などを保持することをユーザーから期待されるため、状態の復元が必要になります。画面を回したりタブを切り替えたらスクロール位置が先頭に戻ったらユーザーはイライラするでしょう。

状態の保存はBundleとonSaveInstanceStateを使い、復元はonCreateとonCreateViewを使うのが楽です。

解決策

画面回転などActivity自体の破棄と再生成が自動的に行われるケースであればFragmentManager管理下にあるFragmentについて自動的に再生成が試みられるため大して難しくはないと思います。タブを切り替えたりするケースについては、以下のいずれかが良さそうな気がしています。
  • 解決策1: すべてのFragmentをFragmentManagerにattachされた状態のままにし、タブが切り替えられたら見せないFragmentはhideする
  • 解決策2: 必要に応じてFragmentManagerにattach/detachを行い、そのかわり自分でBundleを作りonSaveInstanceStateを呼び出す
1のメリットはタブ切り替え時に復元がそもそも発生しないため管理が簡単です。確実に動作しますし、再生成も必要ないためパフォーマンスも良いです。デメリットはFragmentおよびFragmentが抱えるView構造をすべて保持し続けるためメモリを大量に消費します。

今回採用した解決策2のメリットはタブ切り替え時にFragmentのView構造をすべて捨てるためメモリが効率的です。MapViewはどうしてもメモリを大量に使うためいくらhide状態とはいえあまり他のタブの後ろにおいておきたくはなかったのでこうしました。デメリットはやはり複雑になります。今回はFragmentのインスタンスフィールドとして一時的にBundleを保持していますが、これは正直なぜFragmentがタブから外れてDetachされてDestroyされてるのにメモリ上に残ってるのかわかりづらい変な挙動になるので、Activity側かまたは何らかのマネージャクラスに任せてしまうべきではないかと思います。・・・ってそれがFragmentManagerなんですけど。もっとうまいやり方で出来そうな気がするんですが・・・


2014年4月30日水曜日

Android で Dagger DI を使いやすくするライブラリを書きました

Daggerというsquare社がオープンソースで提供しているAndroid向けDI (Dependency Injection)フレームワークがあります。


これを試しに自分のAndroidアプリで使ってみようと思い立ったのですが、幾つか問題が発生しました。

  • DI自体の概念が難しい
  • そもそもドキュメントを読んでもDaggerの使い方がよくわからない、公式のサンプルを真似してみても正直いまいちわからない
  • AndroidでDIを行うとなるとandroid.content.Contextの注入が必須になるのだが、Contextは動的なインスタンスであるためDIでの取り扱いが難しい

そこで四苦八苦しながら動くようになったものをライブラリとして公開し、少しでも簡単にDIのメリットだけを享受できればと思いまして DaggeredAndroid なるものを作ってみました。

使い方とかはREADMEを見てください。全部英語ですがすみません(´・_・`)

AndroidでDIを使う際のメリットは主に以下のとおりです。

  • オブジェクトをメンバに接続するだけのコードを無くせるので、コード量が減る。
  • シングルトンの取り扱いが楽になる。
  • Contextの取り扱いが楽になる。
  • Moduleを差し替えればインスタンスが安全に差し替わるので、テスト環境を作ったり、本番と開発環境を分離したりなど、環境の差し替えが楽になる。テスト時のみModuleを上書きすることもできる。

2014年4月20日日曜日

Android の TextView.setText() が遅い場合の原因と対処法

AndroidでTextViewを使っている時に、setText()に数百行単位のテキストを渡すとメインスレッドが1秒弱完全に固まってしまうという現象に見舞われてしまいました。昔の2.3端末ではともかく、手元の最新鋭機Nexus 5 (Android 4.4)でこんなに遅いのでは話になりません。しっかりと原因を調査し対処法を考えることにしました。

まずググってみると出るわ出るわ同じ問題。やはりみんな同じ場所で躓いているようです。
しかしながらいまいち具体的な原因がググっても見つかりません。そこでtraceviewを取ってみました。


すると原因が一発でわかりました。android.graphics.Paint.getTextRunAdvances()です。
Nexus 5では高速化のためJNI経由でネイティブ実装が呼び出されているようですが、それでもまだ間に合わないぐらい遅いようです。それもそのはず、このメソッドは与えられた文字の幅を計算するメソッドです。すなわち数百行のテキストのサイズを計算するため時間がかかっているようです。iOSで例えるならCore TextのCTGryphを計算するようなもの、UILabelのsizeThatFitsを呼び出すようなもので、非常に時間がかかってしまいます。

そこで対処法として、setText()でテキスト全体をセットし直すのではなく、TextViewが裏で保持しているテキストの一部だけを書き換えたり追記したりすることで一度に計算されるテキストのサイズの量を減らして高速化する事を考えました。iOSの場合はUITextViewにはsetText相当のプロパティしか用意されていないので、そのようなことをするのはdelegateを経由してみたりUIKeyInputプロトコルを自前で用意したりなどと困難がつきまとうのですが、Androidの場合は最初からTextViewの裏で保持しているテキストを自在に書きなおすための仕組みが用意されています。

そのためにはまずTextViewの裏で保持されているテキストを「編集モード」にしなければなりません。XMLでandroid:bufferTypeをeditableに指定するか、またはsetText()の第二引数にTextView.BufferType.EDITABLEを指定すると、テキストが編集モードで保持されるようになります。

そうするとgetEditableText()でTextViewが裏側で保持しているテキストが編集可能な状態で取得できます。あとはこのEditableオブジェクトに対して好きなように加工を行うだけです。単にテキストを追加するだけならTextView.append()を実行しても同じ結果が得られます。

こうすると数百行程度であればそれほど遅くなくテキストの追加ができるようになりました。しかしながら1000行を超えてくるとこれでも速度が足りなくなるので、自前でTextViewをサブクラス化して作っていくか、またはListViewにして一度に表示するテキスト量を減らすのが良いと思います。


2014年4月1日火曜日

Objective-🍣


全く新しい、真にユニバーサルな言語へ。


皆さんiOS開発の際にお世話になっているObjective-Cですが、一部の開発者の方々から以下の様な否定的な意見をいただくことがあります。

  • とっつきにくい
  • 文法がキモい
  • @や[]がキモい
  • シグネチャが無駄に長い
  • Apple製品でしか使えない

確かにObjective-Cは習熟すればこれらの欠点を補って余りある素晴らしい言語ですが、これからの更なるモバイルアプリの世界の拡充のためにはより一層多くの開発者に愛される言語になる必要があると私は考えました。

そこでこの度ご紹介するのがObjective-Cをさらに使いやすく、さらに親しまれるように、全く新しく一から作りなおした新言語

Objective-🍣

です!

Objective-🍣とは

以下の様な特徴を持つ言語です!

  • 驚異的に短く、真にユニバーサルな、洗練された文法を持ちます。これまでのプログラミング言語は基本的に英語による記述を強いるものでした。Objective-🍣は真に全人類にとってユニバーサルな絵文字をサポートすることで、この問題をすべて解決しました。Objective-🍣の親しみやすい文法にはすべてのプログラマがシンパシーを感じることができます。
  • Objective-Cと100%のランタイム互換性を持ちます。新しいiOSが登場してもその瞬間からあなたはObjective-🍣の圧倒的なパワーを手にすることができます。

さっそくObjective-🍣で書かれたサンプルコードを見てみましょう!拙作確率計算機のコードをObjective-🍣に書き換えてみました!

before

after

なんというこれまでにない全く新しいソースコード!あのキモかった@や[]、長ったらしいメソッド名がその姿を消しています!そのあまりの美しさには全プログラマが歓喜の涙を流すこと間違いありません!

Objective-🍣の導入方法

こちらのobjsushi.hをあなたのプロジェクト上でincludeするだけで使用できます!簡単ですね!


Objective-🍣を使ってみる

それでは早速ビルドしてみましょう!

あれ

えっちょ

\(^o^)/

ちなみに日本語セレクタ自体はXcode 5以降で普通に使えますよ。

お詫び

こちらの記事にはiOSならびに最新のMacでのみご覧になれる文字(具体的には🍣)を多数含んでおりますことをお詫び申し上げます(´・_・`)