2014年8月10日 星期日

iOS 開發筆記 - 快速使用 Google Analytics SDK for iOS : Screens Usage



工作上看著老闆很重視統計資料,比我這個本業搞 Web Service 還認真,因此,想嘗試用在 Mobile app 會有如何成果!過去在 Blog 也曾用過,大概就可以得知哪篇文章比較多人看、哪個國家比較多人等等

這次也用 CocoaPods 安裝 Google Analytics SDK for iOS (pod 'GoogleAnalytics-iOS-SDK'),過程:

Step 1: 先在 Google Analytics 網站上註冊一個 App ,以此得到 tracking id

Google Analytics 首頁 -> 管理員 -> 資源(Click) -> 新建資源 -> 行動應用程式 -> 取得追蹤編號 -> 例如 @"UA-#######-#" 等

Step 2: 使用 CocoaPods 管理 Google Analytics SDK for iOS

$ vim Podfile
pod 'GoogleAnalytics-iOS-SDK'
$ pod install
...
Using GoogleAnalytics-iOS-SDK (3.0.9)
...


Step 3: 在 AppDelegate.m 初始化 Google Analytics 資訊

#import "GAI.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GAI sharedInstance].trackUncaughtExceptions = YES;
[GAI sharedInstance].dispatchInterval = 30;
//[[[GAI sharedInstance] logger] setLogLevel:kGAILogLevelVerbose];
[[[GAI sharedInstance] logger] setLogLevel:kGAILogLevelNone];
[[GAI sharedInstance] trackerWithTrackingId:@"UA-########-#"];

id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
tracker.allowIDFACollection = YES;

// ...

return YES;
}


Step 4: 在任何想回報的地方埋下 Codes

#import "GAIDictionaryBuilder.h"
#import "GAIFields.h"

- (void)reportStatus:(NSString *)pattern {
id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
[tracker set:kGAIScreenName value: pattern];
[tracker send:[[GAIDictionaryBuilder createScreenView] build]];
}


如此一來,則可以透過 Google analytics 網站上觀察到多少使用者用了 App,並且透過上述 reportStatus 搭配的 pattern 字眼,可以用在使用者用了個功能就回報一次等。此外,在 Google Analytics SDK for iOS 上,其實有整合一個 GAITrackedViewController 供人繼承使用,仿造 Web 上頭的經驗,直接幫你記錄哪個 ViewController 用了多久 :) 有興趣的可以翻翻官方文件。
以下就是送出 [self reportStatus:@"test"]; 的統計資訊,需要留意的離開某個功能時,也該回報另一個狀態才能完整的終止。因此,透過 Screen 的用法,可以快速不破壞原先的程式架構。



註:由於 CocoaPods 裡頭維護的 GoogleAnalytics-iOS-SDK 沒有 link libAdIdAccess.a 這支,這將導致無法正確使用 IDFA 訊息,所以我自己額外再包了一個:changyy / GoogleAnalyticsSdkiOSUsingIDFA 來用用。

iOS 開發筆記 - 透過 CocoaPods - FMDB / FMDatabase 管理 SQLite Databases

好久沒用 C/C++ 處理 SQLite 的操作,原本有意直接在 Objective-C 一樣寫 C 來處理,但想起來最近一直把玩 CocoaPods ,就搜尋一下,發現 FMDB 還滿多的推薦的,且 github.com/ccgus/fmdb 上頭也有很猛的人數 XD 就下海來使用 FMDB 啦!

主要看中 FMDB 的特色:提供 Data Sanitization 機制!就是寫 PHP 時,會透過 mysql_real_escape_string 來處理 raw data ,避免資料格式破壞 SQL 語法。

把玩筆記:

#import "FMDatabase.h"
// $ vim Podfile
// pod 'FMDB'

- (void)insert {
// Documents/test.db
NSString *dbPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"test.db"];
BOOL needInitTable = ![[NSFileManager defaultManager] fileExistsAtPath:dbPath];
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
if ([db open]) {
// init table
if (needInitTable && ![db executeStatements:@"CREATE TABLE IF NOT EXISTS t (id VARCHAR(8), number INT)"]) {
NSLog(@"table init error");
return;
}

// insert data
if (![db executeUpdate:@"INSERT OR IGNORE INTO t (id, number) VALUES ( :id, :number )" withParameterDictionary:@{
@"id" : @"id_data",
@"number": @(12345)
}] ) {
NSLog(@"insert error");
}
[db close];
}
}

- (NSArray *)query {
// Documents/test.db
NSString *dbPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"test.db"];
BOOL needInitTable = ![[NSFileManager defaultManager] fileExistsAtPath:dbPath];
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
if (!needInitTable) {
if ([db open]) {
NSMutableArray *output = [[NSMutableArray alloc] init];
FMResultSet *rs = [db executeQuery:"SELECT id, number FROM test;"];
while ([rs next]) {
// records to NSArray
NSMutableDictionary *item = [[NSMutableDictionary alloc] init];

item[@"id"] = [rs stringForColumn:@"id"];
item[@"number"] = @([rs intForColumn:@"number");

[output addObject:item];
}
[db close];
return output;
}
}
return @[];
}

2014年8月8日 星期五

iOS 開發筆記 - Timezone / NSTimeZone 使用方式

處理 iOS App Push Notification 時,面對著 Global User 時,希望可以依照時區來進行,所以就需要取得使用者現況時區資訊。

NSLog(
@"\nlocalTimeZone = [%@]\nDisplay = [%@]\nGMT = [%d] hours",
[NSTimeZone localTimeZone],
[[NSTimeZone localTimeZone] name],
(int)[[NSTimeZone localTimeZone] secondsFromGMT] / 60 /60
);


localTimeZone = [Local Time Zone (Asia/Taipei (GMT+8) offset 28800)]
Display = [Asia/Taipei]
GMT = [8] hours

[PHP] 從 Country Code 判斷 Timezone 以及計算 GMT 時差

原本正在考慮要不要自己刻,研究一下還是有一些接近內建方式查詢:

<?php
foreach( array(
'US', 'CN', 'GB', 'DE', 'NL', 'FR', 'ES', 'IT', 'TR', 'RU', 'TW', 'HK', 'BR', 'KR', 'AE', 'TH', 'AU'
) as $code )
{
$timezone = geoip_time_zone_by_country_and_region($code)."\n";
$timezone = trim($timezone);
if(empty($timezone))
continue;
$dtz = new DateTimeZone($timezone);
$hourOffset = (int)( $dtz->getOffset(new DateTime('now', $dtz)) / 60 / 60 );
echo "Timezone: $timezone, $hourOffset hours\n";
}


結果:

Timezone: Europe/London, 1 hours
Timezone: Europe/Berlin, 2 hours
Timezone: Europe/Amsterdam, 2 hours
Timezone: Europe/Paris, 2 hours
Timezone: Europe/Rome, 2 hours
Timezone: Asia/Istanbul, 3 hours
Timezone: Asia/Taipei, 8 hours
Timezone: Asia/Hong_Kong, 8 hours
Timezone: Asia/Seoul, 9 hours
Timezone: Asia/Dubai, 4 hours
Timezone: Asia/Bangkok, 7 hours

其中有些國家的腹地廣,所以時間變化大,就無法查到,正解應該是要再搭配 region 資訊才行,請參考 php - geoip_time_zone_by_country_and_region

2014年8月4日 星期一

iOS 開發筆記 - Warning: Attempt to present YourViewController on ViewController whose view is not in the window hierarchy!

有點久沒有純手工寫 UI 互動類的 Orz 測試時不知為何在 viewDidLoad 彈跳新的 YourViewController 時,會出現這個 Warning 並且沒有任何結果。

查詢了一下,發現要在 viewDidAppear 呼叫就能避免現象,有人說是在 viewDidLoad 時沒有 Window Hierarchy 資訊。擺在 viewDidAppear 時,若彈跳的 YourViewController 關閉時,切入 ViewController 時,又進入 viewDidAppear 又會彈跳 YourViewController 出來。

總之,測試時應該可以先這樣用吧,若要正式使用大概要多加一些條件判斷:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self presentViewController:[[YourViewController alloc] init] animated:YES completion:NULL];
}
參考資料
@ Updated 2014-08-17: 另一招則是在 viewDidLoad 中,採用 dispatch_async -> dispatch_get_main_queue 使用也行。

- (void)viewDidLoad
{
    [super viewDidLoad];

    dispatch_async(dispatch_get_main_queue(), ^{
        YourViewController *v = [[YourViewController alloc] init];
        [self presentViewController:v animated:YES completion:^{}];
    });
}