高見龍

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

OOP in Objective-C

這回來我們來看看在Objective-C裡的物件導向程式(Object-Oriented Programming)的東西,不過有些像是”何謂物件導向”以及”物件導向的優缺點”之類的基礎觀念就不特別提了,網路上應該可以找到更多更詳細的參考資料,我假設你曾經在別的程式語言有寫過OOP,大概知道繼承、封裝是怎麼回事。

在別的程式語言裡,會用關鍵字class來定義類別,在Objective-C的話則是用@interface這個關鍵字來定義。不要搞混這個@interface跟其它程式語言的interface,它們講的是不同的東西(其它程式語言的interface在Objective-C裡比較像是Protocol,之後會再做說明)。為了讓環境單純一點,我們直接開一個Command Line Tool的專案來練習手感:

image

下一步設定專案名稱,我取名為OOPTest。再來我們要來新增個類別,在Source資料夾上按右鍵,新增檔案:

image

選擇一個Objective-C Class,Subclass的地方選擇NSObject:

image

再來因為我要來寫個Book的類別,所以我就把它取名為Book.m,這邊要注意的是,這裡有個”Also create Book.h”也把它勾起來,它會一起幫忙產生header檔:

image

要特別說明的是,在Java/AS3裡,類別名稱跟檔案的名稱(*.java、*.as)是要同名的,但在Objective-C裡卻沒這樣的強制規定,不過為了方便維護起見,我會習慣把類別跟檔名取一樣的名字。另外這邊的”.m”跟”.h”檔,.h檔是它的”h”eader定義檔(放@interface的地方),.m則是它的i”m”plementation實作檔(放@implementation的地方),其實你要放在同一個檔裡,甚至是全部直接寫在main裡也ok,但為了好維護,通常我們會另外把它拆成兩個檔案來寫。接著我們就要來Book.h裡放類別的定義:

1
2
3
4
5
6
7
8
9
10
@interface Book : NSObject
{
  int price;
}

-(int) price;
-(void) setPrice: (int) p;
+(void) printBookInfo;

@end

其實這個類別定義,用翻譯成一般話就是說「我定義了一個叫做Book的類別,在這個類別裡有一個price的整數變數,並寫了一對的getter跟setter,還有一個叫做printBookInfo的class method」。為什麼需要getter跟setter? 在 Objective-C 裡的實體變數(instance variable,可簡稱為ivar)預設是 protected 的,所以你沒辦法從外部直接存取。當然也是可以透過 @public 語法來設定成公開的屬性,但這並不是好的 OOP 實作方法。

這裡有幾個比較陌生的字,NSObject是Objective-C裡最上層的類別,所有的物件都是繼承自它。要繼承的話,只要用一個冒號”:”即可,後面放的是要繼承的父類別名稱。有注意到前面的加號(+)跟減號(-)了嗎? 它也是有特別意義的,加號代表這個方法是屬於類別方法(class method),減號則表示這個方法是實體方法(instance method)。有什麼不同? class method是針對類別本身呼叫的,不需要產生instance即可使用,像這樣:

1
[Book printBookInfo];

而instance method則需要先由類別產生物件,再由物件來呼叫:

1
2
3
Book *book = [[Book alloc] init];
[book setPrice: 20];
NSLog(@"the price of the book is %i", [book price]);

在Java/AS3裡,類別的定義跟實作是寫在一起的,但在Objective-C裡則是分開寫的。到目前為止,我們只用了@interface只是”定義”了類別的骨頭,但還沒實際把肉填進去,要填肉的話,用的是@implementation語法,接著切換到Book.m(你可以用command + option + 上鍵來切換):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import "Book.h"

@implementation Book
-(int) price
{
  return price;
}

-(void) setPrice: (int) p
{
  price = p;
}

+(void) printBookInfo
{
  NSLog(@"Hello, This is a book");
}

@end

記得我們在@interface裡有定義了2個instance method跟1個class method,在@implementation就要把它實作出來。補充一下,這裡的#import跟在c/c++裡出現的#include有類似的用途,都是把東西給匯進來使用,但這兩個語法最大的差別是#import只會匯入一次,所以你可能會在c/c++的程式碼裡常看到#ifdef之類的用法來做檢查,但如果用#import就不用擔心這問題了。

最後回來我們整個程式的進入點,在這個範例裡是OOPTest.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
#import <Foundation/Foundation.h>
#import "Book.h"

int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

  // 新增一個Book實體
  Book *b = [[Book alloc] init];

  // 設定價錢
  [b setPrice:20];

  // 把價錢印出來
  NSLog(@"the price of the book is %i", [b price]);

  // 用完記得放掉
  [b release];

  // 這是類別方法
  [Book printBookInfo];

  [pool drain];
  return 0;
}

按下”Build and Run”之後的執行結果:

the price of the book is 20
Hello, This is a book

如果你的程式在編譯過程有發生錯誤而沒辦法正常的執行,那可能是有程式碼打錯字,再回頭檢查看看,常見的錯誤是大小寫打錯,還有每行程式碼最後面記得要加分號。

你也許會好奇,Book哪裡來的allocinit,還有release方法? 因為這些方法都是繼承來的,不用特別寫就有了。

不過每次為了某個變數就要寫一對getter/setter應該覺得很煩人吧,Objective-C有提供了@property@synthesize語法,之後會再特別說明相關用法。另外,Objective-C是單一繼承的,可以靠Protocol以及Category來補足這部份的不足。下一篇,我們會來看看@property跟@synthesize是怎麼回事。

建議閱讀:

Apple電腦之無窮迴圈

上一篇練習MapView的時候,發現當把MapView的showsUserLocation打開後,在模擬器上執行的結果,會出現在美國的某個地方:

image

拉近一看才發現,原來那個發亮的點的地址是在”Infinite Loop”,正是Apple電腦的總部所在地。然後又到google map上看了一下衛星照片:

image

就是一個圈圈的樣子,如果你用google map的街景模式去看,還會看到在入口的地方放了一個”1″:

image

因為他們的門牌號碼就是”1 infinite loop”(無窮迴圈1號),真有趣 :)

iPhone app 實作練習 - 地圖 app

image

有看過在iPhone裡直接秀出地圖app嗎? 看起來挺酷的吧。猜猜看寫一個把google map載入到iphone的功能要花多少時間? 又要寫幾行程式碼? 如果只是要單純的把地圖拉進iPhone顯示,答案是3分鐘以內(還包含開啟Xcode、interface builder跟編譯的時間,如果動作快一點,不用1分鐘就搞定了),而且還不用寫到一行程式碼,你可以親自開Xcode來試試看就知其實並不會太麻煩的。

在iOS 3.0之前要讀取地圖可能得用一些比較麻煩的方法去抓google map進來,但在3.0之後新增了MapKit Framework之後,人生就都變彩色的了。

打開Xcode,新增專案,這裡我先偷懶的選用view-based application,選擇專案名稱,這裡我幫它取名為MapDemo(不一定要取這名稱,只是如果你取的名稱是不同的,後面專案的程式碼檔名可能會有所不同)。

image

專案新增後,打開Resources資料夾裡的MapDemoViewController.xib,它會自動啟動interface builder,接著到Library裡找到一個叫做MapView的元件,把它拖拉到場景上放好對齊。

image

存檔,回到xcode環境。接下來看一下你的Frameworks資料夾,因為預設的Framework只放了UIKit、Foundation跟CoreGraphics這三個,如果要用到MapView的話,還需要把MapKit Framework加進來,不然編譯會發生錯誤。在Frameworks資料夾按右鍵->Add->Existing Frameworks..

image

找到MapKit Framework,把它加進來:

image

你應該就可以在你的Frameworks資料夾裡看到MapKit了:

image

加好之後,執行Build and Run,這樣就完成了!

image

從開始到現在如果手腳快一點可能還不用一分鐘就搞定了,而且除了一開始你打了MapDemo幾個字母的專案名稱外,應該沒有再需要用到鍵盤輸入任何程式碼。MapView預設是以Google Map為資料來源,而且這個地圖已經有包含放大、縮小、拖拉的功能了。你可以在試著模擬器裡按著option鍵加滑鼠移動可以模擬你在實機上縮放的手勢。

但是,就這樣出現個世界地圖的app似乎沒什麼鳥用,跟之前看到某房仲業推出的手機看屋的app的複雜度還差得多,不過但至少知道其實在iPhone上面加入地圖功能不算是非常難做的功能。

接著來加一下可以依據經緯度座標來定位的功能:

1
2
3
4
5
6
7
8
9
10
11
// 檔名:MapDemoViewController.h

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

@interface MapDemoViewController : UIViewController
{
  IBOutlet MKMapView *map_view;
}

@end

然後到interface builder裡,把這個map_view變數”串”到你剛剛拖到場景上的那個MapView,讓我們在程式碼裡面可以透過這個變數去操作場景上的那個元件。這個動作如果不熟悉的話,可以參考前面那篇BMI app實作的文章。

image

串完之後,接著就要來開始寫code了,為了展示目的,我直接把程式碼寫在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
30
31
// 檔名:MapDemoViewController.m

#import "MapDemoViewController.h"

@implementation MapDemoViewController

- (void)viewDidLoad
{
    // 建立一個region,待會要設定給MapView
    MKCoordinateRegion kaos_digital;

    // 設定經緯度
    kaos_digital.center.latitude = 25.01141;
    kaos_digital.center.longitude = 121.42554;

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

    // 把region設定給MapView
    [map_view setRegion:kaos_digital];

    [super viewDidLoad];
}

- (void)dealloc
{
    [super dealloc];
}

@end

存檔,回到Xcode執行Build and Run,畫面看起來應該比剛剛的好多了。剛我在程式裡設定的那組經緯度是我公司的位置,沒問題的話你看到的地圖應該會在台灣新北市樹林跟新莊的交界處附近。

image

另外,MapView預設的模式是一般的街道地圖的,你可以用程式來調整:

1
2
3
4
5
6
7
8
// 這是預設的標準模式
map_view.mapType = MKMapTypeStandard;

// 衛星地圖模式
map_view.mapType = MKMapTypeSatellite;

// 混合模式
map_view.mapType = MKMapTypeHybrid;

或是在interface builder環境,從MapView的inspector去調整:

image

如果你想要秀出使用者的所在位置:

1
map_view.showsUserLocation = YES;

或是用inspector設定:

image

如果你把這個選項開啟的話,在模擬器裡會出現在美國的某個有趣的地方:

image

希望做到目前這個階段對大家還不會太困難,但老實說就算現在這樣其實也沒什麼用,甚至也不知道到底指定的位置在哪裡,想靠它賺大錢還早。不過沒關係下一篇我們會來弄一些比較進階的功能,例如是幫地圖加上大頭針,或是加一些自訂的icon圖示等功能,讓地圖看起來更漂亮、實用些。但因為可能會用到在Objective-C裡的Protocol觀念,所以在正式開始實作之前,會先來說明一下在Objective-C裡的CategoryProtocol是怎麼回事。

以上,希望對各位有幫助,若有錯誤的地方還請不吝指教,感謝 :)

Objective-C記憶體管理之Autorelease

上一篇概略的提到了Objective-C裡的記憶體管理,其實Objective-C其實還滿多這種眉眉角角的地方要注意的,接下來我們來看看關於Autorelease Pool的東西。這篇文章會比較長,而且會比較悶又囉嗦,還請多多擔待。

在開始之前要先看一下這一行程式碼:

1
NSMutableString *s1 = [NSMutableString stringWithString:@"hello eddie"];

如果用地球的語言來說,就是「建立一個NSMutableString型態的s1指標變數,這個指標變數指向某個內容為”hello eddie”的NSMutableString物件」,在s1前面的那個星號*表示這個s1是一個指標(pointer)變數,不過這裡不會對指標有太多的說明(因為我跟指標也不熟),有興趣可再找找C/C++的相關書籍來研究。在Objective-C裡的物件變數都是指標,所以來看看底下這個例子:

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
#import <Foundation/Foundation.h>
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

  // 建立2個字串變數
  NSMutableString *s1 = [NSMutableString stringWithString:@"hello eddie"];
  NSMutableString *s2 = s1;

  // 印出這兩個字串的位置
  NSLog(@"location of s1 is %p", s1);
  NSLog(@"location of s2 is %p", s2);

  // 印出兩個字串的內容
  NSLog(@"s1 is %@", s1);
  NSLog(@"s2 is %@", s2);

  // 改變了s1的內容
  [s1 setString:@"eddie"];

  // 看看這兩個字串的內容是否改變
  NSLog(@"s1 is %@", s1);
  NSLog(@"s2 is %@", s2);

  [pool drain];
  return 0;
}

輸出結果:

location of s1 is 0x10010cad0
location of s2 is 0x10010cad0
s1 is hello eddie
s2 is hello eddie
s1 is eddie
s2 is eddie

因為s1跟s2其實是指向同一個地方,所以印出來的記憶體位置也是相同的。一但修改了s1的內容,指向同一個地方的s2也會跟著改變。為什麼要特別提這個? 待會後面會用到。

回到主題,在上一篇文章有提到東西用完要release掉,把記憶體還給系統,這個autorelease pool照字面看起來像是會自動釋放資源的東西,是沒錯,但其實它並不是Garbage Collection。簡單的說,想像你有個游泳池(pool),裡裝著一堆被標記為autorelease的客人,當這個池子裡的水要放掉的時候,它會自動對這個池子裡的所有客人發送release訊息叫他們滾蛋,上一篇也提到,當這些客人的retain count降到0的時候,dealloc方法會自動被呼叫,然後把自己清掉並歸還記憶體。

有點複雜嗎? 我直接借用Stanford大學CS193P第三堂課的其中一張投影片來說明可能會比較清楚:

image

這個圖示就是app運作的生命週期,前半段的程式啟動、載入..等等的動作反正就是那麼一回事,重點在於wait for event跟handle event的這個迴圈(run loop),如果沒意外,這個迴圈會一直執行下去,直到可能有人打電話來中斷了你的app,或是你按了home按鈕離開這個app為止。從圖例裡可以看到而autorelease pool在每個run loop都會進行釋放。

那要怎麼樣把物件設定為autorelease? 兩個方法:

1.明確的呼叫autorelease方法

1
NSNumber *n1 = [[[NSNumber alloc] initWithInt:100] autorelease];

2.上一篇有提到「當你用alloc、new或是copy開頭的方法建立一個物件的話,程式就會向系統要一塊記憶體來放這顆物件,而這顆物件就算在你頭上」,其實除了這些以外的方法來建立物件件的話,它回傳的就是個autorelease物件了。舉例來說:

1
2
3
4
// 你用alloc建立的,用完就要把它release掉
NSNumber *n1 = [[NSNumber alloc] initWithInt:100];
// 用完之後release
[n1 release];

如果是這樣:

1
NSNumber *n1 = [NSNumber numberWithInt:100];

這樣不需要再對n1呼叫autorelease方法,這個n1就是autorelease了。

要特別注意的是,autorelease跟release不同,release會馬上在執行後把retain count減1,而autorelease則是待會才會減1。另外,在上一篇最後提到的記憶體管理原則:「You only release or autorelease objects you own」,一但你把物件設定為autorelease之後,這個東西基本上就不算是你管的,你之後就不需要也不應該再去對它做release的動作了。

或許你會覺得用一般的release就好了嗎? 為什麼特別需要用到這個autorelease? 其實有些地方還是真的非它不可,我們來看看底下這個例子:

1
2
3
4
5
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  return the_name;
}

這裡我們寫了一個會回傳NSString物件的方法,看起來好像沒問題,而且在其它語言也常這樣寫。但注意到在這邊這個區域變數the_name是用alloc產生的,但都沒地方把它release掉,所以可能會出問題。好吧,既然這樣那就把它release掉,所以改寫成:

1
2
3
4
5
6
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  [the_name release];
  return the_name;
}

這樣不行,因為在return之前就release掉了。那把release動作放到return後面?

1
2
3
4
5
6
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  return the_name;
  [the_name release];
}

這當然也不行,在return之後的動作是不會執行的。這時候,autorelease就派上用場了:

1
2
3
4
5
-(NSString *) getBookName
{
  NSString *the_name = [[NSString alloc] initWithString:@"This is a book"];
  return [the_name autorelease];
}

這樣這個區域變數因為被標記成autorelease,它在待會,也就是run loop結束的時候會自動被釋放掉了。

再舉個例子,在Objective-C裡,物件的instance variable預設是設定為protected的,如果沒有getter/setter的話是沒辦法取得取得或設定該物件的屬性(如果你是用@property/@synthesis的做法就不用這麼麻煩了)。所以,舉個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface Book : NSObject
{
  NSString *title;
}

-(void) setTitle: (NSString *)book_title;
-(NSString *)title;
@end

@implementation Book
-(void) setTitle: (NSString *)book_title
{
  title = book_title;
}

-(NSString *)title
{
  return title;
}
@end

我在這裡寫了一組getter跟setter,分別可以設定及讀取Book這個類別的title屬性,看起來是很直覺的getter/setter寫法,在其它的程式語言中也常這樣寫。其實在有GC的環境,這樣寫是沒問題的[註1],但在不支援GC的環境上,例如iphone,這樣的寫法可能會出問題,直接來看個例子:

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
int main (int argc, const char * argv[])
{
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

  // 建立物件
  Book *book = [[Book alloc] init];
  NSMutableString *s1 = [NSMutableString stringWithString:@"hello eddie"];

  // 把字串s1透過setter設定給title屬性
  [book setTitle:s1];

  // 輸出結果:the title of the book is 'hello eddie'
  NSLog(@"the title of the book is '%@'", [book title]);

  // 修改s1的內容
  [s1 setString:@"not eddie"];

  // 輸出結果:the title of the book is 'not eddie'
  NSLog(@"the title of the book is '%@'", [book title]);

  // 把s1釋放掉
  [s1 release];

  // 這一行就爆炸了..
  NSLog(@"the title of the book is '%@'", [book title]);

  [book release];
  [pool drain];
  return 0;
}

記得一開始有提到指標的東西,在這裡當把s1設定給title的時候,其實是s1跟title是指向同一個地方,所以當修改s1,title也會跟著改,這不一定是你想要的結果。更麻煩的是,當你送release訊息給s1之後,title也跟著指不到地方。所以setter的地方應該改寫成:

1
2
3
4
5
6
7
8
-(void) setTitle: (NSString *)book_title
{
  if (title != book_title)
  {
      [title release];
      title = [book_title retain]; // 或是copy,端看用途而定
  }
}

為什麼這裡要先做判斷是否相同? 因為title跟傳進來的book_title有可能會指向同一個物件,如果直接先把title release掉,會導致book_title一起跟著無法存取。如果title跟傳進來的book_title的記憶體位置不同的話,那就把原來的title放掉,並從把傳進來的book_title做retain(或copy),確保retain count增加,即使像剛剛那個情況把傳進來的東西release掉,因為retain count不會降到0,所以就還會留著。這裡也可以使用autorelease來處理:

1
2
3
4
5
-(void) setTitle: (NSString *)book_title
{
  [title autorelease];
  title = [book_title retain]; // or copy
}

這邊就不做判斷了,直接把title設定成autorelease,然後待會交給autorelease pool去放掉就好。

這樣看來,感覺autorelease好像很方便,何不乾脆把全部都交給autorelease pool處理好了? 的確是比較方便沒錯,但如果可以的話,請儘量用手動的release而不要用自動的autorelease。一來程式設計師能在資源用完之後馬上主動還回去本就是個好習慣,二來autorelease是在每個run loop才釋放一次,雖然每個run loop可能是零點零零幾秒而已,但也有可能在一個run loop裡的某個迴圈裡一下子產生太多的物件,還沒跑到pool釋放的地方就爆炸了(通常遇到這種情況會在產生大量物件的地方放個巢狀的pool在裡面,讓內層的pool提早釋放,而不是等到最外層的run loop才釋放)。

希望這篇文章對一起學習的朋友會有幫助,若內容有誤也請不吝指正。

建議閱讀:

註1:GC支援功能是可以手動打開,不過要記得不是每個環境都有GC可以用的,在Project Settings裡..

image

找到Objective-C Garbage Collection的選項,設定成Supported:

image

iPhone App實作練習-BMI

看一堆Objective-C原理的書,什麼物件、類別、記憶體管理的,沒實作個東西出來就整個沒成就感。突然間想起自己是北醫營養系畢業的,但總是不務正業,終於有機會發揮我專長的時候了,這次就來寫個計算BMI(Body Mass Index)的iPhone app吧。

我的SDK版本是4.1的,所以如果你的SDK版本跟我的不一樣的話,看到的畫面可能會跟我的不太一樣。

開啟Xcode,選擇iOS Application->View-Base Application:

image

下一步,選擇專案名稱,我這裡取名為「BMI」,存檔,準備開始動工。接下來,你應該會看到一堆資料夾跟檔案(如下圖):

image

暫時先不解釋那些檔案要幹嘛的,直接按下上方工具列的”Build and Run”按鈕。沒意外的話,你應該會看到一個iPhone模擬器自動開起來,並且看到模擬器裡出現一個空白畫面,上面什麼都沒有,因為我們還沒開始寫 :)

接下來要來上面放一些元件了。請點開Resources裡的BMIViewController.xib (如果你的專案名稱跟我不同的話,檔名也會跟著不同)。點開之後應該會開啟Interface Builder,這個是讓你用視覺的方式把元件佈置到場景上的工具。接下來你應該可以找到一個library視窗(如下圖),裡面放了許多預先建立好的元件。如果找不到這個視窗的話,請至上方工具列裡選擇”Tools”->”Library”。

image

接下來我們會從元件庫裡拿幾個元件出來用:

  • Label 是拿來放一些靜態文字用的,提示使用者某個欄位要輸入什麼值,在這裡也可以拿來做為BMI結果的輸出值。
  • Text Field 是用來讓使用者輸入身高體重的地方。
  • Button 則是按下去之後會幫我們計算BMI,並呈現在畫面上。

接下來請把畫面佈置得如下圖:

image

label或是button上的文字,只要點兩下就可以進行編輯。在畫面中間下面,你看到的那個Label字樣,是待會計算完成會出現的結果。但在按下計算之前,它應該是隱藏的,所以點選Label之後,可以從Attributes Inspector把它設定為”hidden”(如下圖)。這樣的話到時候它出場的時候就會看不見了(但在設計階段會看到半透明的)。

image

在attributes inspector這邊還有很多屬性是可以自己調整看看的,例如字型、顏色、大小等。

完成之後記得存檔,然後回到xcode,再度按下”Build and Run”,你就可以看到你剛剛做的成果,按鈕可以按了,文字可以輸入了,但應該都還沒功能,而且那個鍵盤會一直卡在那邊,不會自己退下去,接下來我們就要來把這個功能給完成。

回到Xcode,點開左邊Classes裡的BMIViewController.h(同上,如果專案名稱不同,檔名也會不同),你會看到一個空的類別定義,這裡暫時不詳細說明什麼是類別,先讓大家取得成就感再來研究細節 :)

接下來我們要在@interface裡加幾個屬性跟方法,程式碼如下:

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

@interface BMIViewController : UIViewController
{
  IBOutlet UITextField *body_height;
  IBOutlet UITextField *body_weight;
  IBOutlet UILabel *result;
}

@property (nonatomic, retain) IBOutlet UITextField *body_height;
@property (nonatomic, retain) IBOutlet UITextField *body_weight;
@property (nonatomic, retain) IBOutlet UILabel *result;

-(IBAction) calcBMI: (id) sender;
-(IBAction) keyboardDismiss: (id) sender;
@end

這裡先暫時不說明什麼是IBOutlet、IBAction還有一堆看不懂的語法是要幹嘛的,有機會的話,會在之後的文章裡再來一一說明。簡單的說,這些IBOutlet就是一些待會要給Interface Builder做連結用的,IBAction則是一些”動作”,待會要設定給按鈕按下去之後用的。這裡的IB,指的就是Interface Builder

定義好了interface之後,接下來請切換到BMIViewController.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 檔案:BMIViewController.m
#import "BMIViewController.h"

@implementation BMIViewController

@synthesize body_height;
@synthesize body_weight;
@synthesize result;

-(IBAction) calcBMI: (id) sender
{
  // 取得身高數值,並換算成公尺
  float height = [[body_height text] floatValue] / 100;

  // 取得體重數值
  float weight = [[body_weight text] floatValue];

  // 顯示計算結果
  result.hidden = NO;
  result.text = [NSString stringWithFormat:@"您的BMI值是:%.2f", (weight / (height * height))];
}

// 把鍵盤退下去
-(IBAction) keyboardDismiss: (id) sender
{
  [sender resignFirstResponder];
}

-(void) viewDidLoad
{
  // 設定這兩個欄位的初始為數字及標點符號鍵盤
  [body_height setKeyboardType:UIKeyboardTypeNumbersAndPunctuation];
  [body_weight setKeyboardType:UIKeyboardTypeNumbersAndPunctuation];
}

- (void)dealloc
{
  [body_height release];
  [body_weight release];
  [result release];
  [super dealloc];
}
@end

特別要說明的是,calcBMI方法做的事,就是去取得畫面上的兩個TextFeid的值,計算之後把結果輸出到剛剛那個被我們隱藏起來的Label;keyboardDismiss方法則是要把鍵盤給退下去,而viewDidLoad則是當整個app執行並載入元件完成後會做的事,這裡我們設定那兩個TextField的keyboard屬性為數字+標點符號鍵盤(身高體重應該只要輸入數字跟小數點)。

到這裡,程式碼的部份其實已經算寫完了,但是還沒”串起來”,所以還不會動。接下來我們就是要來做”串起來”的動作。

回到Interface Builder,找到File’s Owner,在上面點滑鼠右鍵應該會出現像下面這個畫面:

image

你剛剛寫的程式碼如果沒錯的話,IBOutlet跟IBAction就會在這裡出現了。接下來就是開始歡樂的連連看了。上圖的IBOutlet跟IBAction旁邊都有一個小圈圈,按下body_height屬性右邊的小圈圈,然後拖拉到你剛佈置的那個身體的TextField,如下圖:

image

body_weight也是以此類推,再來把calcBMI這個IBAction拖拉到計算的按鈕上,它會出現一個選單,選擇”Touch Down”,表示這個IBAction會在這個按鈕被按下去的時候執行:

image

同樣的把result也拖拉到那個隱藏的label上:

image

最後比較特別的是keyboardDismiss這個,請把它各別拖拉到身高及體重的TextField,並選擇”Did End On Exit”,這表示當在這兩個TextField按下enter或return之後會做的事,在這裡是把鍵盤給退下去:

image

如果全部都完成的話,畫面應該看起來會像這樣:

image

以上這些”串連”的動作,就是把畫面上的東西跟程式碼串接起來,讓按鈕按下去會有反應,該出現的出現,該隱藏的隱藏。除了從File’s Owner這邊可以拉之外,也可以從另外的Connection視窗來看,例如你點一下那顆計算的按鈕,並切換到Connection視窗(如下圖),如果看到這顆按鈕目前有跟什麼屬性或動作有連結。在這裡可以看到這個按鈕的Touch Down事件會去觸發calcBMI動作。

image

存檔,並回到Xcode環境,按下Build and Run,程式沒錯的話,應該就可以看到你的app啟動了。

image

最後,當你按下模擬器裡的home按鈕,你看到你的app的icon是白白的(如下圖):

image

這樣很醜,應該不會有人想買你的app,所以通常都會設計個漂亮的icon來放上去。製作方法也很容易,icon的尺寸是57×57,開影像處理軟體做好之後,回到Xcode,把做好的圖檔拖拉到Xcode裡的Resources資料夾。它會跳一個視窗出來:

image

勾選Copy選項,它就會把你剛做的icon圖檔也複製一份到你的專案裡。接著同樣在Resources資料夾裡有個BMI-info.plist檔案(檔名同專案名稱,所以你可能跟我的不一定會一樣),打開之後中間有個icon file的選項,填入你剛剛做好的icon檔名(我的是eddie_icon.png)。

image

存檔,Build and Run,應該就能看到新的icon了:

image

差不多就這樣了。不過其實還有很多防呆裝置沒做,例如欄位沒填,或是數字除以零..等等,還有畫面也都還沒美化過,所以如果你要拿上架去賣錢的話,記得把這些功能補齊。

好啦,這樣你可以準備上架去狠狠賺它個幾百萬了,有賺錢的話記得分紅啊 :)

原始檔下載

補充:

練習久了,你應該會發現在你的模擬器裡塞了一堆的app,如果想要清除的話,請到/Users/個人帳號/Library/Application Support/iPhone Simulator這個資料夾,把裡面的檔案及資料夾砍掉就行了。