使用Xib來Layout

最近在複習以前沒注意到的東西, 順便也對AutoLayout做了一點功課, 記錄一下如何使用Xib來快速做一些Layout的動作.

(謎之音:這一段可以直接跳過, 不想看就直接往下吧)

對於大多數的iOS開發者應該都有使用過Interface Builder, 可能也習慣使用IB來做開發; 就個人而言, 其實我更習慣用程式碼來做layout的動作, 即使到了iOS5推出了Storyboard也沒有改變我的習慣; 最近在追iOS6的內容(相容要到5.0, you know),看到AutoLayout覺得這真的是一個很棒的功能, 方便、快速, 只是可能太多物件會有點亂(在Xib中), 但是使用Xib來製作就還沒上手的我來說是一個比較容易上手的方式, 順便也嘗試改變一下只用程式來做Layout, 試試看是否會比較快或者是兩個互相搭配有更好的開發方式.

這次會記錄的內容會有

  • 使用Xib來定義一個UIView’s Subclass.
  • 在ViewController中, 不同的View使用不同的Xib載入內容.
  • 使用Xib來定義UITableViewCell, 並直接讓UITableView使用.

內容大多數會差不多, 不過有幾個比較特別的地方會特別點出來

建立UIView subclass with Xib

這邊會有下面幾個步驟

  • 建立並宣告一個UIView subclass.
  • 為UIView subclass建立Xib並作連結.
  • 建立UIView class實體的時候載入Xib內容.

Step. 1-1

首先我們先建立一個Class MyHeader

建立UIView subclass named MyHeader
1
2
3
4
5
@interface MyHeader : UIView
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UIButton *clickButton;
- (IBAction)touchUpButton:(id)sender;
@end

上面的程式碼, 我在.h宣告了兩個property跟一個method, 前面加上IBOutletIBAction, 這兩個修飾字(是讓Class跟Xib溝通用的), 接下來新增Xib檔案讓MyHeader使用.

Step. 1-2

在Menu的File->New->File, 然後選擇iOS下的User Interface->Empty, 建立檔案並命名為MyHeader.(參考下圖)

接著開啟MyHeader.xib, 並增加一個UIView到畫面上, 同時將這個View的class設定為MyHeader, 在MyHeader.h中我有定義兩個視覺物件分別是一個Label跟一個Button, 所以在剛剛Xib的View我們接著建立Button跟Label, 完成後大該會跟下圖一樣. (這邊跟我們再使用UIViewController, 將 File’s Owner的Class設定成我們的ViewController Class有點不同, 需要注意一下)

接著我們將MyHeader.hMyHeader.xib需要連接的property跟method連起來

這邊可以發現因為View是MyHeader Class, 所以在Connections Inspector上會有我們再Class中定義可以跟Xib連結的項目.

到這邊我們已經完成了Xib跟Class的基本設定, 接下來要回到程式的部份, 在建立MyHeader的時候讓MyHeader去載入Xib的內容.

Step. 1-3

MyHeader.m中實作init method, 在init中我們可以使用兩種方式來載入Xib內容, 不過效果都是相同的.

implement MyHeader’s init method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (id)init {
    UINib *nib = [UINib nibWithNibName:@"MyHeader" bundle:nil];
    NSArray* array;
    array = [nib instantiateWithOwner:nil
                              options:nil];

    self = (MyHeader*)[array lastObject];
    if(self) {
        /**
         * 需要修改 AutoresizingMask, 不然可能因為大小的關係跑掉.
         */
        self.autoresizingMask = UIViewAutoresizingNone;
        // your statement
    }
    return self;
}

在這邊, 先介紹第一種方法使用UINib來載入Xib的內容, 載入完成後在將Xib的資料實體化, 這邊會回傳一個NSArray包含著Xib所有的內容(top-level objects).

UINib只能使用Class Method來建立實體

在Line4跟5的地方, [nib instantiateWithOwner:nil options:nil] 帶入的Owner跟Options都是nil, 因為我們再Xib中並沒有對File's Owner做Class的設定, 所以並不需要帶入(下一個內容會帶入) 不過即使這邊帶入也沒關係並沒有特別影響, 至於options的部份因為我自己沒有用到, 不過有查到一個相關的內容, 在最後附上給大家參考.

另外, 關於回傳的Array, 因為xib裡面只有一個view, 所以才能用`lastObject`這個method去取得, 如果Xib裡面有多個獨立的view(可以參考第二個內容的圖), 就需要先判斷載入的view的class, 在去做設定.

最後把MyHeader加到RootViewController的View中, 呈現的畫面參考下圖

上面有提到說, 載入Xib的方式有兩種, 一種是使用UINib, 另一個方式是使用NSBundle, NSBundle有為載入Xib提供一個Category, 那麼我們修改一下init的內容在嘗試一次.

use NSBundle loading Xib
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (id)init {
    NSArray* array;

//    UINib *nib = [UINib nibWithNibName:@"MyHeader" bundle:nil];
//    array = [nib instantiateWithOwner:nil
//                              options:nil];

    array = [[NSBundle mainBundle] loadNibNamed:@"MyHeader"
                                          owner:nil
                                        options:nil];

    self = (MyHeader*)[array lastObject];
    if(self) {
        /**
         * 需要修改 AutoresizingMask, 不然可能因為大小的關係跑掉.
         */
        self.autoresizingMask = UIViewAutoresizingNone;
    }
    return self;
}

修改過後, 呈現的內容將跟使用UINib一模一樣.

Q&A

Q: 如果再一個ViewController.xib中放入一個MyHeaderView, 這個MyHeader有辦法載入嗎?
A: 我現在嘗試還沒成功, 如果有成功的話在額外補充囉.

在ViewController中, 不同的View使用不同的Xib載入內容.

這邊先稍微解釋一下這個內容跟第一個內容最後的Q&A的差異, 這邊的View不會是一個UIView的Subclass, 只是讓這個View的載入從另一個Xib去帶入, 跟一般ViewController.xib可能包含不一只一個View(參考下圖), 其中我所選取的是ViewController’s view(灰底), 旁邊有另一個view, 現在這個內容就是把白色的View抽出來到另一個Xib裡面.

接下來我們要建立一個ViewController, 這個ViewController有一個UITableView跟UIView, 其中UIView是要用另個一Xib(不是ViewController所使用的Xib)來載入這個View.

這邊的一些動作跟上面有些類似會省略一些內容, 大致要做的事情有下列

  • 建立UIViewController, 讓ViewController有tableViewtableViewHeader
  • 為tableViewHeader建立Xib, 並完成連結

Step. 2-1

建立MyViewController
1
2
3
4
5
6

@interface MyViewController : UIViewController
<UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, weak) IBOutlet UITableView* tableView;
@property (nonatomic, strong) IBOutlet UIView *header;
@end

接著打開MyViewController.xib, 並在View加入一個TableView, 並且與Class建立連結, 參考下圖

Step. 2-2

接下來我們直接建立ViewController’s header要使用的xib並命名為MyVCHeader, 這邊比較不同的地方是, 這次的File’s Owner地方, 需要到Identity Inspector將Custom Class更改為MyViewController, 這樣一來我們才有辦法在MyVCHeader.xib存取到MyViewController的IBOutlet參數; 完成後, 在xib建立一個view, 並在view加入幾個subview. 最後再把view跟File’s Owner的header做連結.(參考下圖)

接下來在MyViewController.mviewDidLoad中, 我們來實作使header從Xib載入, 並將header設定為tableView’s tableHeaderView.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.

    UINib* nib = [UINib nibWithNibName:@"MyVCHeader"
                                bundle:nil];


    [nib instantiateWithOwner:self options:nil];


    self.tableView.tableHeaderView = self.header;
}

在執行之後的內容就是上圖; 在這邊你可能注意到, 這次並沒有運用回傳的Array來進行assign的動作, 而是在建立實體的時候, 將owner傳給帶入method中, 就會自己去做完成最後的connect動作, 這部份其實跟UIViewController在loadView之後的在去載入Xib(最後會到viewDidLoad)是一樣的動作.

ps. 這邊也可以使用NSBundle來載入內容

使用Xib來定義UITableViewCell, 並直接讓UITableView使用.

這個內容會延續第二個內容製作的項目, 同時運用xib來建立Cell; 接下來會有兩個動作要去完成

  • 建立一個UITableViewCell Subclass然後跟Xib做連結(命名為MyCell)
  • 用UITableView註冊步驟一建立的UITableViewCell Subclass

Step. 3-1

這個動作跟第一個內容很像, 不過在.m中不需要特別使用UINib(NSBundle)來實作載入xib的動作; 在這邊就不多做說明, 可以參考下面在去實作xib就可以.

MyCell.h
1
2
3
4
5

@interface MyCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UILabel *indexLabel;
@end

Step. 3-2

接下來在viewDidLoad讓TableView註冊MyCell, 這邊註冊的時候會需要帶入一個identifier, 這個identifier在之後是取得Cell的內容.

對TableView註冊MyCell
1
2
3

UINib* myCellNib = [UINib nibWithNibName:@"MyCell" bundle:nil];
[self.tableView registerNib:myCellNib forCellReuseIdentifier:myCellIdentifier];

接著再實作UITableViewDataSource取得Cell的mehtod

1
2
3
4
5
6
7
8
9

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MyCell* cell = [tableView dequeueReusableCellWithIdentifier:myCellIdentifier];

    cell.titleLabel.text = [NSString stringWithFormat:@"t-%d", indexPath.row];
    cell.indexLabel.text = [NSString stringWithFormat:@"%d", indexPath.row];

    return cell;
}

上面的程式碼你會注意到, 你不需要在去檢查cell是不是存在的動作, tableview會自己去做reuse跟create新的instance的動作, 這樣就可以直接使用MyCell. 執行的結果參考下圖.

最後

關於用code藍Layout比較快還是用Xib會比較快速, 個人覺得可以是情況而定, 比較複雜的ViewController如果有較多的View需要去做切換, 可以使用xib+code的混合方式, 減少過多的程式碼, 也避免一個xib有太多的view而不好管理, 不過檔案可能會比較多, 就依照個人取捨來選擇作法吧. 如果有任何錯誤, 在麻煩大家幫忙解答一下, 謝謝.

References & Others

最後一個Link有提到關於載入Xib要帶入options的部份, 如果有興趣可以特別閱讀一下

Resource Programming Guide
NSBundle UIKit Additions Reference
UINib Class Reference How to use a xib and a UIView subclass together?
How to use a common target object to handle actions/outlets of multiple views?

Comments