高見龍

Git/Ruby/Rails/iOS app Developer & Instructor, 喜愛非主流的新玩具 :)

在你的app裡加入廣告

image

蘋果的App Store上有許多的軟體,有的重量級的大作是直接掛個價錢就上架,有的是先提供免費的試玩版,滿意再買正式版,其中有些則是不收費,但會放一些廣告在裡面。這些軟體的開發者有的是真的佛心,有的是純練功,不過置入廣告來賺取一些生活費並沒什麼對錯的問題,畢竟大家也都是要過日子。放個廣告上來只要不影響整個app的使用也沒應該沒什麼大礙,畢竟這些開發者也還是需要一些”動力”才能繼續幫大家開發更好用的軟體。

你也許會好奇放這個廣告是能賺什麼錢,在不久前有個故事,就是有個人做了一個手電筒的app,裡面放了廣告,結果這個開發者在第一天就賺了將近1,400美元的收入,CTR將近12%。我不知道這個故事的真實性,也許這個故事只是為了吸引更多的開發者上勾,也或許這只是個特例,不過iAd的拆帳比例是六四分,開發者六,蘋果四。跟一般我們買網站banner的利潤比起來,六成的拆帳不算低。也許是大家覺得新奇,又或是不小心點到(畢竟iPhone那麼小一隻),跟一般網站banner的CTR比起來,手機上的廣告CTR都還滿高的。

其實要在app上置入廣告還滿容易,不需要寫到程式碼,一樣以前面那個地圖app做範例,首先點擊我要放廣告的那個xib檔,跳出interface builder,在元件庫裡可以找到一個iAd的元件:

image

把它拉到場景上,調整一下位置跟尺寸:

image

最後,因為project預設並沒有把iAd framework放進來,所以需要手動自己加,在Frameworks上按右鍵,選擇”Add” -> “Existing frameworks”:

image

測試看看,你應該可以看到一個測試的banner:

image

點擊之後的效果:

image

大概這樣就完成了,記得廣告不要放太大塊而影響整個app的運作囉(放大太塊到時候送審的時候也不一定會過)。

建議閱讀:

Apple Developer – iAd

iPhone app 實作練習 - 幫你的地圖 app 加上大頭針

image

前面幾篇有提到一些@category@progocol,還有一些記憶體管理的東西,我自己也看得頭很花,不過看一堆的理論還不如直接實作來得快。所以,接下來我們就來把前面那個陽春的地圖功能,加上一些註解或大頭針的功能,讓整個畫面看起來豐富、實用一些。

首先,我想要做的效果是在地圖上加個大頭針跟註解,如下圖:

image

做法就是在原來的MapView上加上一個註解(Annotation),讓MapView把它畫出來。要注意的是,Annotation本身不一種View,所以不是看得到的元件,不會直接在畫面上看得到。Annotation類別只是描述一些資料,例如title、subtitle,加到MapView之後,有個叫做MKAnnotationView的類別幫我們畫出來的。而把annotation加到MapView的語法是:

1
[map_view addAnnotation:(id <MKAnnotation>)annotation]

這可以讓你一次加一個Annotation到MapView裡,如果你要一次加多個大頭針進去,則可使用addAnnotations,後面接一個NSArray,裡面裝你要標示的Annotation即可。如果要移掉,則是使用removeAnnotation,如果要移掉多個則是使用removeAnnotations,詳細使用方法可再請參考使用手冊。

回來看上面那行,希望你還記得protocol的東西,如果不記得的話請再看一下這篇複習一下,那行的意思就是說addAnnotation方法後面接的參數必須是個有實作MKAnnotation這個protocol的物件。但是Cocoa Touch並沒有內建Annotation的類別可以直接塞給它,我是滿好奇這麼常用的功能為什麼不直接就內建進來,直接new一個來用就好了。不過沒關係,沒有的話我們就自己手動刻一個。

首先,先在Classes裡新增一個Objective-C class檔案,我把它命名為MyCompany(.m跟.h),然後我的Classes群組看起來會像這樣:

image

再來,修改一下MyCompany.h裡的@interface,讓它實作自MKAnnotation protocol:

1
2
3
4
5
6
7
8
9
10
// 檔案:MyCompany.h
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface MyCompany : NSObject <MKAnnotation>
{

}

@end

好了,然後呢? 要實作哪些方法要怎麼看? 在MKAnnotation上按滑鼠右選,選擇”find text in documentation”,它會去找到這個protocol的相關詳細說明。但我個人會更偏好在MKAnnotation上按住command鍵加上double click,它會跳到這個protocol的定義,直接看原始碼是最快的。MKAnnotation這個protocol的定義如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import <CoreGraphics/CoreGraphics.h>
#import <CoreLocation/CoreLocation.h>
#import <Foundation/Foundation.h>

@protocol MKAnnotation <NSObject>

// Center latitude and longitude of the annotion view.
@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;

@optional

// Title and subtitle for use by selection UI.
- (NSString *)title;
- (NSString *)subtitle;

// Called as a result of dragging an annotation view.
- (void)setCoordinate:(CLLocationCoordinate2D)newCoordinate __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);

@end

從定義看來,實作這個protocol只需要實作做coordinate這個getter就行了,其它的都是optional的。但因為我想要在大頭針上面加上註解說明,所以需要再實作title跟subtitle的setter,程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 檔案:MyCompany.h
#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface MyCompany : NSObject <MKAnnotation>
{
  CLLocationCoordinate2D coordinate;
  NSString *title;
  NSString *subtitle;
}

@property (nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property (nonatomic, retain) NSString *title;
@property (nonatomic, retain) NSString *subtitle;

@end

骨架好了,再來跳到MyCompany.m,把肉填起來:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 檔案:MyCompany.m
#import "MyCompany.h"

@implementation MyCompany

@synthesize coordinate;
@synthesize title;
@synthesize subtitle;

-(id) initWithCoordinate: (CLLocationCoordinate2D) the_coordinate
{
  if (self = [super init])
  {
    coordinate = the_coordinate;
  }
  return self;
}

-(void) dealloc
{
  self.title = nil;
  self.subtitle = nil;
  [super dealloc];
}

@end

前面幾行的@synthesize是對應到@property用的,這裡唯一比較長的程式碼是initWithCoordinate,這是個委任的建構子,用來在init的時候把coordinate也一併傳進來。

OK! 到這裡的前置工作已經完成了,接著要把它併到之前那個範例的程式碼裡了,首先別忘了要先把MyCompany.h給import進來:

1
#import MyCompany.h

再來一樣直接偷懶的把程式碼全部寫在viewDidLoad裡面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)viewDidLoad
{
  // 建立一個CLLocationCoordinate2D
  CLLocationCoordinate2D mylocation;
  mylocation.latitude = 25.01141;
  mylocation.longitude = 121.42554;

  // 建立一個region,待會要設定給MapView
  MKCoordinateRegion kaos_digital;

  // 設定經緯度
  kaos_digital.center = mylocation;

  // 設定縮放比例
  kaos_digital.span.latitudeDelta = 0.003;
  kaos_digital.span.longitudeDelta = 0.003;

  // 準備一個annotation
  MyCompany *mycompany = [[[MyCompany alloc] initWithCoordinate:mylocation] autorelease];
  mycompany.title = @"高思數位網路";
  mycompany.subtitle = @"媽,我在這裡啦!";

  [map_view setRegion:kaos_digital];

  // 把annotation加進MapView裡
  [map_view addAnnotation:mycompany];

  [super viewDidLoad];
}

上面這段程式碼是從前面那篇借來用的。做到這裡,按下Build and Run,應該就可以看到有一根紅色的大頭針定在畫面上:

image

再點一下大頭針就會跳出說明:

image

其實MKAnnotationView能做的變化還不少,例如變更大頭針的樣式、顏色,或是在註解的左邊或右邊加上按鈕,按下按鈕之後會執行其它的事情(callout),更詳細的說明可再參考Apple的官方手冊,地圖在行動裝置上能做的應用還挺多的,大家可以再想想怎麼拼裝組合囉 :)

原始檔下載

建議閱讀:

iOS Reference Library – MKAnnotationView

Category in Objective-C

image

OOP的精神之一,就是如果你想研發一台新款式的車子,你並不需要重新發明輪子,通常的做法會去繼承某個現有的”車子類別”,然後加上你要的功能跟屬性,改一改就變成一款新的車子可以來騙錢了。

不過有時候你想幫原來的類別加功能,但又不想動到原來的程式碼,例如你可能下載了某款功能超強的2D物理引擎程式碼,但因為某些小地方寫的不合你的需求,於是你便動手改原始碼來加功能。這當然沒問題,但萬一原作出新的版本,你要不就選擇維持自己原來的版本不update,不然就是update之後,你原來加在舊版本的程式碼得再重貼一次到新版。

Objective-C裡有個叫做category的東西可以幫你在現有的類別加上新功能,這樣一來上面這個問題就可以搞定了。跟別的程式語言比較起來,category的觀念有點像是在Ruby的mixin或是Python的open class,都是在不影響或修改原來的類別或模組的情況下去修改原有的功能。

舉個例子,因為NSObject是所有物件的源頭,但我想要加一個方法讓所有的子類別都可用(把要加的功能放在繼承階層的最源頭並不是好的設計,在這裡只是舉個例子而已)。程式碼這樣寫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// interface
@interface NSObject(MySuperObject)
-(void) printRetainCount;
+(void) sayHello;
@end

// implementation
@implementation NSObject(MySuperObject)

-(void) printRetainCount
{
  NSLog(@"The retain count is %d", [self retainCount]);
}

+(void) sayHello
{
  NSLog(@"Hello everybody!");
}

@end

這裡用的是在後面加個小括號以及category的名稱。要注意的是category只能加method(instance method或是class method都行),沒辦法增加instance variable(其實也不是完全不行,只是可能要用一些怪招,不過如果要做到這種程度,是不是該考慮直接用一般的繼承就好?)。使用起來的樣子:

1
2
3
4
5
6
7
8
9
10
11
// 建立Book物件
Book *b = [[Book alloc] init];

// instance method from category
[b printRetainCount];

// 用完放掉
[b release];

// class method from category
[Book sayHello];

上面這段程式碼如果一般的情況下,在編譯階段就會跳出警告(認不得printRetainCount跟sayHello這兩個方法),硬是執行的話就會直接錯誤。但因為我們已經有了category的加持,所以執行結果會是:

The retain count is 1
Hello everybody!

因為category是在原來的類別裡加功能,所以你可能會想萬一原來的類別裡有個跟你的category同名的方法怎麼辦? 答案是,會以category的定義為主,也就是原來類別裡的那個方法就被你蓋掉了,當然這不見得是你想要的結果。所以通常這種方法擴充的,會建議可以在前面加個prefix,避免跟原來的方法有重複而造成不幸的結果。

另外,前面有篇提到protocol的文章,也提到了informal protocol,其實它就是一種依附在NSObject上,但是沒有把功能實作出來的category。一般的protocol必需乖乖實作所有@required的方法(在Objective-C 2.0以前全部預設都是@required),但在informal protocol並沒有強制規定全部都要實作出來。事實上,在Objective-C 2.0之後才在protocol裡加進來的@optional語法,就是打算用來取代informal protocol的,在比較新版本的SDK大多都是用標準的protocol在寫了。

範例原始檔下載

Set to Nil After Release

image

iOS的記憶體管理真的不少東西要學,在Objective-C裡有時候會看到一些程式碼這樣寫:

1
2
3
4
5
6
7
-(void) someMethod
{
  [mybook release];
  mybook = nil;

  [super dealloc];
}

先把它release掉了,然後下一行再把它指向nil。nil跟其它程式語言裡的null或none是一樣的東西,就是nothing、虛無飄渺、沒有任何東西的意思。

問題是,這個nil是必要的嗎? 不指向nil會出什麼包嗎?

先來釐清一個觀念:對一個物件發送release訊息並不會直接把物件佔用的記憶體還給記憶體,release這個動作只是減少retain count而已,當retain count變成0的時候會自動啟動dealloc,這時候才是真正的被消滅掉。

在Objective-C裡所有的物件變數都是指標,指向某一塊配置的記憶體,所以,下面這行程式碼:

1
mybook = nil;

意思是把mybook這個變數指向nil。這樣說來,是不是也不用什麼release了,直接把變數設定成nil不就好了?

我們先來看個例子:

1
UIImage *img = [[UIImage alloc] init];

這應該不陌生了,這建立一個名為img的指標變數,並指向某一個UIImage的實體。如果我在它下面再加一行:

1
img = [[UIImage alloc] init];

就是再做一次alloc/init,讓剛剛那個img變數指向這個新的實體。看起來很直覺,但指標的東西跟我們一般寫的程式的習慣不同,當你把img指向另一個新的實體,原來舊的實體並不會消失,它會存在記憶體裡,只是沒有人指向它,所以也沒有人能再存取它。當然在有支援GC的環境上,系統會自動回收那些沒人要的東西,但在不支援GC的環境,那個沒人要的孩子就會一個人孤獨的漂流著了,所以如果你要這樣做,請記得先把原來的那顆給release掉,再給它一顆新的。

那如果我再接著這樣做:

1
img = nil;

把img指向nil並不會減少retain count,那剛剛前面我產生的第二個物件就會跟第一個物件一樣,它不會消失,它只是在記憶體空間裡漂流,當然它佔用的記憶體也不會還給系統。所以請切記,如果你要把物件給丟掉,不要直接把變數設成nil,請使用release或autorelease

回到最一開始的問題,我的答案是:其實是沒什麼需要一定要在release之後再弄個nil給它,但這樣做是個好習慣。

那什麼時候會出問題?

在別的程式語言裡,當你要存取null物件的時候,通常會直接跳錯誤給你看,但在Objective-C裡如果你對nil送訊息的,即使是沒定義的method,它也只會靜靜的不回應而已。所以,回到最前面的程式碼來看,當你把mybook給release之後,也許有那個萬一的情況下,有別的method要存取它,但它可能因為已經被dealloc掉了,所以就會爆炸了(發生機會很小,但不代表永遠不會發生)。但如果把mybook指向nil,即使有人要再來存取這個mybook也不會出現錯誤了。

以上,我對記憶體管理的東西的了解相當有限,所以如果以上內容有誤還請不吝指正 :)

Protocol in Objective-C

image

前面提到了OOP的繼承,但不像C++可以有多重繼承,Objective-C是單一繼承的,如果想要做到一個類別同時擁有多種型別的能力,可以透過實作其它型別的interface來達成這個目的。在Java/AS3是用”interface”這個關鍵字,在Objective-C則是用”@protocol”。(有寫過Java/AS3的要特別注意不要把interface跟protocol搞混了,在Objective-C的interface等於Java/AS3的class,而protocol則是相當於interface)

直接來看看要怎麼做吧。如果你要新增一個自定的protocol的話,可以直接在你的專案裡新增一個protocol檔:

image

當然,你要全部寫在一起也沒人反對,只是為了模組化以及以後的可重複使用考量,建議獨立出來另外寫。新增完成之後(它是一個header檔),就可以開始來寫了,程式碼如下:

1
2
3
4
5
6
@protocol Drawable

-(void) draw;
-(void) changeColor;

@end

在Objective-C裡的protocol是用@protocol這個語法來定義的。在上面這段程式碼裡,我放了兩個方法,但沒有寫內容。接下來如果我要實作自這個protocol的話,所有定義在@protocol裡的方法都得實作出來。另外,在Objective-C 2.0之後加了@required@optional的語法,可以讓你設定這個method是不是必需一定要實作的項目。用法如下:

1
2
3
4
5
6
7
8
9
10
@protocol Drawable

@required
-(void) draw;
-(void) changeColor;

@optional
-(void) whateverMethod;

@end

如果沒特別標明的,預設是@required。如果你要實作這個protocol的話,照英文字面來看,@required的部份是規定要實作的,@optional的話就隨你高興了。要注意的是@required跟@optional這兩個語法的影響範圍,是從它以下所有的method都會被影響,直到另一個directive或是@end為止,所以如果你要省略@required的話,記得那些method要寫在@optional前面。接下來來看看要怎麼實作這個protocol:

1
2
3
4
5
6
7
8
9
10
#import <Cocoa/Cocoa.h>
#import "Drawable.h"

@interface Book : NSObject <Drawable>
{
  int price;
}
@property int price;

@end

實作protocol的方法就是用”<>”標記,裡面放protocol的名稱。並不限定只能實作一個protocol,如果要實作多個protocol的話,則是用逗點分開:

1
@interface Book : NSObject <Drawable, Openable>

因為到目前為止,我們都還沒實作那個protocol裡定義的方法,所以這時候如果直接按下Build的話,就會跳出警告訊息:

image

接著來把該做的填一填吧。因為在protocol的地方已經有定義好了方法,所以在@interface的地方就不用再特別寫一次,只要在@implementation裡補上該實作的方法就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// --------------
// interface
// --------------
#import <Cocoa/Cocoa.h>
#import "Drawable.h"

@interface Book : NSObject <Drawable>
{
  int price;
}
@property int price;

@end

// --------------
// implementation
// --------------
#import "Book.h"
@implementation Book

@synthesize price;

// 實作方法draw
-(void) draw
{
  NSLog(@"draw me!");
}

// 實作方法changeColor
-(void) changeColor
{
  NSLog(@"change color!");
}

@end

如果你實作了所有@required的方法的話,則稱為遵守(conform)或採納(adopt)這個protocol(硬翻成中文還是覺得怪怪的,還是英文比較簡潔直接)。若要檢查某物件是否有乖乖遵守某個protocol的規定:

1
2
3
4
5
Book *book = [[Book alloc] init];
if ([book conformsToProtocol:@protocol(Drawable)] == YES)
{
  NSLog(@"the book is conform to Drawable protocol");
}

protocol本身也可以像一般類別的繼承,例如:

1
2
3
4
5
6
7
@protocol A
-(void) methodA;
@end

@protocol B <A>
-(void) methodB;
@end

這時如果你要實作protocol B,則methodA跟methodB都需要實作。

另外,你也可以把protocol拿來當一般的型別定義來用,例如:

1
id <Drawable> some_object;

表示說這個some_object是個有實作Drawable這個protocol的物件,在編譯階段就可以先做型別檢查。當然也可以一次多個,一樣用逗點分開:

1
id <Drawable, Openable> some_object;

上面提到的這種用@protocol來定義方法的,稱做formal protocol,從名字看大概猜得出來一定也有叫做informal protocol的東西,不過這個會在category的部份再做說明。