2013年4月29日 星期一

[Xcode] 初學者筆記(四)

這篇以紀錄Table View和Cell View為主的一些心得。



1. Cell View的實現

這邊介紹比較簡單的方式來實作Cell View。從Storyboard新增一個Navigation Controller,可以看到實際上新增的是兩個Controller:Navigation Controller以及Table View Controller。



編譯後可以看到一個空的Table View。在Xcode幫你創建的Table View Controller的.m檔(請參考iOS App教學書籍,新增UITableViewController類別的.h和.m檔案並連結到Sotryboard的Table View)裡面有兩個很重要的函式:numberOfSectionsInTableView以及numberOfRowsInSection,一般iOS App教學書籍一定會提到這兩個函式,他們的return值是用來告知iOS Table要產生多少個Section與多少個Row。下面列了4張圖分別是:

I. 3個Section搭配0個Row

II. 0個Section搭配3個Row

III. 1個Section搭配3個Row

IV. 2個Section分別搭配1個和2個Row


實驗中可以發現Table的Section與Row其實有些潛規則(很多書上都沒講):
  • Row可以為0,但是Section必須大於0,否則看不到Section和Row
  • 一定要實作titleForHeaderInSection()才能看到Section,而且必須return非nil以及非@""內容的字串
  • 一定要實作cellForRowAtIndexPath(),否則執行時一定會crash
  • 一定要在Storyboard裡面Cell的ID設定成與.m底下cellForRowAtIndexPath()裡面的ID一致,否則編譯可以通過但執行時會crash

這裡放上以“2個Section分別搭配1個和2個Row”為例子的程式:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 2;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    if (section == 0)
        return 1;
    else if (section == 1)
        return 2;
    return 3;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    return [NSString stringWithFormat:@"%d", section+1];
}
    
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    cell.textLabel.text = [NSString stringWithFormat:@"%d", indexPath.row];
    
    return cell;
}

注意上面的Cell ID(CellIdentifier)預設取名為"Cell",因此在Storyboard裡面的Cell也要取相同名稱。


2. 動態加入Cell View的方法

為了能紀錄每個Cell對應的內容並動態增減Cell,必須先建立一個矩陣來存放資料。矩陣的使用方式與Table很相近。建立Table View Controller對應的.h和.m(這裡取名TestTable作為例子)之後,在.m宣告一個NSMutableArray。

#import "TestTable.h"

@interface TestTable ()
{
    NSMutableArray *dataArray;
}
@end

接著填入本篇第一個教學「Cell View的實現」裡面提到的幾個重要函式的內容與回傳值。首先,必須得配置記憶體給剛宣告的dataArray,因此在一開始就會被執行的viewDidLoad()底下,必須配置記憶體給dataArray並初始化。接著numberOfSectionsInTableView()一定要大於零才能顯示Cell,所以即使不需要section也還是得回傳1。numberOfRowsInSection()則是為了正確顯示矩陣的內容,所以將矩陣的內容數量當作Cell數量的回傳值。cellForRowAtIndexPath()是用來產生Cell實體的函式,一般會在這底下設定Cell顯示的內容。

- (void)viewDidLoad
{
    [super viewDidLoad];

    dataArray = [[NSMutableArray alloc] init];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    return [dataArray count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    
    // Configure the cell...
    NSString *text = [dataArray objectAtIndex:indexPath.row];
    cell.textLabel.text = [NSString stringWithFormat:@"%@", text];
    return cell;
}

再來是建立一個Button用的event,按下按鈕即可執行增加Cell的動作。首先得在.h檔底下幫TableView取名字,因為在程式中得對它做控制。然後將此名稱連結到Table View。

#import <UIKit/UIKit.h>

@interface TestTable : UITableViewController

@property (weak, nonatomic) IBOutlet UITableView *TestTableView;

@end


接著宣告一個可以讓Storyboard認得到的函式,然後在Storyboard的Navigation Bar上新增一個按鈕並將此按鈕的Sent Actions指到此函式。需注意的是如果少了呼叫reloadData,則按下按鈕只會增加矩陣dataArray的內容,但畫面上的Cell數量並不會改變。

- (IBAction)addCell:(id)sender
{
    [dataArray addObject:[NSString stringWithFormat:@"%d", [dataArray count]+1]];
    [self.TestTableView reloadData];
}




可參考Youtube教學影片:http://www.youtube.com/watch?v=YEaxIVv-EPI



3. 點擊Cell怎麼連結到另一個顯示詳細內容的View

讓Cell全部轉場至同一個View的方法很簡單,只要在Storyboard上面直接將Cell拉藍線到創建的詳細內容View即可,不論新增多少個Cell,點擊都可以轉場至所連線的頁面。



4. 怎麼區分是點擊哪個Cell並且顯示對應內容?

本篇第3點教學是不管點擊哪個Cell都會轉場至同一個View。一般開發需求是希望這同一個View會因為來自不同的Cell而能顯示不同的內容。這裡有兩種方法可以實現:
  1. 使用Xcode預設函式didSelectRowAtIndexPath() + viewDidAppear()
  2. 或是不照第3點教學建議的方式,改成不透過Storyboard建立View,而是當函式didSelectRowAtIndexPath()被呼叫時才動態建立View
這裡可以看出didSelectRowAtIndexPath()是個重要的函式,當View A的Cell被點擊並轉場至View B時,便會觸發此函式。所以開發者直覺做法是將變數的設定行為寫在View A的didSelectRowAtIndexPath(),然後把變數extern給View B使用,並且預期設定值能在View B最開始的viewDidLoad()裡面被讀取以載入對應的畫面。可是真正執行時,開發者會發現View B的變數並未遵照didSelectRowAtIndexPath()裡面設定,這是因為函式被執行的順序不如預期所導致。實際上點擊Cell觸發轉場的過程中,各函式被執行的順序如下:
  1. 使用者點擊
  2. 轉場過程中,執行View A的函式prepareForSegue()
  3. 轉場的View B執行viewDidLoad()
  4. 執行View A的didSelectRowAtIndexPath()
  5. 執行View B的viewDidAppear()
就因為viewDidLoad()出現的比didSelectRowAtIndexPath()來得早,因此viewDidLoad()無從得知是View A的哪個Cell進入View B的。這問題可以藉由將讀取變數的地方移到viewDidAppear()底下,這時didSelectRowAtIndexPath()已經執行過,因此新的設定值就會帶到View B的viewDidAppear()。此方法的缺點是viewDidAppear()與viewDidLoad()之間有個大約50ms時間的間隔,因此使用者在點擊Cell之後會感覺到View B閃爍一下才顯示出正確畫面。

另個不遵照教學3的方式則是不透過Segue來換場至View B。此方式不會有閃爍的缺點,但必須在Storyboard之外的地方另行建立一個長得像Storyboard的xib檔案。xib只會實作自己的View,接著就要以程式的方式來開啓此View。


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Navigation logic may go here. Create and push another view controller.
    aaa = [dataArray objectAtIndex:indexPath.row];
    MjToolDetailView *detailTableViewController = [[MjToolDetailView alloc] initWithNibName:@"MjToolDetailView" bundle:nil];

    [self.navigationController pushViewController:detailTableViewController animated:YES];
}

這種方式是等didSelectRowAtIndexPath()被執行後才去建立新的View,因此可以輕鬆解決順序問題。這裡的aaa就是一個extern變數,MjToolDetailView則是額外建立有著.h .m和.xib,取得此Cell對應的aaa之後再將aaa帶入新建立的MjToolDetailView。


5. 使用刪除鍵移除Cell View的方法

一旦可以動態增加Cell後,接下來就會需要刪除Cell。
  • 要如何在Navigation Bar上面加一個刪除按鈕
  • 這個按鈕按下去要能出現讓使用者刪除Cell的功能
前面提到在Storyboard加入Navigation Controller便可以快速實作Table與Cell功能,貼心的Apple還讓開發者能夠使用Table View Controller簡易新增可以用來刪除Cell的按鈕。方法是在Table View Controller對應的.m檔下的ViewDidLoad()設定按鈕:

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.editButtonItem.title = @"編輯";
    self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
由此可以看出,editButtonItem以及navigationItem是個預設就存在的物件,也就是說它們本來就存在Navigation Controller裡面,所以我們要做的事情就是讓editButtonItem成為navigationItem右邊的一個按鈕。


只要Cell裡面有東西,則點選"編輯"按鈕之後可看到Cell左邊出現"減號"。按下"減號",Cell右邊會出現"Delete",然而此鍵在實作之前是沒有功用的。附帶一點,不點選"編輯"而是用手指由左往右滑動一樣可以看到"Delete"鍵。

接著來想辦法讓Delete鍵生效。Xcode幫你創建Table View Controller的.m檔裡面有個commitEitingStyle()函式,如下,預設此函式是被註解起來的,拿掉註解符號就可以使用此函式。
/*
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete the row from the data source
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
    }   
    else if (editingStyle == UITableViewCellEditingStyleInsert) {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    }   
}
*/
重點來了,這個動作僅能刪除UI上的Cell,但對應後面的資料庫內容通常也需要跟著被刪除。因此必須在editingStyle == UITableViewCellEditingStyleDelete判斷式裡面加入從資料庫或是先前宣告的矩陣中刪除一筆資料。所以程式碼會長得像下面:

// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete the row from the data source
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        [dataArray removeObjectAtIndex:indexPath.row];
        [self sqlDelData];
    }   
    else if (editingStyle == UITableViewCellEditingStyleInsert) {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    }   
}
使用removeObjectAtIndext刪除dataArray的一筆資料,另外存放在資料庫的資料就得實作另個函式來方便處理,這部分可以參考下面介紹資料庫函式FMDB的連結:



6. 更改Table View上方Navigator Bar的Button文字的方法

前面有介紹將editButtonItem設定成Navigator Bar右邊按鈕或是左邊按鈕的方法,

        self.navigationItem.rightBarButtonItem = self.editButtonItem;
        self.navigationItem.leftBarButtonItem = nil;
只要更改editButtonItem的內容就可以更改Navigator Bar上按鈕的文字:
        self.editButtonItem.title = @"編輯";

可是如果只在viewDidLoad()設定文字,會發現一旦點選按鈕後,按鈕的字就會回復成原本的英文字。所以還得把這設定加到按鈕觸發的函式裡面,那就是setEditing()。

- (void)setEditing:(BOOL)editing animated:(BOOL)animate
{
    [super setEditing:editing animated:animate];
    if(editing)
    {
        self.editButtonItem.title = @"完成";
    }
    else
    {
        self.editButtonItem.title = @"編輯";
    }
}

沒有留言:

張貼留言