MagicalRecord で非同期クエリを行う
MagicalRecord の + MR_findAll
などの同期メソッドは非常に便利なのですが、UI を非常にブロックしやすい性質があります。大量のオブジェクトが保存されうる場合に特に注意が必要です。
めやすとしては、1000 件以上のデータは同期メソッドでは顕著になります。
特に、- viewDidLoad
でクエリを行うと、画面遷移が起こる前に UI がブロックされます。結果、「タップしたのになかなか移動しない」イライラ感に繋がります。
非同期にクエリを行う
非同期にクエリを行うには、NSManagedContext
と NSFetchRequest
を直接取り扱う必要があります。
// XXXManager.m - (void)fetchAllXXXWithPredicate:(NSPredicate *)predicate completionBlock:(void(^)(NSArray *fetchResults))completionBlock { NSFetchRequest *request = [XXX MR_createFetchRequest]; request.predicate = predicate; NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread]; [context performBlock:^{ NSArray *result = [context executeFetchRequest:request error:nil]; completionBlock(result); }]; }
NSFetchedResultsController
で非同期にクエリを行う
NSFetchedResultsController
を使う場合は、利用する ViewController
内で NSFetchedResultsController
を初期化してから、以下のようにします。(エラー処理は簡略化してます)
@weakify(self); [self.fetchedResultsController.managedObjectContext performBlock:^{ @strongify(self); [self.fetchedResultsController performFetch:nil]; @weakify(self); [[NSOperationQueue mainQueue] addOperationWithBlock:^{ @strongify(self); [self.collectionView reloadData]; }]; }];
これは NSFetchedResultsController
のカテゴリを作ってもよいでしょう。
// NSFetchedResultsController+AsyncFetch.m - (void)ki_performFetchWithCompletionBlock:(void(^)(NSError *errorOrNil))completionBlock { @weakify(self); [self.managedObjectContext performBlock:^{ @strongify(self); NSError *errorOrNil = nil; [self performFetch:&errorOrNil]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ completionBlock(errorOrNil); }]; }]; }
参考文献
App Store Views / App Units は「インストール率」ではありません
iTunes Connect Analytics で以下のようなハマりどころがあったので共有します(*´ω`*)
- まず、iOS8 以降の端末での情報のみがカウントされます。iOS7 以下の情報を含まないので正確な数字ではありません。
- App Store Views / App Units = インストール率 ではありません
- なぜなら、検索結果から直接インストールすることが出来るからです。それらは、App Store Views には反映されず、App Units だけが増えてしまいます。
- App Usage のセクションの情報は実情に比べて少ないです。
- なぜなら、「アナリティクスによってトラッキングされることを承諾したユーザ」のみがカウントされているからです。
- WebSite からの流入は、ドメインまでしかわかりません。
- 広告などでリンクの貼り方を指定できる場合は、必ず campaign リンクを貼ってもらいましょう。
- App Store 内での検索ワードがわかりません。
- (出来るようになってるって宣言してたのに…)
- 対策は存在しません。
- Searchman などを利用すれば、それぞれのキーワードに対して、ある程度ボリュームを見積もることができます。
その他
- App Units はデバイスによりユニークにカウントされます。
小ネタ
Source 毎のエンゲージメントのトラッキングなどは、App Analytics では特定のリンクとデバイスとの紐付けができないのでかなり不自由です。
Yozio などを使うことをおすすめします。 Yozio は、いい感じにメタデータをつけたリンクを発行できるサービスです。このリンクからアプリをインストールしてもらうと、初回起動時にそのメタデータをコールバックにて受け取ることができます。
Yozio を使えば、Source 毎にユニークなメタデータをつけて、初回起動時に計測ツールと結びつけてやれば OK です。
参考
デバッグ用に、ログと CoreData のダンプを送信する
さいきん、デバッグ用に、ログと CoreData のダンプを送信出来る機能を実装しました。
デバッグ情報の送信機能を実装することで、クラッシュレポートでは検知出来ないようなロジックのバグやデータの不整合、その他様々な不具合のデバッグをサポートする事が出来ます。 今までは、このような不具合に対するデバッグを行なう際、データベースとログの内容を見る事が出来ないので非常に歯がゆい思いをしていました。
上記のような、モバイルアプリデバッグの難しさを減らすことを目的とし、表題の機能を実装しました。
CoreData のダンプを送る
NSPersistentStore
の永続先として設定してある .sqlite
をそのままアップロードしてやれば OK です。
私は MagicalRecord を使っていたので少し工夫が必要でした。
MagicalRecord では、特別なことをしていない場合、次のようにして .sqlite
の filepath を取得する事が出来ます。
NSURL *filePath = [NSPersistentStore MR_urlForStoreName:[MagicalRecord MR_defaultStoreName]];
これを AFNetworking
とかで適当にアップロードしてやれば OK です。
[self.httpOperationManager POST:url parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) { [formData appendPartWithFileURL:filePath name:@"database" fileName:@"film-model.sqlite" mimeType:@"application/x-sqlite3" error:nil]; } success:^(AFHTTPRequestOperation *operation, id responseObject) { FLMLog(@"Debug: Sending dump succeed!"); success(responseObject); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { FLMLog(@"Debug: Sending dump failed!"); failure(error); }];
ログを送信する
CocoaLumberjack/CocoaLumberjack · GitHub を使います。
CocoaLumberjack は、ログを複数のロガーに流せたりするナイスな古参ライブラリです。CocoaLumberjack の FileLogger を使って送信用ログをファイルに貯めます。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [DDLog addLogger:[DDTTYLogger sharedInstance]]; // ふつうのロガー DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; [DDLog addLogger:fileLogger]; // ファイルに書き込むロガー。遅くない。 // ... }
初期化時にこのようにロガーの設定を行ないます。
以降は DDLogDebug()
などのマクロでログを行なう事が出来ます。
そのため、NSLog
を直接使っている場合は、DDLogDebug
などに書き換える必要があります。
面倒ですが、どうせ NSLog
を直接使うと「プロダクション時にログを吐かない」などの調整が出来ないのでやめましょう。
ログファイルのアップロード
ログファイルの取得には一工夫必要です。なぜなら、DDFileLogger
はログローテーションに対応しているため、ログファイルが一つに定まりません。
対象のログ全てを送ればよいのですが、面倒くさいです。最新のログを送っておけば大体 OK です。たぶん。そのためには次のようなハックが必要になります。
ios - Where is Logfile stored using cocoaLumberjack - Stack Overflow
// @see http://stackoverflow.com/questions/6411549/where-is-logfile-stored-using-cocoalumberjack // CocoaLumberjack の FileLogger から現在の .log ファイルを取得する為のハック @interface DDFileLogger (CurrentLogFileHack) - (DDLogFileInfo *)currentLogFileInfo; @end //
このようにすれば、fileLogger.currentLogFileInfo.filePath
でファイルパスを取得する事が出来ます。
この filePath からファイル用の NSURL
を得るには次のようにします。
- (NSURL *)logFileURLForCurrentLogFilePath:(NSString *)filePath { // @see http://stackoverflow.com/questions/11798537/reading-a-file-from-documents-directory-objective-c NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; NSString *filename = [filePath componentsSeparatedByString:@"/"].lastObject; NSString *logFilePath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"/Logs/%@", filename]]; NSURL *fileURL = [NSURL fileURLWithPath:logFilePath]; return fileURL; }
このようにして取得した FileURL を AFNetworking 等を用いてアップロードしてやります。
[self.httpOperationManager POST:url parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) { [formData appendPartWithFileURL:fileURL name:@"log" fileName:@"film-model.log" mimeType:@"text/plain" error:nil]; } success:^(AFHTTPRequestOperation *operation, id responseObject) { FLMLog(@"Debug: Sending log succeed!"); success(responseObject); } failure:^(AFHTTPRequestOperation *operation, NSError *error) { FLMLog(@"Debug: Sending log failed!"); failure(error); }];
AFNetworking の部分はバラバラに書きましたが、実際は一緒に送ります。
以上。
おち:手に入る情報が増えて遥かにマシになりました
デバッグでは情報が多い事がとにかく大事で、何はともあれまずはログとデータです。ログとデータがあればどんな事が起こったか想像がつきますが、無ければ状況から想像するしかありません。
デバッグに関して、面倒くさい事を考えたくなければとにかく様々な情報をたくさん送信しましょう。(*´ω`*)
Google Cloud Monitoring を使って障害のアラートを受け取る(*´ω`*)
App Engine でサービスを作っていると、ログは確かにいつでも見られて安心ですが、何かが起こった時にすぐに知れないのが辛いですね。 そんな時には Google Cloud Monitoring です。
下の図のようにアラートを設定し、メール, Slack, SMS, 電話など様々な手段で受け取る事が出来ます。
つかいかた
Google Cloud Console のメニューより、Monitoring > Dashboard & alerts をクリックすることで、Google Cloud Monitoring のダッシュボードを表示する事が出来ます。
いわゆる障害監視のサービスですので、色々出来る事がありますが、まずは表題の通り、アラートを設定しましょう。
"Set Alerting Policies" と見出しのある場所のボタン "Create Policy" を押して、アラートを作る事が出来ます。
アラートを作成するページでは、まず「どんな種類の条件か」を聞かれます。最初から設定されている "Threshold Conditions" を使うので、青い "Next" ボタンを押してください。
Resource Type (監視対象) を聞かれるので、"App Engine" に設定し、自分のアプリ名を選択します。その後 "Next" です。
最後に監視する指標を決めます。App Engine で困るのはだいたい、「エラーがたくさん発生してる」「課金が足りなくて Over Quota になってる」の二つなので、設定しておきましょう。
下の図は、「エラーがたくさん発生している」の設定例です。実際のエラー率を元に数字を決める事が出来ます。
「課金が足りなくて Over Quota になってる」は "Quota Based Request Denials" という Metrics で監視出来ます。これは Threshold を 1 など低い値にしておくといいでしょう。
あとは適当にアラート先(メール, Slack, SMS, 電話)を設定すれば、障害の検知を行なう事が出来ます。
便利(*´ω`*)
追記
課金アカウントを変えると App Engine の Daily Budget が $0.00 になります。(わお)他にも、課金の支払いを何回も失敗すると同様の事が起こります。
この謎仕様により Over Quota になってしまい障害になるという、笑えない失敗事例がありました。不測の事態に対応するためアラートは設定しておきましょう…
ソーシャル共有の数を一括で取得して JSONP で返す API
何に使うわけでもありませんがつくってみた
GET https://socialcountapi.appspot.com/count?callback=jsonp&url=https://facebook.com/
jsonp({"twitter":31135,"facebook":234483"pocket":503041});
ページを速く表示する為に、ソーシャルプラグインを使わずに独自で実装する、というメンドクサイことをやるときに使えます。
おち: ない
ActivityLifecycleCallbacks で view イベントを自動でログる
ActivityLifecycleCallbacks は Activity のライフサイクルを監視して、処理を挟み込める機構です。
使い方は至って単純で、Application.ActivityLifecycleCallbacks
を implement し、Application
のインスタンスメソッド registerActivityLifecycleCallbacks()
を呼ぶだけ。
public class YourApplication extends Application implements Application.ActivityLifecycleCallbacks { @Override public void onCreate() { super.onCreate(); registerActivityLifecycleCallbacks(this); } @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { final Bundle logArgs = new Bundle(); logArgs.putString("screen", activity.getClass().getSimpleName()); someTracker.track("view", logArgs); } }
全ての Activity に対して、コールバックが発動する事に注意します。
BaseActivity との違い?
こういった使い方では、BaseActivity
に色々処理を書くよりも、ActivityLifecycleCallbacks
のほうが継承レースにならなくて良いと思います。
「共通処理」というよりは、「ライフサイクルを監視して何かする」ときに嬉しいです。
ですから、「EventBus に register, unregister する」「ButterKnife.inject() する」とかはやめた方が良いと思います。Activity のコードだけを読んだ際に自明ではありません。
飛び道具なので用法容量を守りましょう。(*´ω`*)
Internet.org は参加しないと表示されないよ(。・ε・。)
Internet.org は Facebook 社が主導する、途上国向けのインターネットプロバイダーです。 利用者は無料もしくは格安でインターネットを利用する事が出来ます。
途上国向けに低コストでインターネットを提供するため、Internet.org はアクセス出来るサイトを制限しています。正確に言えば、Internet.org 互換であり、かつ Internet.org に参画する Web サイトのみを閲覧することが出来ます。
これらの Web サイトは Internet.org アプリにてリストされます。
国際的に展開する Web サービスは Internet.org に互換し、参画することでよりユーザを増やすことの出来る可能性があります。
ここでは、Internet.org の定める仕様と、参画のための条件をメモりたいと思います。
利用技術の制約
Internet.org ユーザはスマートフォン、フィーチャフォンその他の機器を利用することが出来ます。また、これらの機器は必ずしも最新のものではなく、ブラウザの互換性も低いです。ゆえに、以下の技術は利用出来ません。
また、利用帯域を増大させる以下の要素は利用出来ません。
- 動画
- 大きな画像 (一つの画像につき、1 MB 以下に限定されます)
(注: 画像の数、合計量の制約は書いていませんでしたが、何らかの制約があると思います)
つまるところ、i-mode のサイトを作れば OK です。
参画手段
レビューがあります。ガイドラインを良く読みましょう。
Internet.org Participation Guidelines
サブミットは下のリンクから可能です。
Submit your service to be in Internet.org
より詳しくは Facebook Developers の Internet.org のページにて。
細かい注意点
- Internet.org 対応の Web サイトは、常に Internet.org Proxy を通じて配信されます。キャッシュはされません。
- 別ドメインの埋め込みコンテンツ(YouTube など)は Proxy 側で切り捨てられます。
- JavaScript を用いた Analytics が不可能なので、古き良き
<img>
タグビーコンで代用する必要があります。 - とにかくあらゆる JavaScript が動きません。
- Internet.org からのリクエストは、
Via
ヘッダーにinternet.org
の文字列が入っているかどうかをみることで、振り分けが可能です。User Agent は各ユーザのそれぞれの値が入ります。 - Chrome Device Emulator で疑似テストが可能です。エミュレートするために、次の設定をします。
- JavaScript Disabled
- Throttled Network