2011年12月23日 星期五

Simple template engine for objective c

所謂template engine就是我們可以先定義一個template
例如一個html檔案
然後我們會希望把裡面少部分的片段透過程式去取代成我們想要的字串
而做這些取代動作的東西我們就稱作template engine

其實在iOS當中已經有一個非常簡單的template engine了,
也就是+[NSString stringWithFormat:(NSString *)format, …]
這東西就很像在c使用sprintf(...)
然而,這東西對大部份來時候來講都很好用,但是也有一些限制,
例如它必需要要一個蘿蔔一個坑 (傳進的參數就是對應到format中的每個%開頭的取代標示)
你不能要求一個蘿蔔兩個坑 (一個參數取代兩個取代標示)
甚至第一個蘿蔔一定要放在第一個坑 (第一個參數一定要放在第一個取代標示)

我查了一下stackoverflow
但是沒有找到我想要的答案
http://stackoverflow.com/questions/2539556/objectivec-builtin-template-system
得到的答案不外乎就是這幾個
1. 用上面提的+[NSString stringWithFormat:(NSString *)format, …]
2. 用-[NSString stringByReplacingOccurrencesOfString:withString:]
然後一個一個把取代標示換成我們要的字串
3. 不然就是給我一個非常強大的template engine..

其實這東西不難做,既然我們都已經有好用的NSScanner了,那我們就做一個簡單的template engine吧。
使用上我希望長這樣
NSString* testTemplate = @"Hello $[name], welcome to $[city].";

NSString* testString =
    [NSString stringWithTemplate:testTemplate
                         fromMap:[NSDictionary dictionaryWithObjectsAndKeys:
                                  @"Popcorny", @"name"
                                  @"Taipei", @"city",
                                  nil]];
NSLog(@"%@", testString);   // output: Hello Popcorny, welcome to Taipei."


程式碼如下



2011年10月31日 星期一

如何把Target/Action放進array裡面呢?

Target-Action相信這個design pattern對於寫ios一陣子的人都不陌生
這是幾個ios fundamental design pattern之一
然而有時候我們會希望把幾個target action放進array或是dictionary之中,或是直接當作member variable
但是SEL並不是一般繼承於NSObject的class,所以不能直接放進Array當中
當然我們可以用
NSStringFromSelector(SEL aSelector)
以及
NSSelectorFromString(NSString *aSelectorName)
讓SEL可以跟NSString*互換
但是怎麼都覺得不是很方便
我就在想有沒有比較漂亮的方法

我找了一下iOS foundation library,當中就屬NSInvocation最合適
這個Class就是把target, action, arguments包在一起,
當我們呼叫-[NSInvocation invoke]
就會呼叫這個target的selector,並且傳進這些arguments...
太棒了,這就是我要的class!!

但是NSInvocation的產生方式實在不怎麼好用,如果我們要把這些東西全部丟進去
那我們需要如下面這段code
NSMethodSignature* sig = 
    [[target class] instanceMethodSignatureForSelector:@selector(mySelector:];
NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:target];
[invocation setSelector:selector];
看起來很不友善吧...

不好用的話,那我們就來自己動手做Category吧...

NSInvocation+PopcornyLu.h / NSInvocation+PopcornyLu.m


這邊我提供了兩個Class messages來產生NSInvocation
其中第二個可以提供不定量的arguments.
使用上很簡單
// Put invocations in array
NSArray* myActions =
    [NSArray arrayWithObjects:
        [NSInvocation invocationWithTarget:self
                                  selector:@selector(action1)],
        [NSInvocation invocationWithTarget:self
                                  selector:@selector(action2:)
                                 arguments:@"foo", nil],
        [NSInvocation invocationWithTarget:self
                                  selector:@selector(action3WithFoo:andBar:)
                                 arguments:@"foo", @"bar", nil],
        nil];

// invoke all the actions
for(NSInvocation* invocation in myActions)
{
    [invocation invoke];
}

[補充]
當然也可以用block來取代target/action
以上面的例子我們可以用block來實作
NSArray* myActions2 = 
 [NSArray arrayWithObjects:
  
 [[^(void){
  [self action1];
 } copy] autorelease],
  
 [[^(void){
  [self action2:@"foo"];
 } copy] autorelease],         
  
 [[^(void){
  [self action3WithFoo:@"foo" andBar:@"bar"];
 } copy] autorelease],            
  
 nil];        

// invoke all the actions
for(void(^invocation)() in myActions2)
{
    invocation();
}

但是缺點是code比較凌亂一點,然後action1, action2:, action3WithFoo:withBar這些message都要在前面要先定義
稍微麻煩了點
但基本上Target/Action或是Block都有其方便之處
像這邊我希望array creation的地方不要寫太多code,所以我會選擇用target/action
但有時候使用上會用inline block比較方便,那我就用block



2011年9月21日 星期三

如何實作通知使用者有version update

可能有一種需求是我們希望如果appstore上有新版的軟體上架
我們會要通知使用者是否要更新
這時候我們可以透過這個URL去取得你的app資訊
http://itunes.apple.com/lookup?id=appid
回傳的結果可能是這樣.
中間就有版本號碼啦
不要問我怎麼抓怎麼parse json
這個solution很好找的
{
    "resultCount": 1,
    "results": [
        {
            "kind": "software",
            "artistId": 291728775,
            "artistName": "5Knot",
            "price": 0,
            "version": "1.1",
            "description": "90 Minutes .....",
            "genreIds": [
                "6005",
                "6012"
            ]
        }
    ]
}
然後自己local的版本可以這樣拿
[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]
自己再寫一個NSString的category去比較版本
@implementation NSString(VersionCompare)
- (NSComparisonResult)compareVersion:(NSString *)version
{
     NSArray *thisVersionParts = [self componentsSeparatedByString:@"."];
     NSArray *versionParts = [version componentsSeparatedByString:@"."];
     NSUInteger count = MIN([thisVersionParts count], [versionParts count]);
     NSUInteger i;
     for (i = 0; i < count; i++)
     {
          NSUInteger thisPart = [[thisVersionParts objectAtIndex:i] integerValue];
          NSUInteger part = [[versionParts objectAtIndex:i] integerValue];
          if (thisPart > part)
          {
               return NSOrderedDescending; //version is older
          }
          else if (thisPart < part)
          {
               return NSOrderedAscending; //version is newer
          }
     }
     if ([thisVersionParts count] > [versionParts count])
     {
          return NSOrderedDescending; //version is older
     }
     else if ([thisVersionParts count] < [versionParts count])
     {
          return NSOrderedAscending; //version is newer
     }
     return NSOrderedSame; //version is the same
}
搞定!!

Rerefenace
[1] Official search API
[2] iVersion


In App Purchase的實作心得分享

其實IAP的一些概念跟使用方法已經有嘎李羊的詳細介紹了,在此貼上連結,但就不再重複分享了
1. iOS In App Purchase 學習筆記 (1)
2. iOS In App Purchase 學習筆記 (2)
3. iOS In App Purchase 學習筆記 (3) : 如何從App Store取得商品資訊

我這邊就補充一下我最近研究IAP的心得,我就分別以三個類型Consumable, Non-consumable, Auto-renewable subscription來分別分享
Consumable
1. 可以透過此來購買Credit(虛擬貨幣),但是必須要在你的app中消費掉,不能拿來購買app以外的商品或是實體商品。
2. 通常實作的方法是交易成功後,程式自己增加購買的點數,這邊的邏輯StoreKit並不會負責幫你做credit增加的部份。
3. 雖然Consumable product可以設定quantity,來一次購買多量的商品,但是我建議還是不要這麼做。你可以用陳列多個商品來達到一次買多量credit的目的。例如:
10credits $0.99
25credits $1.99
50credits $2.99
4. 如果要讓多個device share你的credit好像沒有比較好的方法,因為我們無法取得apple id,所以不知道怎麼share。
而如果把credit存在server,那會不會違背policy這我就不敢說了。

Non-consumble
1. 買了之後就永久有效。即使app移除了再裝回來,或是在別的device裝app,只要用同個apple id就可以restore。
2. 有關restore相關的文章請看 Restoring Transactions
建議在你的app中如果有支援Non-consumable的app,要提供restore的功能,讓使用者有辦法恢復購買。
當然用原本的buy的方法也可以restore,但是比較不user friendly。
3. 交易成功後,記得要把買成功的狀態儲存起來,而app也要對應的打開對應的功能給end-user。

Auto renewable subscription
1. 有點類似Non-consumable product,但是是有時效性的服務。當訂閱週期到了,app store會自動展期,除非使用者取消自動展期。
2. 在itunes connect上面我們可以設定多種duration
3. 使用者可以透過[設定]這個app中的 Store -> Apple ID: -? -> 檢視Apple ID -> Manage Subscriptions 來取消訂閱或是更改週期
4. 比較多人可能會遇到的問題是我要怎麼知道subscription已經因為使用者取消註冊而過期失效了呢?
方法是App中可以透過http post去詢問subscription狀態,
你需要的是記住最後一次交易的receipt (-[SKPaymentTransaction transactionReceipt]),
或是透過restoreTransaction來取得transaction object,再透過其中的receipt來詢問 。
相關的文件在這 Verifying an Auto-renewable Subscription Receipt
下面是如果成功的話會回傳的結果,status = 0 代表成功。
而last_receipt_info可能會跟receipt不一樣,因為有可能server會自動renew subscription。

{
 "latest_receipt_info" =     {
        bid = "com.smartq.MyStore";
        bvrs = "1.0";
        "expires_date" = 1316424354739;
        "expires_date_formatted" = "2011-09-19 09:25:54 Etc/GMT";
        "item_id" = 465588051;
        "original_purchase_date" = "2011-09-19 08:34:24 Etc/GMT";
        "original_transaction_id" = 1000000008159291;
        "product_id" = "com.smartq.mystore.sub1m";
        "purchase_date" = "2011-09-19 09:20:54 Etc/GMT";
        quantity = 1;
        "transaction_id" = 1000000008162671;
    };
    receipt =     {
        bid = "com.smartq.MyStore";
        bvrs = "1.0";
        "expires_date" = 1316424354739;
        "expires_date_formatted" = "2011-09-19 09:25:54 Etc/GMT";
        "item_id" = 465588051;
        "original_purchase_date" = "2011-09-19 08:34:24 Etc/GMT";
        "original_transaction_id" = 1000000008159291;
        "product_id" = "com.smartq.mystore.sub1m";
        "purchase_date" = "2011-09-19 09:20:54 Etc/GMT";
        quantity = 1;
        "transaction_id" = 1000000008162671;
    };
    status = 0;  
而如果失敗的話可能會如下所示,
status = 21006代表subscription expired。
注意的是這次他不會回傳latest_receipt_info,而是lastest_expired_receipt_info

{
    "latest_expired_receipt_info" =     {
        bid = "com.smartq.MyStore";
        bvrs = "1.0";
        "expires_date" = 1316424354739;
        "expires_date_formatted" = "2011-09-19 09:25:54 Etc/GMT";
        "item_id" = 465588051;
        "original_purchase_date" = "2011-09-19 08:34:24 Etc/GMT";
        "original_transaction_id" = 1000000008159291;
        "product_id" = "com.smartq.mystore.sub1m";
        "purchase_date" = "2011-09-19 09:20:54 Etc/GMT";
        quantity = 1;
        "transaction_id" = 1000000008162671;
    };
    receipt =     {
        bid = "com.smartq.MyStore";
        bvrs = "1.0";
        "expires_date" = 1316424354739;
        "expires_date_formatted" = "2011-09-19 09:25:54 Etc/GMT";
        "item_id" = 465588051;
        "original_purchase_date" = "2011-09-19 08:34:24 Etc/GMT";
        "original_transaction_id" = 1000000008159291;
        "product_id" = "com.smartq.mystore.sub1m";
        "purchase_date" = "2011-09-19 09:20:54 Etc/GMT";
        quantity = 1;
        "transaction_id" = 1000000008162671;
    };
    status = 21006;
}    
5. 在開發中我們都是在sandbox mode中測試,但是subscription的週期會因為為了測試方便會短很多,相關的mapping如下
6. 在sandbox mode中只會自動展期五次,第六次會直接expire。

其他注意事項
1. IAP只能在實機上測試,並且只能在sandbox mode下測試,而sandbox mode也只能用測試帳號測試。
2. 測試帳號必須要先在你的itunes connect中,簽署“iOS Paid Applications”這份contract才可以產生。
3. 因為IAP product的價錢可以透過itunes connect上調整,建議商品售價可以透過SKProductRequest去跟itunes connect詢問,而非寫死在code。
下面是4/12/2013新增的
4. Test account請不要在系統的設定(Settings)去登入,也不要留下信用卡帳號,因為一旦使用就不能在sandbox mode中使用
5. 如果你有gmail帳號,你可以用gmail的alias來新增Test account。例如你原本的email是 example@gmail.com,那你可以用example+test@gmail.com去申請。這樣就可以申請一堆測試帳號。
6. 即使是測試帳號,交易過的就不能移除,這對non-consumable product測是很麻煩,唯一的解法就是多產生幾個測試帳號。

Ref
[1] iTunes Connect Developer Guide 7.0
[2] In App Purchase Programming Guide

2011年8月29日 星期一

探討Objective-C Block (part 3)

前面講了簡單的block用法跟block variable,
但是block難的地方應該就是記憶體管理的部份,
如果你不是很瞭解block內部的記憶體管理,
很容易一個不小心就導致circular reference而導致memory leakage..

前篇有提到,我們可以把block當作參數傳給function或是message,
但是傳進去後,有可能這個function會想把你的block pointer留下來
最常見的就是做event handling的例子,
我們把一個事件觸發的block當作參數丟進來,
但是是件觸發可能是數秒之後的事情,
而此時註event handler的function/message已經return了,
那此block所reference的local變數可能已經invalid了,
這時候block要怎麼處理這樣的情形呢?

首先,在block的定義中,此block還停留在call stack之中,
也就是他的生命週期會隨著定義此block的function return之後,其生命週期就會結束
除非我們呼叫block_copy或是[myblock copy]
此時block才會從stack變到heap中。
之後我們才可以把參數傳過來的block指給instance variable或是global variable,
而block中所用到的物件在此同時reference count也會+1。
但reference count +1這件事情卻在每一種case不一樣
因為block內部可以使用環境中看的到local, block, instance, local static, global variable
那copy這個動作會發生什麼事情呢?
我們先寫一個範例code

//MyBlockTest.h
#import 

typedef void (^myBlockTest_type)(void);

@interface MyBlockTest : NSObject {
    NSObject*           instanceRef;
    myBlockTest_type    myBlock;
}

- (void) test;

@end


//MyBlockTest.m
#import "MyBlockTest.h"
@implementation MyBlockTest

static NSObject* globalRef;

+(void) initialize
{
    globalRef = [NSObject new];
}

- (id)init
{
    self = [super init];
    if(self)
    {
        instanceRef = [NSObject new];
    }
    return self;
}

- (void) test
{
    // Local variable
    NSObject* localRef = [NSObject new];
   
    // Block variable
    __block NSObject* blockRef = [NSObject new];
   
    // Local static variable
    static NSObject* localStaticRef;   
    if(!localStaticRef) localStaticRef = [NSObject new];
   
    // create a block
    myBlockTest_type aBlock =
    ^{
        NSLog(@"%@", localRef);
        NSLog(@"%@", blockRef);
        NSLog(@"%@", instanceRef);
        NSLog(@"%@", globalRef);
        NSLog(@"%@", localStaticRef);
    };
   
    //copy the block
    myBlock  = [aBlock copy];
   
    NSLog(@"%d", [localRef retainCount]);
    NSLog(@"%d", [blockRef retainCount]);
    NSLog(@"%d", [instanceRef retainCount]);
    NSLog(@"%d", [globalRef retainCount]);
    NSLog(@"%d", [localStaticRef retainCount]);
   
    [localRef release];
}

@end


大家可以先想想看,當呼叫test的時候,會印出什麼樣的結果?
正確的答案是
2 1 1 1 1
不知道你答對了沒?

第一個localRef應該最能夠理解,基本上就是+1,這個就是這樣設計。

第二個 blockRef,由前面一張對block variable的解釋,
我們可以知道block variable是一個closure用一份。
因此此block variable並沒有額外的retain的動作。
所以被block variable指到的物件也不會有reference count +1的情況。

第三個instanceRef為什麼沒有+1呢?
事實上這個問題也是挺有陷阱題的味道。
對block來講,他看到的是self這個變數,而非instanceRef。
所以ref. count +1的不是instanceRef而是self。
如果在block copy的前後各把self的ref count印出來你就可以佐證這個事實了。

第四個globalRef跟第五個localStaticRef本質上很像,所以兩個可以一起討論。
由於這兩個變數在runtime中的位置是固定而且唯一的,
所以基本上在block內用上面兩個變數跟block沒有什麼兩樣。
因此block copy並不會也不需要增加ref. count的數目。

瞭解之後,那什麼時候可能會出現circular reference呢?
其實跟我們之前聊到的ios delegate你必須知道的事情所說的內容很像。
只是這次主角從delegate換成block。
試想,如果有3個view controller,分別是VC1, VC2, VC3
如果VC1產生並retain VC2
VC2也產生VC3
而且VC2可能跟VC3註冊了一個event handler並且參數是用一個block。
在這個block中可能長這樣。
[vc3 setOnClose:^{
        [self dismissModalViewControllerAnimated:YES];
    }];
那這樣會發生什麼是情呢?
答案是當VC1 release VC2的時候,
VC2因為自己有參照VC3,所以VC3的retain count還是1
VC3因為他的instance variable有retain這個block
而這個block因為用到block中的self
這個self就是VC2,
那這樣可糟了個糕,circular的悲劇就產生了。

目前官方文件告訴我們要這樣做
__block VC2* tempVC2 = self;

    [vc3 setOnClose:^{
        [tempVC2 dismissModalViewControllerAnimated:YES];
    }];
我們透過block variable不會retain的特性,
來把self丟給tempVC2,
如此在block在被copy的時候不會增加retain count。
我只能說太不friendly了,
不過目前好像也只有這樣解,而且到了ARC之後這個問題還是存在。
所以大家一定要改清楚block的memory management,
才不會不知道為什麼,reference count永遠不會歸零的狀況。

探討Objective-C Block (part 1) - block的使用
探討Objective-C Block (part 2) - block變數
探討Objective-C Block (part 3) - block的記憶體管理

探討Objective-C Block (part 2)

原本只打算寫兩篇的,但是忽然覺得很多東西可以講。
而且原本在這篇就要講的有關記憶體的問題,
我選擇了往後擱著
我們來先探討block variable,也就是__block這個修飾字。

前一篇有提到,block在別的語言叫做closure,
這個closure的概念很像是把環境閉鎖起來,
而這個環境就是指定義該block的這個call stack frame。
當block被呼叫block_copy的時候,或是[someBlock copy]的時候
這個block就會進入heap,
並且會指到目前的stack frame形成closure。
而block variable會跟這個closure綁住,
所以可能說closure variable會更加的貼切。

我們來看下面一個有趣的例子
typedef NSUInteger (^countdown_type)(void);
countdown_type createCountdown(NSUInteger number)
{
    __block NSUInteger counter = number;
   
    return [[^NSUInteger{
        return counter--;
    } copy] autorelease];
}
這個function產生了一個block,這個block中有一個block variable,
起始值是由傳進來的參數決定。
之後每呼叫一次counter都會減1,並且把原本的值傳回去。
下面使用的範例
countdown = createCountdown(10);
    NSLog(@"%d", countdown()); //10
    NSLog(@"%d", countdown()); //9
    NSLog(@"%d", countdown()); //8

所以我們更能清楚知道local variable跟block variable的差異。
local variable隨著function回傳而該變數的位置就隨著call stack pop掉。
而如果local static variable跟block variable的比較
static local整個app只有一份。
但是block variable是一個closure一份,
所以以這個例子,如果我們用的是local static,則所有的countdown都共用一個counter。
而如果是用block variable,則每個block都各自有自己的一份。

如果你比較瞭解了block variable的定義,
你可以在回頭想想為什麼在block中local variable只能當常數使用,
而block variable可以當變數使用。
相信你心中已經有答案了 :D

探討Objective-C Block (part 1) - block的使用
探討Objective-C Block (part 2) - block變數
探討Objective-C Block (part 3) - block的記憶體管理

2011年8月24日 星期三

探討Objective-C Block (part 1)

在ios4推出後,出現了一個新的語言功能block。這個功能其實有接觸多種程式語言的話,會知道其實這個功能不算是Objctive-C特有的
別的語言可能叫做Lambda或是Closure,
這東西主要的特性是他除了是function外,但是更紀錄了此function外部的環境。
也許有點抽象,我先舉個javascript的例子
var i = 2;
    var func = function(){ i = i * i;};
    func();  // than i = 4
    func();  // than i = 16
我們在中間定義一個function並且指派到func這個變數,
而在這邊可以用i這個外面的變數,因為在javascript中這樣的定義的就是一個closure,
他會記住外面有i這個變數,
所以之後我們連續呼叫兩次func,
則可以得到i是16這個結果
這個跟objective-c的block,或是別的語言的lambda,closure都是一樣的東西
相關的資料可以參考Clousre - wikipedia 裡面的介紹。

回到objective-c的block,當然這個好物當然要好好的利用一下,
這個東西最吸引人的地方就是可以帶不定數量的變數到function當中,因為你只要環境中可以取得的都可以在function中使用
並且因為block(或是其他語言的closure)通常使用上都是anonymous function,直接藏身在你的code當中,因此可以讓程式碼更加精簡。
但是我覺得剛開始用block的時候,有時候會有知其然而不知其所以然的情況。
尤其是Objective-c最令人頭疼的記憶體管理的部份在block中更是複雜,
如果你不知道你的物件怎麼在block中去使用那會是一件危險的事情,
所以這個主題算是經驗分享文,來分享我現在對block的認知,
而當然ios有很多官方的API都支援block,最有名的就是Grand Central Dispatch (GCD)
但是這篇不會介紹GCD,我們把內容專注在block這個語言功能。

在講block怎麼用之前,容我先介紹C的function跟function pointer,我認為在學block之前要有這個基本的認識比較好。
一來是block跟funtion有點相似,但是又有點不一樣,知道function再來看block我覺得會比較透徹的瞭解
二來它們兩個定義的部份真的很像,但是實作一個function跟block卻大不相同,仔細想想兩個的差異絕對會對block更有深刻的感覺。
首先function相信大家都會實作function,這再簡單不過了
int plusone(int a)
{
    return a+1;
}


上面這裡定義了一個傳入參數是int回傳是int的function,這應該不用解釋太多了。
那function pointer呢? 可能開始有些人不是那麼熟悉了...
int ooxx()
{
     typedef int (*myfunc_type)(int a);
     myfunc_type myfunc = plusone;
     NSLog(@"return value = %d", myfunc(5));
}

這邊開始就要解釋了
第一行是我定義一個myfunc_type這個function pointer type,而這個function type是傳入是int,回傳是int
而第二行是定義一個myfunc_type的變數,我讓他指到我們剛剛定義的plusone
而第三行我們就是去呼叫這個function pointer,則他就會等同去呼叫plusone,並且帶入5。
如果上面的code你看懂了,恭喜你,block幾乎沒差太多,甚至更好用。
int ooxx()
{
     typedef int (^myblock_type)(int a);
     myblock_type myblock = ^int(int a){
        return a+1;
     };
     NSLog(@"return value = %d", myblock(5));
}

第一行跟function pointer很像,原本function pointer是用*,但是block是用^
第二行也是定義一個myblock_type的變數,但是不一樣的是他指到的是一個block,
block的實作也是^開頭,緊接著是回傳type,接著是傳入的參數,後面接上{....}就是block實作的內容啦。
就像前面所說的,block本身就是anonymous function的方式去定義,
有些時候anonymous function非常好用,例如定義event handler,
我們可以註冊event handler時,直接在後面寫event handler的邏輯,可以很快的可以看到事件發生之後會要做哪些動作
不必像用function pointer或是delegate的方式需要把邏輯寫在另外一個function,
第三行(應該說第三個statement)就跟function pointer很像了,就是呼叫這個block。

當然block強大的地方就是block所包含的code可以使用外面的變數
例如下面的例子,temp是block外面所定義的,
但是我們可以直接拿來用
int ooxx()
{
     int temp = 5;
     typedef int (^myblock_type)(int a);
     myblock_type myblock = ^int(int a){
        return temp+1;
     };
     NSLog(@"return value = %d", myblock(5));
}

但是值得注意的是,上面的code,在block中會把temp當作常數看待,也就是說當定義block的時候,temp的值是5
即便我們在後來把temp改成1
但是得到的結果不會變,
這同樣也說明了一個特性,
那就是我們不能在block中,直接的修改(write)一個外部(區域)變數的值,
所以看下段code
int ooxx()
{
     int temp = 5;
     typedef int (^myblock_type)(int a);
     myblock_type myblock = ^int(int a){
        temp++;
        return temp;
    };
     NSLog(@"return value = %d", myblock(5));
}

這邊compile就會錯了,因為這邊不只是會讀temp的值,還會寫temp的值。
但是如果我們加上__block這個變數修飾..
那此變數就可以在block中修改。
下面就是個例子
int ooxx()
{
     __block int temp = 5;
     typedef int (^myblock_type)(int a);
     myblock_type myblock = ^int(int a){
        temp++;
        return temp;
    };
     NSLog(@"return value = %d", myblock(5));
}

以上就是常用的block的方法
事實上更常用的用法會是把block當參數傳到function,
這邊舉個GCD的例子,
dispatch_async(queue, ^(void) {
        // some task here
    });
這邊就是把一個block丟到queue中等待被背景執行,
而就是丟進一個傳入是void傳回是void的block
由於傳回是void,所以可以把^void(void){...}簡化成^(void){...}
在block的使用上常常會見到這種把block當參數的寫法,之後相信會漸漸的習慣。

探討Objective-C Block (part 1) - block的使用
探討Objective-C Block (part 2) - block變數
探討Objective-C Block (part 3) - block的記憶體管理

2011年7月22日 星期五

ios delegate你必須知道的事情

當你開始寫iOS程式不久,應該開始面對到很多的delegate,
不管是用別人的library或是自己寫library,可能都逃不了delegate。
為了怕有些人不知道什麼是delegate,在這邊簡單的介紹一下,
delegate中文叫做委託,通常會用在class內部把一些事件處理"委託"給別人去完成。
舉個例子,XML Parser可能他知道怎麼parse xml,但是parse到的東西要怎麼處理xml parser可能不知道。
所以NSXMLParser就提供了一個NSXMLParserDelegate給client去實作,
當parse到某個element的時候,就callback delegate所定義的message,
讓他client自己去決定怎麼去處理這個element。
好吧,我承認我解釋的很模糊,不過我這篇本來就不是要你搞懂什麼是delegate,
而是針對使用或是設計delegate的時候,可能會要注意的事情。

在我們的class中設計delegate的時候,我們通常會有幾個注意事項。
假設我的class叫做MyClass,那我們可能會有定義一個MyClassDelegate這個protocol當作我的delegate protocol。
而MyClass中我們可能是這樣寫。
@protocol MyClassDelegate <NSObject>
- (void) myClassOnSomeEvent:(MyClass*)myClass;
@end

@interface MyClass
{
    id<MyClassDelegate> _delegate;
}
@property (nonatomic, assign) delegate;
@end
上面的code我們注意到delegate此property是定義為@property (assign)
為什麼我們不用retain而要用assign呢?
原因就是在於iOS的reference counting的環境中,我們必須解決circular count的問題。
讓我們來寫寫我們平常都怎麼用delegate的,下面的code我想大家應該不陌生
- (void)someAction
{
   myClass = [MyClass new];
   myClass.delegate = self;
   ....
}
這邊很快的就出現circular reference了
假設上面的code是寫在一個myViewController的物件當中,
之後一旦myViewController的reference count變成1的時候,
myViewController跟myClass這兩個兄弟兩只剩下互相retain,那就變成了孤島,也就因此造成了memory leak!!!


也因為這樣,iOS官方文件才會要建議我們所以的delegate都要用assign property。
也就是所謂"weak reference"的property,他的特色就是雖然會持有對方的reference,但是不會增加retain count。
如此下來,當myViewController的retain count變成0,則會dealloc。
同時在dealloc中,也一併把myClass release,則myClass也跟著被release。
- (void)dealloc
{
   [myClass release];
   [super dealloc];
}


事情就結束了嗎? 還沒有唷...
這邊還有一個大家常常忘記的重點,那就是上面的dealloc這樣寫會有潛在危險。
應該要改成這樣
- (void)dealloc
{
   myClass.delegate = nil;
   [myClass release];
   [super dealloc];
}
你可能會很納悶,myClass不是馬上就會被release了嗎? 幹嘛要先把他的delegate設成nil?
那是因為我們假設myClass會馬上會被dealloc,但是現實狀況這個是不一定的,
有可能裡面內部有建個NSURLConnection,或是正在做某件事情而讓其他物件也retain myClass。
如果myClass沒有馬上dealloc,那他的myClass.delegate不就正指向一個不合法的位置了嗎? (此種pointer稱作dangling pointer)


解決方法是在MyViewController的dealloc中,在release myClass之前,
要先把原本指向自己的delegate改設成nil,這樣才可以避免crash發生。
在我之前寫的project,很大一部份的crash都是這樣造成的,因為這個問題通常不是每次都發生,
但是發生的時候確很難在重新複製,所以不可不慎啊。


但是很興奮的是到了iOS5中的Automatic Reference Counting這個問題可以有所改善。
在ARC中提出了一個新的weak reference的概念來取代原本的assign,
weak reference指到的物件若是已經因retain count歸零而dealloc了,則此weak reference也自動設成nil。
而原本舊的這種assign的作法,在ARC中叫做__unsafe_unretained,這只是為了相容iOS4以下的版本。

回顧重點:
如果你是寫library給別人用的,記得把你的delegate設成assign property,這樣才不會造成circular reference
當你是要始用別人的library,記得在你自己dealloc的時候,把delegate設成nil,以避免crash的事情發生。

References
[1] Ownership of Delegates, Observers, and Targets

2011年7月12日 星期二

Flurry - Mobile Application Analytics


原本想說要一個禮拜寫一篇文章的
想不到才一開始就懶散了
趁今天晚上下雨沒有辦法踢足球的晚上
再來趕功課

今天聽到客戶說要分析我們的ipad app上使用者的行為
希望可以知道使用者點了哪些item 還有做了哪些事情
希望進一步份析使用者的使用狀況
相信有寫web的應該二話不說就直接想到Google Analytics
但是到了mobile領域 還真不知道使用什麼東西勒
經過學長的指點 說有一個service叫做Flurry好像就是在做這樣的服務
用了一下還真的挺簡單易用了
在此就跟大家報告一下

要分析使用者的行為我的需求如下
1. 可以自訂我想監控的行為
2. 很簡單的可以插入我的code
3. 最好可以在離線的時候也可以統計使用者行為
4. server端有類似Google Analytics的報表介面
5. 有API可以抓報表資料整合外部的系統

想不到Flurry是五個願望一次滿足,
而且我只用了一個小時的時間就知道怎麼用了
完全沒有花到太多的功夫。
以下是使用方法及功能介紹

1. 註冊帳號
首先連到它們的官方網站 http://www.flurry.com/
註冊一個帳號 並且新增加一個application 並選擇你的平台 (我當然是選iPad, iPhone, iPod..)
之後就可以根據你選擇的平台Download SDK,而它也會給你這個app的application key
我們之後就必須用這個key來跟Flurry互動

2. 整合進你的project
a. 解開sdk,裡面主要有兩個檔案需要加到我們的project
FlurryLib/FlurryAPI.h
FlurryLib/libFlurry.a

b. 在你的AppDelegate的進入點,塞進初始化function

#import "FlurryAPI.h"

- (void)applicationDidFinishLaunching:(UIApplication *)application {
[FlurryAPI startSession:@"YOUR_API_KEY"];
}



3. 追蹤使用者行為
如果你想監控使用者是否按下一個button,你可以在對應的action加上下面這段code
[FlurryAPI logEvent:@"EVENT_NAME"];
這個event name可以自己定義,每個event name會變成server端的一個獨立數據


如果想用類似網站的pageview概念
可以使用
+[FlurryAPI logAllPageViews:]
+[FlurryAPI logPageView]
前者可以帶一個參數是UINavigationController或是UITabBarController,在每次切換的動作發生時,自動增加一次page view。
後者是手動的增加一次page view

4. 上傳追蹤資料。
上傳的動作是完全自動的。
Flurry會在app開啓或是關閉的時候自動的把這些追蹤資料整包的丟給server,
我們完全不用寫任何一段code。
另外因為他的做法本來就是先把這些使用者的行為放在local,
所以即使離線也可以紀錄,
直到下次使用者打開或是關閉app的時後是有連上網的時候在一併上傳

5. 報表系統
server端有完整的圖形化介面來統計數據
有Dashboard的整體分析
還有Usage, Audience, Events, Technical的細項分析
基本上功能還算完善







6. 數據資料下載
如果你有自己的server,想要把這些統計資料整合到自己的系統的話
我們也可以用它們的REST API去抓取這些資料
如果有需要的話可以連到這邊去看詳細資料
http://wiki.flurry.com/index.php?title=API_Usage


另外提醒一點的是,
在我初次測試的時候,我會想說log的event應該要馬上出現在server
事實上他並沒有那麼快的反應,可能會要等個半個小時左右才會更新一次報表 (詳細間隔還需要確認)
所以如果你的log沒有出現可能要請你等一會唷..

2011年6月25日 星期六

初探ARC - Automatic Reference Counting

WWDC 2011的影片開始下載
今天颱風天
那就抓個大家之前有討論的ARC來研究一下
下面是我的小小心得 :D

我想寫過iOS的人
絕對最痛很的就是令人髮指的Memory Management
就連TBBT的leonard也為此煩惱

source:
http://digdog.tumblr.com/post/3628378498/actually-sheldon-has-better-design-if-youre

程式中充斥著retain, release, autorelease
相信即使是老手,也難免會恍神而不小心少release而產生memory leakage
或是多release而產生bad access直接當掉
在這個大家都用garbage collection的時代
這種memory management的方式實在讓人寫得很痛苦

當然目前objective-c的做法也不能說完全沒有好處
畢竟gc的動作是無法預測什麼時候會去執行的
也許剛好在你正在忙的時候給你gc一下就卡了
這也是為什麼iOS跑起來可以比別家順的其中一個原因。
而在iOS5首次提出了ARC(Automatic Reference Counting)這個解決方案
試圖讓開發者減少寫Reference Counting的時候產生的錯誤。

ARC有哪些重點?
1. 它是Compile-time的技術,並不是runtime有一個thread去幫你處理reference count..
2. 開發者不能再直接使用retain, release, autorelease,改由compiler幫你塞這些code
3. 所有的property不再使用retain, assign而改用strong, weak取代
4. NSAutoreleasePool也要改用特有的 @autoreleasepool{ } 來取代
沒了!!

好啦,其實當然不只這樣,但是先從簡單的來
先看下面的code

-(NSString*)helloString
{
return [[NSString alloc] initWithFormat:@"Hello %@", _name];
}

以現在的MRC(Manual Reference Counting)環境
這種寫法有潛在的memory leak的風險
因為根據命名規則,非new, copy, mutableCopy開頭的message回傳的物件不能是retained object
而是要retained and autoreleased object
所以可能要改成這樣的寫法

-(NSString*)helloString
{
return [NSString stringWithFormat:@"Hello %@", _name];
}

但是如果使用了ARC之後
前後兩個方法都可以
主要是因為compiler會分析你的程式碼中呼叫的message是否有含alloc, new, copy, mutableCopy這些開頭
如果有的話就知道回傳的object的ownership是你的
接著根據你所在的message是否有alloc, new,... 來決定你回傳的物件是哪種類型。
以上面message為例子,回傳的應該是一個retained and autoreleased object
以往MRC中,我們就要跟leonard一樣,好好的計算一下才不會錯
但是在ARC這些複雜的加減法都交給compiler來幫我們搞定,完全不需要知道實作細節。

另外memory mangagement還有一個重點是在member variable
以前我們定義在class中的member variable並沒有留下是否要retain物件的資訊
除非我們透過@property (retain)或是@property (assign)的方法才可以知道個別的ownership語意。
但是在ARC中,因為所有retain/release都是要自動的
因此所有的Member variable就定義為預設都是strong type,這有點類似@property (retain)的味道
除非我們在member variable前加上__weak這個描述才會是weak type,這就有點類似@property (assign)
所以在程式碼中,若物件被指到strong pointer,就會自動retain
weak pointer則不會retain..

另外weak reference還有一個非常吸引我的功能
就是所謂的zeroing weak reference.
這個不得不歡呼一下
以前我們在用delegate pattern的時候
文件都教我們使用@property (assign)來hold delegate object
目的就是為了避免circular reference所造成的memory leakage
但是常常會遇到delegate所指到的object已經因為retain count歸零而被清掉了
若我們還去使用它 系統馬上就當給你看
所以以前我們都必須要記得把delegate設成nil
在ARC當中,如果weak pointer所指到的object的retain count已經是0的時候
則這個weak pointer也會被設成0
這就是所謂的"zeroing weak reference"
所以在weak pointer中又比以前的@property (assign)更為安全

從我最新抓的xcode4.2 with iOS5的環境來看
預設已經是把ARC變成預設了
而舊的project可以透過refactor的方式Convert to Objective-C ARC
究竟麻不麻煩,這我還不敢發表評論 XD
但是我相信長痛不如短痛
ARC絕對是對objective-c的記憶體管理有非常大的幫助。
讓我們準備擁抱ios5的ARC時代吧

References
[1] http://clang.llvm.org/docs/AutomaticReferenceCounting.html

2011年6月23日 星期四

在ios使用sqlite的救贖 - FMDB

最近有個專案需要小型資料庫,當然首選一定是iOS內建的sqlite,但是因為他是C的lib,使用上是出了名的困難。之前就有聽說用sqlite可以使用FMDB這個objective-c wrapper library。果不其然的簡單易用,我馬上把我使用經驗跟大家分享,如果你想馬上體驗,也可以去抓我的Sample project - MySqlite

安裝FMDB
方法很簡單,就跟所有的open source library一樣的三步驟。
1. 首先你要先把 libsqlite3.dylib 加進library
2. 在需要用到FMDB的檔案中#import "FMDatabase.h"
3. 把FMDB的.h跟.m加進你的project中
接著就可以開心的使用FMDB

產生Database
基本上就是透過+[FMDatabase databaseWithPath]取產生Database instance。

NSURL *appUrl = [[[NSFileManager defaultManager]
URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject];
NSString *dbPath = [[appUrl path] stringByAppendingPathComponent:@"MyDatabase.db"];
FMDatabase* db = [FMDatabase databaseWithPath:dbPath];

if (![db open]) {
NSLog(@"Could not open db");
}


產生所需table
其實寫過sql的應該對接下來的動作都不陌生,都一樣就是sql語法囉,詳細的請參考sqlite文件
下面的動作是產生一個user的table,並且有uid, name, descritpion三個column。我們使用IF NOT EXISTS來保證table不存在才會產生table。

if(![db executeUpdate:@"CREATE TABLE IF NOT EXISTS user (uid integer primary key asc autoincrement, name text, description text)"])
{
NSLog(@"Could not create table: %@", [db lastErrorMessage]);
}


新增record
在FMDB中,它有提供很貼心類似-[NSString stringWithFormat:...]的功能,但是比較不一樣的是如果你是字串,它會幫你加上括號,使用上更為便利。

if(![db executeUpdate:@"INSERT INTO user (name, description) VALUES (?,?)", name, description])
{
NSLog(@"Could not insert data: %@", [db lastErrorMessage]);
}


刪除record
一樣是下sql語法,這邊比較需要注意的是如果你要傳進去的是int, double這種基本形態,記得要轉換成物件,也就是用+[NSNumber numberWithInt]去轉換

if(![_db executeUpdate:@"DELETE FROM user WHERE uid = ?", [NSNumber numberWithInt:uid]])
{
NSLog(@"Could not delete data: %@", [db lastErrorMessage]);
}


查詢資料
在FMDB中有提供另外一個class FMResultSet來存放查詢出來的records,透過-[FMResultSet next]來iterate出所有的record

NSMutableArray* items = [NSMutableArray arrayWithCapacity:0];
FMResultSet *rs = [db executeQuery:@"SELECT uid, name, description from user"];

while ([rs next]) {
int uid = [rs intForColumn:@"uid"];
NSString *name = [rs stringForColumn:@"name"];
NSString *description = [rs stringForColumn:@"description"];

[items addObject:[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:uid], @"uid",
name, @"name",
description, @"description",
nil]];
}

[rs close];


基本上在其他oo平台中有用過sql的,對於FMDB的設計應該並不陌生,可以說完全不會有太多的困難。而相較於用傳統的plist或是archive的方法,我相信不管是效能上或是功能上,Sqlite+FMDB的組合絕對會更有優勢。對於中型以上的iOS程式應該會是標準配備。

2011年5月31日 星期二

XCode tip: 觀看UI Hierarchy的方法

在xcode的console下,我們除了可以看log
其實因為它是gdb環境,所以我們還可以對目前的環境做操作
我比較常用的是
gdb> po self
來列印中斷點所在的物件

也可以列印所在物件的message所回傳的object
gdb> po [self view]

如果該物件是個UIView.. 則可以列印它的UI Hierarchy
gdb> po [[self view] recursiveDescription]

這個是大絕招
gdb> po [[[[UIApplication sharedApplication] windows] objectAtIndex:0] recursiveDescription]

這樣就可以隨時pause.. 隨時看UI Hierarchy
就不需要在該UIView或UIViewController設定中斷點
參考資源
http://goo.gl/alEjL

2011年3月19日 星期六

XCode4一些使用經驗分享

XCode4 3/10正式開放下載,小弟馬上抓下來玩玩看。雖然從XCode3轉到XCode4一定會有些不適應,不過很快的也被很多讓開發更加便利的小細節所吸引。這篇是跟大家分享一些小技巧,希望對大家有幫助。

Command-Click / Option-Click
現在Command按著移到Symbol上面點擊,就可以直接跳到定義的部份
還有Option按著移到Symbol上面點擊,就可以直接看這個Symbol的文件
(如果有開發iOS的,記得到Preference -> Documentation裡面下載iOS library文件)

Assistant Editor:
這是 XCode4才有的新功能,方便我們同時修改兩個檔案甚至多個檔案。這對diff兩個檔案,或是同時想修改.h/.m都很有幫助。使用方法很簡單,XCode4右上角的Editor那邊就可以切換了。

而Assistant Editor也有一些進階用法。
1. 在左邊瀏覽頁面上,按Option + Click檔案,可以開啓檔案到Assistant editor
2. 在Editor中,按Command + Option + Click一個Symbol,可以在Assistant editor開啓。可以跟上面講的Command+Click選擇使用。
3. Command + Enter則可以關掉Assistant editor

Tab bar
不知道這是不是Xcode4才有的功能? 但這個功能在大部份的Editor都有,也很實用。
為了方便使用,我修改了一些設定
1. View -> Show Tab bar 把他點出來,讓Tab bar可以總是出現
2. Preference裡面 General -> Double Click Navigation 改成Use Separate Tab
這樣好處就是我在Project Navigators裡面雙擊一個檔案就可以開在一個新的tab
同樣的按著command.. 雙擊某個Symbol也可以在新的tab開啓。

Display Navigation Chooser

如果需要更有彈性的開啓方法
可以使用Navigation Chooser
1. 在Project Navigator當中用Shift+Option點檔案
2. 在Editor中按著Command+Shift+Option去點檔案


整理上面的幾個熱鍵
在Editor中
Command + Click: 同個Editor開Symbol
Option + Click: 看Symbol的Document
Command + Option + Click: 開在Assistant Editor
Command + Shift + Option + Click: 用Navigation Chooser來開
Command + DoubleClick: 用新Tab開
若在project navigator中
Click: 同個Editor開Symbol
Option + Click: 開在Assistant Editor
Shift + Option + Click: 用Navigation Chooser來開
DoubleClick: 用新Tab開

另外我覺得這個縮排熱鍵也很實用
先框選要排版的範圍
Command+[ 集體反縮排
Command+] 集體縮排
Control + | 集體重新排版
切換.h/.m改成
Command+Control+Up
Command+Control+Down
回到前一個位置
Command+Control+Left
回到下一個位置
Command+Control+Right

其他還有蠻多新的功能也很不錯 如
Auto build: 這點跟Eclipse很像,XCode會隨時在背景build,讓我們在Edit的時候就可以馬上發現是否有錯誤產生

Debug: 可以在Debug的時候,滑鼠移到變數上可以直接看變數的內容,甚至可以直接dump description到console


Workspace Window的設計:算是把以前多個Window整合到一個window的設計,這個好壞見仁見智,我是覺得還不錯的。


IB整合到XCode裡面: 這個早就該這樣設計了

新的Scheme的概念: 把以前的Build Configuration, Target, Executable都包進Scheme裡面來,這個使用上還需要習慣。