iOS中的block全面分析

代理設計模式對於iOS開發的人來說肯定很熟悉了,代理delegate就是委託另一個對象來幫忙完成一件事情,爲什麼要委託別人來做呢,這其實是MVC設計模式中的模塊分工問題,例如View對象它只負責顯示界面,而不需要進行數據的管理,數據的管理和邏輯是Controller的責任,所以此時View就應該將這個功能委託給Controller去實現,當然你作爲碼農強行讓View處理數據邏輯的任務,也不是不行,只是這就違背了MVC設計模式,項目小還好,隨着功能的擴展,我們就會發現越寫越難寫;還有一種情況,就是這件事情做不到,只能委託給其他對象來做了,下面的例子中我會說明這種情況。

下面的代碼我想實現一個簡單的功能,場景描述如下:TableView上面有多個CustomTableViewCell,cell上面顯示的是文字信息和一個詳情Button,點擊button以後push到一個新的頁面。爲什麼說這個場景用到了代理delegate?因爲button是在自定義的CustomTableViewCell上面,而cell沒有能力實現push的功能,因爲push到新頁面的代碼是這樣的,

[self.navigationController pushViewController...];

所以這時候CustomTableViewCell就要委託它所在的Controller去做這件事情了。

按照我的編碼習慣,我喜歡把委託的協議寫在提出委託申請的類的頭文件裏面,現在的場景中是CustomTableViewCell提出了委託申請,下面是簡單的代碼,

@protocol CustomCellDelegate <NSObject>

- (void)pushToNewPage;

@end


@interface CustomTableViewCell : UITableViewCell

@property(nonatomicassign) id<CustomCellDelegatedelegate;

@property (nonatomicstrongUILabel *text1Label;

@property(nonatomic,strong) UIButton *detailBtn;

@end

上面的代碼在CustomTableViewCell.h中定義了一個協議CustomCellDelegate,它有一個需要實現的pushToNewPage方法,然後還要寫一個屬性修飾符爲assign、名爲delegate的property,之所以使用assign是因爲這涉及到內存管理的東西,以後的博客中我會專門說明原因。

接下來在CustomTableViewCell.m中編寫Button點擊代碼,

[self.detailBtn addTarget:self action:@selector(btnClicked:) forControlEvents:UIControlEventTouchUpInside];

對應的btnClicked方法如下,

- (void)btnClicked:(UIButton *)btn

{

    if (self.delegate && [self.delegaterespondsToSelector:@selector(pushToNewPage)]) {

        [self.delegate pushToNewPage];

    }

}

上面代碼中的判斷條件最好是寫上,因爲這是判斷self.delegate是否爲空,以及實現CustomCellDelegate協議的Controller是否也實現了其中的pushToNewPage方法。

接下來就是受到委託申請的類,這裏是對應CustomTableViewCell所在的ViewController,它首先要實現CustomCellDelegate協議,然後要實現其中的pushToNewPage方法,還有一點不能忘記的就是要設置CustomTableViewCell對象cell的delegate等於self,很多情況下可能忘了寫cell.delegate = self;導致遇到問題不知雲裏霧裏。下面的關鍵代碼都是在ViewController.m中,

首先是服從CumtomCellDelegate協議,這個大家肯定都知道,就像很多系統的協議,例如UIAlertViewDelegate、UITextFieldDelegate、UITableViewDelegate、UITableViewDatasource一樣。

@interface ViewController ()<CustomCellDelegate>

@property (nonatomicstrong) NSArray *textArray;


@end

然後是實現CustomCellDelegate協議中的pushToNewPage方法,

- (void)pushToNewPage

{

    DetailViewController*detailVC = [[DetailViewController alloc] init];

    [self.navigationController pushViewController:detailVC animated:YES];

}

還有一個步驟最容易被忘記,就是設置CumtomTableViewCell對象cell的delegate,如下代碼,

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

    static NSString *simpleIdentify = @"CustomCellIdentify";

    CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleIdentify];

    if (cell == nil) {

        cell = [[CustomTableViewCell allocinitWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleIdentify];

    }

    //下面代碼很關鍵

    cell.delegate = self;

    cell.text1Label.text = [self.textArray objectAtIndex:indexPath.row];

    return cell;

}

通過cell.delegate = self;確保了CustomTableViewCell.m的判斷語句if(self.delegate && ...){}中得self.delegate不爲空,此時的self.delegate其實就是ViewController,cell對象委託了ViewController實現pushToNewPage方法。這個簡單的場景描述了使用代理的一種情況,就是CustomTableViewCell沒有能力實現pushViewController的功能,所以委託ViewController來實現。

代碼在github可以下載。

有什麼錯誤,還請大家指正。



----------------------------------------------下面是block的內容----------------------------------------------------

Block是一個C語言的特性,就像羣裏有人說的,它就是C語言的函數指針,在使用中最多的就是進行函數回調或者事件傳遞,比如發送數據到服務器,等待服務器反饋是成功還是失敗,此時block就派上用場了,這個功能的實現也可用使用代理,這麼說的話,感覺block是不是有點像代理了呢?

我之前接觸block,都是使用它作爲函數參數,當時感覺不是很理解。現在在項目中,很多時候block作爲property,這樣更加簡單直接,想想,其實property不就是定義的合成存儲的變量嘛,而block作爲函數參數也是定義的變量,所以作爲函數參數或者作爲property本質沒有區別。

看一看別人總結的block的語法吧,http://fuckingblocksyntax.com,這個鏈接亮了,fucking block syntax,操蛋的block語法啊。block有如下幾種使用情況,

1、作爲一個本地變量(local variable)

returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};

2、作爲@property

@property (nonatomic, copy) returnType (^blockName)(parameterTypes);

3、作爲方法的參數(method parameter)

- (void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName;

4、作爲方法參數的時候被調用

[someObject someMethodThatTakesABlock: ^returnType (parameters) {...}];

5、使用typedef來定義block,可以事半功倍

typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};

上面我也只是複製粘貼了一下,接下來還是實現點擊CustomTableViewCell上面的Button實現頁面跳轉的功能,我之前不止一次的類比block就像delegate,這邊我也是思維慣性,下面的內容我就當block爲代理,一些用詞描述還是跟delegate差不多。首先,在提出委託申請的CustomTableViewCell中定義block的property,

@interface CustomTableViewCell : UITableViewCell

@property (nonatomicstrongUILabel *text1Label;

@property (nonatomicstrongUIButton *detailBtn;


//下面的定義,請看官們對比一下

/*delegate的定義 我沒有刪除,因爲大家可以類比了看下*/

@property (nonatomicassignid<CustomCellDelegate> delegate;

/*這裏定義了ButtonBlock*/

@property (nonatomiccopyvoid (^ButtonBlock)();

@end

這裏用copy屬性來修飾ButtonBlock property,這個原因,我會在以後的博客中作專門的解釋。

接下來在CustomTableViewCell中給它上面的detailBtn綁定點擊方法,

[self.detailBtn addTarget:self action:@selector(btnClicked:) forControlEvents:UIControlEventTouchUpInside];

然後是btnClicked方法的細節,我把delegate的內容也沒有刪除,就是給各位比較一下block和delegate的功能和語法的相似性,

- (void)btnClicked:(UIButton *)btn

{

    //這是之前的delegate

    if (self.delegate && [self.delegate respondsToSelector:@selector(pushToNewPage)]) {

        [self.delegate pushToNewPage];

    }

    

    //這是現在我們要說的block    

    if (ButtonBlock) {

        ButtonBlock();

    }

}

下面是一個關鍵性的地方,在ViewController2中設置其CustomTableViewCell的cell對象的ButtonBlock,也就是給它賦值,此處我還是保留了cell.delegate = self;代碼,

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

    NSString *blockIdentify = @"BlockIdentify";

    CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:blockIdentify];

    if (cell == nil) {

        cell = [[CustomTableViewCell allocinitWithStyle:UITableViewCellStyleDefault reuseIdentifier:blockIdentify];

    }

    cell.text1Label.text = [self.textArray objectAtIndex:indexPath.row];

    //delegate的不可缺少的代碼,這裏放在這兒只是爲了給各位類比一下

    cell.delegate = self;

    

    //ButtonBlock不可缺少的代碼

    cell.ButtonBlock = ^{

        [self pushToNewPage2];

    };

    return cell;

}

之所以cell.ButtonBlock = ^{};賦值,是因爲我們我們是這樣定義ButtonBlock的,void (^ButtonBLock)(),表示無返回值無參數。

然後編寫pushToNewPage2方法,

- (void)pushToNewPage2

{

    DetailViewController *detailVC = [[DetailViewController allocinit];

    [self.navigationController pushViewController:detailVC animated:YES];

}

你們看這個方法是不是與CustomCellDelegate協議中的pushToNewPage方法類似。然後在回過頭來類比一樣,是不是block就是精簡版的delegate,因爲delegate設計模式要寫協議CustomCellDelegate,還有容易遺漏cell.delegate = self;但是block使用的時候就簡單多了。


如果有Block語法不懂的,可以參考fuckingblocksyntax,裏面對於Block

爲了方便對比,下面的代碼我假設是寫在ViewController子類中的

1、第一部分

定義和使用Block,

?
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
- (void)viewDidLoad
{
    [super viewDidLoad];
    //(1)定義無參無返回值的Block
    void (^printBlock)() = ^(){
        printf("no number");
    };
    printBlock();
     
 
    printBlock(9);
     
    int mutiplier = 7;
    //(3)定義名爲myBlock的代碼塊,返回值類型爲int
    int (^myBlock)(int) = ^(int num){
        return num*mutiplier;
    }
    //使用定義的myBlock
    int newMutiplier = myBlock(3);
    printf("newMutiplier is %d",myBlock(3));
}
//定義在-viewDidLoad方法外部
//(2)定義一個有參數,沒有返回值的Block
void (^printNumBlock)(int) = ^(int num){
    printf("int number is %d",num);
};

定義Block變量,就相當於定義了一個函數。但是區別也很明顯,因爲函數肯定是在-viewDidLoad方法外面定義,而Block變量定義在了viewDidLoad方法內部。當然,我們也可以把Block定義在-viewDidLoad方法外部,例如上面的代碼塊printNumBlock的定義,就在-viewDidLoad外面。

再來看看上面代碼運行的順序問題,以第(3)個myBlock距離來說,在定義的地方,並不會執行Block{}內部的代碼,而在myBlock(3)調用之後纔會執行其中的代碼,這跟函數的理解其實差不多,就是只要在調用Block(函數)的時候纔會執行Block體內(函數體內)的代碼。所以上面的簡單代碼示例,我可以作出如下的結論,

(1)在類中,定義一個Block變量,就像定義一個函數;

(2)Block可以定義在方法內部,也可以定義在方法外部;

(3)只有調用Block時候,纔會執行其{}體內的代碼;

(PS:關於第(2)條,定義在方法外部的Block,其實就是文件級別的全局變量)

那麼在類中定義一個Block,特別是在-viewDidLoad方法體內定義一個Block到底有什麼意義呢?我表示這時候只把它當做私有函數就可以了。我之前說過,Block其實就相當於代理,那麼這時候我該怎樣將其與代理類比以瞭解呢。這時候我可以這樣說:本類中的Block就相當於類自己服從某個協議,然後讓自己代理自己去做某個事情。很拗口吧?看看下面的代碼,

?
1
2
3
4
5
6
7
8
9
10
//定義一個協議
@protocol ViewControllerDelegate<NSObject>
- (void)selfDelegateMethod;
@end
 
//本類實現這個協議ViewControllerDelegate
@interface ViewController ()<ViewControllerDelegate>
@property (nonatomic, assign) id<ViewControllerDelegate> delegate;
 
@end

接着在-viewDidLoad中的代碼如下,

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    self.delegate = self;
    if (self.delegate && [self.delegate respondsToSelector:@selector(selfDelegateMethod)]) {
        [self.delegate selfDelegateMethod];
    }
}
 
#pragma mark - ViewControllerDelegate method
//實現協議中的方法
- (void)selfDelegateMethod
{
    NSLog(@"自己委託自己實現的方法");
}

看出這種寫法的奇葩地方了嗎?自己委託自己去實現某個方法,而不是委託別的類去實現某個方法。本類中定義的一個Block其實就是閒的蛋疼,委託自己去字做某件事情,實際的意義不大,所以你很少看見別人的代碼直接在類中定義Block然後使用的,Block很多的用處是跨越兩個類來使用的,比如作爲property屬性或者作爲方法的參數,這樣就能跨越兩個類了。

2、第二部分

__block關鍵字的使用

在Block的{}體內,是不可以對外面的變量進行更改的,比如下面的語句,

?
1
2
3
4
5
6
7
8
9
10
- (void)viewDidLoad
{
    //將Block定義在方法內部
    int x = 100;
    void (^sumXAndYBlock)(int) = ^(int y){
    x = x+y;
    printf("new x value is %d",x);
    };
    sumXAndYBlock(50);
}

這段代碼有什麼問題呢,Xcode會提示x變量錯誤信息:Variable is not assigning (missing __block type),這時候給int x = 100;語句前面加上__block關鍵字即可,如下,

?
1
__block int x = 100;

這樣在Block的{}體內,就可以修改外部變量了。

3、第三部分:Block作爲property屬性實現頁面之間傳值

需求:在ViewController中,點擊Button,push到下一個頁面NextViewController,在NextViewController的輸入框TextField中輸入一串字符,返回的時候,在ViewController的Label上面顯示文字內容,

(1)第一種方法:首先看看通過“協議/代理”是怎麼實現兩個頁面之間傳值的吧,

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//NextViewController是push進入的第二個頁面
//NextViewController.h 文件
//定義一個協議,前一個頁面ViewController要服從該協議,並且實現協議中的方法
@protocol NextViewControllerDelegate <NSObject>
- (void)passTextValue:(NSString *)tfText;
@end
 
@interface NextViewController : UIViewController
@property (nonatomic, assign) id<NextViewControllerDelegate> delegate;
 
@end
 
//NextViewController.m 文件
//點擊Button返回前一個ViewController頁面
- (IBAction)popBtnClicked:(id)sender {
    if (self.delegate && [self.delegate respondsToSelector:@selector(passTextValue:)]) {
        //self.inputTF是該頁面中的TextField輸入框
        [self.delegate passTextValue:self.inputTF.text];
    }
    [self.navigationController popViewControllerAnimated:YES];
}

接下來我們在看看ViewController文件中的內容,

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//ViewController.m 文件
@interface ViewController ()<NextViewControllerDelegate>
@property (strong, nonatomic) IBOutlet UILabel *nextVCInfoLabel;
 
@end
//點擊Button進入下一個NextViewController頁面
- (IBAction)btnClicked:(id)sender
{
    NextViewController *nextVC = [[NextViewController alloc] initWithNibName:@"NextViewController" bundle:nil];
    nextVC.delegate = self;//設置代理
    [self.navigationController pushViewController:nextVC animated:YES];
}
 
//實現協議NextViewControllerDelegate中的方法
#pragma mark - NextViewControllerDelegate method
- (void)passTextValue:(NSString *)tfText
{
    //self.nextVCInfoLabel是顯示NextViewController傳遞過來的字符串Label對象
    self.nextVCInfoLabel.text = tfText;
}

這是通過“協議/代理”來實現的兩個頁面之間傳值的方式。

(2)第二種方法:使用Block作爲property,實現兩個頁面之間傳值,

先看看NextViewController文件中的內容,

?
1
2
3
4
5
6
7
8
9
10
11
12
//NextViewController.h 文件
@interface NextViewController : UIViewController
@property (nonatomic, copy) void (^NextViewControllerBlock)(NSString *tfText);
 
@end
//NextViewContorller.m 文件
- (IBAction)popBtnClicked:(id)sender {
    if (self.NextViewControllerBlock) {
        self.NextViewControllerBlock(self.inputTF.text);
    }
    [self.navigationController popViewControllerAnimated:YES];
}

再來看看ViewController文件中的內容,

?
1
2
3
4
5
6
7
8
9
10
11
12
13
- (IBAction)btnClicked:(id)sender
{
    NextViewController *nextVC = [[NextViewController alloc] initWithNibName:@"NextViewController" bundle:nil];
    nextVC.NextViewControllerBlock = ^(NSString *tfText){
        [self resetLabel:tfText];
    };
    [self.navigationController pushViewController:nextVC animated:YES];
}
#pragma mark - NextViewControllerBlock method
- (void)resetLabel:(NSString *)textStr
{
    self.nextVCInfoLabel.text = textStr;
}

好了就這麼多代碼,可以使用Block來實現兩個頁面之間傳值的目的,實際上就是取代了Delegate的功能。

另外,博客中的代碼Sample Code可以再Github下載,如果因爲Github被牆了,可以在終端使用git clone + 完整鏈接,即可克隆項目到本地。

Github中的代碼,可以開啓兩種調試模式,你需要在項目的配置文件BlockSamp-Prefix.pch註釋或者解註釋下面的代碼,

?
1
#define Debug_BlcokPassValueEnable

即可開啓兩種調試的方式,如果註釋了上面的語句就是使用Delegate進行調試;否則使用Block進行調試。


(1)block作爲本地變量(local variable)

returnType (^blockName)(parameterTypes) = ^returnType(parameters){...};

(2)block作爲類的成員屬性(@property)

@property (nonatomic, copy) returnType (^blockName)(parameters);

這時候可以類比delegate,實現代理功能。

(3)block作爲函數參數(method parameter)

- (void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName;

調用包括block參數的函數,

[someObject somethodThatTakesABlock:^returnType(parameters){...}];

(4)使用typedef定義block類型

typedef returnType (^TypeName)(parameterTypes);

TypeName blockName = ^returnType(parameters){...};

上面的內容翻譯自fuckblocksyntax,大家忘了block語法的時候可以反覆的看看。


Block的使用舉例,

(1)作爲本地變量

int (^Mutiply)(int,int) = ^(int num1,int num2){

    return num1*num2;

};

block可以訪問局部變量,但是不能修改,否則會編譯報錯,

int mutiplier = 7;

int (^myBlock)(int) = ^(int num){

    mutiplier++;//編譯報錯

    return num*mutiplier;

};

如果要在block內部修改局部變量,則需要使用__block來修飾該局部變量,如下,

__block int mutiplier = 7;

int (^myBlock)(int) = ^(int num){

    mutiplier++;//編譯不會報錯

    return num*mutiplier;

};

(2)作爲函數參數使用

作爲函數的參數,block某種意義上替代了回調函數或者delegate,當函數調用的時候,假設某個事件發生,這時候block裏面的內容就會運行。這樣有利於代碼的整合和閱讀,不需要到處去實現委託方法了。

Block作爲函數參數,到底該怎麼寫代碼呢?今天(2014.05.09)晚上突然想明白,只要堅定的把Block當做delegate,一切就很簡單了,我們先看看delegate作爲參數,是怎麼寫的吧,如下代碼,

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:@"點擊Cell" delegate:self cancelButtonTitle:@"確定" otherButtonTitles:nil, nil];

[alert show];

這裏是初始化一個alert並且讓alert彈出,讓我們看看初始化的-(void)initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:是怎麼定義的吧,如下代碼,

- (id)initWithTitle:(NSString *)title message:(NSString *)message delegate:(id /*<UIAlertViewDelegate>*/)delegate cancelButtonTitle:(NSString *)cancelButtonTitle otherButtonTitles:(NSString *)otherButtonTitles, ... NS_REQUIRES_NIL_TERMINATION;//Xcode中複製過來的

我特別把delegate參數用紅色註明,所以是不是有了一點怎樣去定義block作爲參數的靈感,下面我就寫完整的代碼,分享我自己的經驗吧,

首先,還是要說一個場景,自定義的cell上面有一個Button,點擊Button,調用在ViewController中的-(void)OperateLog:方法,

不多說了,上代碼,

ImageTableViewCell.h file

typedef void (^BtnBlock)();//定義一個block

@interface ImageTableViewCell:UITableViewCell

{

    BtnBlock _btnBlock;//定義一個block成員變量

}

//下面是兩個xib拖動的控件

@property (nonatomic, strong) IBOutlet UIButton *btn;

@property (nonatomic, strong) IBOutlet UIIageView *imgView;

- (void)configureCell:(UITableViewCellStyle)style Block:(BtnBlock)block reuseIdentifier:(NSString *)reuseIdentifier;

//點擊Button觸發的方法

- (IBAction)btnClicked:(id)sender;

@end

ImageTableViewCell.m file

@implementation ImageTableViewCell

//...省略無關代碼

- (void)configureCell:(UITableViewCellStyle)style Block:(BtnBlock)block reuseIdentifier:(NSString *)reuseIdentifier

{

    //...省略不相干代碼

    _btnBlock = block;

}

- (IBAction)btnClicked:(id)sender

{

    if(_btnBlock)

    {

        _btnBlock();

    }

}


ViewController.m file

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

    NSString *simpleIdentify = @"UITableViewCellIdentify";

    ImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleIdentify];

    if (!cell) {

        //將Custom.xib中的所有對象載入

        NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"ImageTableViewCell" owner:self options:nil];

        //第一個對象就是CustomCell了

        cell = [nib objectAtIndex:0];

        [cell configureCell:UITableViewCellStyleDefault Block:^{

            [self OperateLog];

        } reuseIdentifier:simpleIdentify];

    }

    return cell;

}

//button點擊,最終傳遞到OperateLog方法,相當於代理方法吧

- (void)OperateLog

{

    NSLog(@"點擊了cell上面的Button");

}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章