en

hi, it seems you are using microsoft internet explorer. it doesn't match web standard and causes problems browsing this site. please please please use mozilla firefox or google chrome instead. thank you!

zh

哦哦!您正在使用Internet Explorer 瀏覽器,它與我們的網頁標準並不相容,可能會導致畫面顯示不正常。
請改用 Mozilla Firefox 或者 Google Chrome 才能正常瀏覽本網站,謝謝!

1.02.2012

使用 Storyboard Segue 實作 UIViewController 的切換(下)

 

延續前一篇的文章使用 Storyboard Segue 實作 UIViewController 的切換(上),我們已經成功建立兩個可以互相切換的 UIViewController,一個是透過 Storyboard Segue 來切換,另一個則是使用 dismissViewControllerAnimated: 的方法來返回先前的 UIViewController,接下來就是要解決兩個 UIViewController 之間傳值的問題,這裡同樣提供兩個方法,在頁面 1 的部份同樣使用 Storyboard Segue 來幫助我們傳遞資訊給頁面 2,而在頁面 2 的部份,則是使用老方法,透過代理委托 delegate 的方式來傳送資訊。

首先我們在兩個 UIViewController 分別拉一個 UITextField 元件,並將此元件與 class 做連結,分別命名為 page1TextField 與 page2TextField,連結的方式可以參考從使用 UIButton 說 Hello 開始說起(上)ㄧ文,如果是使用 Xcode 4 的朋友也可以直接參考 Xcode 4 的 Assistant Editor 關聯編輯功能ㄧ文,節省撰寫程式碼的時間。

在透過 Storyboard Segue 傳值的部份,我們必須在頁面 2 的 UIViewController class 裡設置一個 NSString 的變數,它的目的是用來接收由頁面 1 透過 Storyboard Segue 所傳過來的資訊,程式碼如下。

@property (weak) NSString *string;

//別忘了在對應的實作檔中加入@synthesize string;

之後在 viewDidLoad 的函式裡我們將 string 的值指定給 page2TextField,這個動作會讓頁面 2 的畫面在被開啓時就會把 page2TextField 的內容設成 Storyboard Segue 所傳送過來的值。

- (void)viewDidLoad
{
    [super viewDidLoad];

    page2TextField.text = string;
}

接著回到頁面 1 的 UIViewController class 裡,新增一個內建的函式如下,就完成透過 Storyboard Segue 傳值的方法。

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {

    //將page2設定成Storyboard Segue的目標UIViewController
    id page2 = segue.destinationViewController;

    //將值透過Storyboard Segue帶給頁面2的string變數
    [page2 setValue:page1TextField.text forKey:@"string"];
}

另一個方法就是使用代理委托 delegate 的方法,透過建立一個協定 @protocol 的方式,讓其它的採納此協定的 class 可以實作協定內的函式,我們在頁面 2 的 UIViewController class 裡設置一個協定,並且在頁面 1 的 UIViewController class 裡實作協定裡的方法,讓程式執行到頁面 2 時,仍然能夠取得頁面 1 的實例 Instance,進而使用協定裡的方法來設定 page2TextField 的數值。

首先來到頁面 2 的 UIViewController class,建立一個協定 Page2Delegate,並且定義其內部的方法 passValue:,接著宣告一個採用此協定的物件 delegate,其程式碼如下。

#import <UIKit/UIKit.h>
//建立一個協定
@protocol Page2Delegate

//協定中的方法
- (void)passValue:(NSString *)value;

@end

@interface Page2ViewController : UIViewController
@property (weak, nonatomic) IBOutlet UITextField *page2TextField;
@property (weak) NSString *string;

//宣告一個採用Page2Delegate協定的物件
@property (weak) id<Page2Delegate> delegate;
@end

接下來就是決定協定中的方法要在什麼時候生效,也就是要在什麼地方呼叫 passValue: 方法函式,而此函式會在採用此協定的類別 Class 中被實作。回顧程式的需求,我們希望在頁面 2 按下返回頁面 1 的按鈕時,能將 page2TextField 的數值傳給頁面 1 的 page1TextField,所以呼叫協定 passValue: 方法的程式碼,勢必要寫在此按鈕事件中。

- (IBAction)returnToFirstPage:(id)sender {

    [self dismissViewControllerAnimated:YES completion:^{}];

    //呼叫協定中的方法並帶入page2textField的數值
    [delegate passValue:page2TextField.text];
}

現在,當我們按下頁面 2 的按鈕時,就會呼叫採用 Page2Delegate 協定的 class,而此 class 必須要實作此協定內的方法,所以回到頁面 1 的 UIViewController class,我們要替此 class 採用 Page2Delegate 的協定,並且實作協定內的 passValue: 方法函式。

採用協定的方式是在 @interface 區段的地方加上 <協定名稱> 的程式碼,由於此協定是寫在別的 class 中,所以在採用協定之前別忘了先引用它,以下是頁面 1 的 UIViewController class .h 標頭檔。

#import <Uikit/Uikit.h>

//引用持有Page2Delegate協定的class
#import "Page2ViewController.h"

@interface MLViewController : UIViewController <page2delegate> //採用協定
@property (weak, nonatomic) IBOutlet UITextField *page1TextField;

@end

接著在 .m 實作檔中實作協定內的 passValue: 方法函式。

- (void)passValue:(NSString *)value {

    //設定page1TextField為所取的的數值
    page1TextField.text = value;
}

最後一個步驟,也就是大家常常會忘記的,要將代理 delegate 設成自己(頁面 1 的 UIViewController),也就是大家在使用協定時最常寫的一行程式碼 object. delegate = self(object 指的就是頁面 2 的 UIViewController),至於這行程式碼要寫在哪裡?還記得之前透過 Storyboard Segue 傳值的部份,我們已經藉由內建的函式取得頁面 2 的 UIViewController 實例 page2,所以我們修改此內建函式,利用 page2 來設定 delegate 變數。

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {

    //將page2設定成Storyboard Segue的目標UIViewController
    id page2 = segue.destinationViewController;

    //將值透過Storyboard Segue帶給頁面2的string變數
    [page2 setValue:page1TextField.text forKey:@"string"];

    //將delegate設成自己(指定自己為代理)
    [page2 setValue:self forKey:@"delegate"];
}

如果忽略此步驟,頁面 2 裡的 delegate 參數在呼叫 passValue 方法時,並不會知道是誰(哪個 class)實作了它的方法,因此參數也無法由頁面 2 傳遞至頁面 1。


ps:如果你對於解散 UITextField 的小鍵盤有問題,可以參考UITextField 輸入結束後的收起小鍵盤的方式ㄧ文。






32 則留言:

  1. 您好,我有个问题是關於第一種傳值方法的。

    如果我1個view要連到2個view的話,該傳值方法是不是就要求所連接的2個view中都必鬚聲明同樣的string變量了呢?

    回覆刪除
  2. 你是說同時傳到兩個不同的 ViewController 中嗎?

    如果是按照上面的範例 segue 應該只能一對一才是一個 ViewController 對另一個 ViewController,想要指向兩個不同的 ViewController 可能需要在新增一個觸發的方式(新增一個新的按鈕)

    最後,基於上述的原因不太可能按下「同一個按鈕」就觸發兩個不同的 segue 並且呈現兩個不同的 ViewController ,但是 segue 所提供的內建方法函式只有一個,在我們沒有辦法分辨彼此的情況下,就需要「宣告同樣的 string 變數」,我想這可能就是您想想問的問題,在 ViewController 接到不同的變數之後才做不同的處理,
    這樣的觀念類似於 Super Class 與 Sub Class 之間的關係,他們可能都擁有同樣的變數名稱或是方法函式,但應用的方式卻不太一樣。

    簡單來說你有兩個 ViewController,即使他們彼此用到的資訊是完全不同的,但是它們仍必須將對方所需要用到的變數做宣告,只是在接到數值之後您可以選擇不使用它。

    同樣地,來源端的 ViewController 在透過 segue 的方法函式傳遞數值時,也必須傳送所有的資訊出去,因為他不知道哪一個 ViewController 會接到這些訊息。

    希望這樣解釋能幫助你解決問題!

    回覆刪除
    回覆
    1. 非常感謝!
      我是再1個ViewController中有2個按鈕,每個按鈕打開一個不同的ViewController。
      使用第一種方法時,運行報錯,提示我没有找到string,(只有一個ViewController包含了string變數)
      現在明白了,使用這種方法的話,需要宣告對方所包含的變數,即便這個變數不會被使用到。
      謝謝

      刪除
    2. 嗯嗯,很高興有幫到你。

      刪除
  3. 匿名9/26/2012

    非常棒的教學,
    在這個網站獲益良多,謝謝你們的努力!

    回覆刪除
    回覆
    1. 您好:

      謝謝您的支持~!!

      刪除
  4. 很實用的教學,推~

    不過我想請教一個問題
    若是第2個viewcontroller換成UINavigationView
    且此UINavigationView後面帶有一個UIViewController
    那該如何把第1個UIViewController的textfield值傳入呢?

    回覆刪除
    回覆
    1. 匿名者您好:

      依照您的意思是想設計成
      「主要ViewContorller」 --按下按鈕-->「NavigationController」---自動帶出root view-->「次要ViewContorller」。

      如果是按照上述設計,您必須在storyboard上也做好對應的連結:
      -主要ViewContorller 上的按鈕透過Segue(model)連結到 NavigationController
      -NavigationController 設定次要ViewContorller為root view controller
      -此時次要ViewContorller畫面邵方會自動多出一條NavigationBar

      之後在傳值的關鍵程式碼處做以下修改:
      UINavigationController *page2 = segue.destinationViewController;
      NSArray *viewControllers = page2.viewControllers;
      UIViewController *rootViewController = [viewControllers objectAtIndex:viewControllers.count - 1];

      [rootViewController setValue:page1TextField.text forKey:@"string"];

      原理是使用一個NSArray來取得UINavigationController下的所有viewControllers,只後再判斷誰是rootViewController

      之後delegate的設定方式也如法炮製。

      刪除
    2. 感謝牛奶~
      目前已經能把第1個ViewController的資料帶到UINavigationView的UIViewController了~

      可是卻不知道怎麼實作Delegate回到第1個ViewController
      主要是卡在第1個ViewController的PrepareForSegue中
      該如何設定delegate呢?

      刪除
    3. 匿名者您好:

      delegate 的設定方式,就與上述一樣,您可以自行研究看看,建議妳可以實作使用 Storyboard Segue 實作 UIViewController 的切換(上)(下)
      兩篇文章,最後再自行插入 UINavigationController,唯一要變動的部份就只有 「主要ViewController」的程式碼,因為「次要ViewController」的程式碼,主要是回傳值給 Delegate,當然協定的用意本身就是不知道在執行時最後會由誰來執行。

      你可以試試看
      [rootViewController setValue:self forKey:@"delegate"];

      也許會有幫助。

      刪除
    4. 可以了~
      是自己想的太複雜啦 XD

      謝謝牛奶,獲益良多^^

      刪除
    5. 匿名者您好:

      不用客氣喔!

      刪除
  5. 您好~~超好的觀念練習~很受用~~想再請問一下~我自己試著 用 2各 tableviewcontroller 內cell 間 資料轉移 可是一直跑不出來~~想問的是在tableviewcontroller 使用segue 是不是有需注意的地方~

    回覆刪除
  6. Bryan Chen 您好:

    關於2個 UITableViewController 的 CELL 資料轉移,您是透過使用 segue 的方式來製作,如果只是簡單的由A傳到B這方面是不會有問題,但是如果是AB互享傳資料,可能最出現不是唯一instance的疑慮,這點在 使用 Storyboard Segue 實作 UIViewController 的切換(下)這一篇文章中有說明。

    由於在使用 Storyboard Segue 來傳送資料時是單向的,當我們建立好 A CONTROLLER 之後,透過 Storyboard Segue 傳參數給 B CONTROLLER,才隨即建立取得 B CONTROLLER 實體,反之經 B CONTROLLER 透過 Storyboard Segue 傳送過來的參數,雖然是傳給 A CONTROLLER,但是他卻不會是我們原先的 A CONTROLLER,你會得到以下這樣的結果。

    A --> B -create-> A(new) 而不是 A --> B -return-> A

    另外,也許您的觀念上沒錯,只是我在字面上的誤解,兩個 UITableViewController 中,並不存在 CELL 的資料轉移,由上面的範例我們可以知道 UIViewController(UITableViewController)只能透過 Storyboard Segue 將數值傳給另一個 UIViewController(UITableViewController),CELL 上的數值呈現只是在 UIViewController(UITableViewController)被建立時,他自己去做設定才會生效,無法由其他人 UIViewController 來控制。

    最後你可以參考看看 在兩個不同的 Class 之間傳遞參數的方法 一文,也許會對您有些幫助。
    http://furnacedigital.blogspot.tw/2012/07/class.html

    回覆刪除
  7. 真的太感謝的你的教學文了!!!!

    回覆刪除
    回覆
    1. 您好:
      不客氣喔!

      刪除
  8. 匿名1/02/2014

    請問如果是第一個View上有一個button 按下後,第二個view 為tabbar,想要按下按鈕後傳值給tabbar的view ,用storyboard 應該怎麼做?

    回覆刪除
    回覆
    1. 你好:

      新年快樂,我不太確定你的問題是不是這個,你可以參考看看:
      http://stackoverflow.com/questions/18120549/passing-the-value-from-one-view-to-other-view-in-uitabbarcontroller

      另外在兩個不同的class之間傳遞參數,不限於使用協定,你可以參考這篇文章:
      http://furnacedigital.blogspot.tw/2012/07/class.html

      刪除
    2. 匿名1/03/2014

      謝謝您的回覆,
      我想要做的是第一個view1為登入畫面可輸入帳號,按按鈕後會進入view2 & view 3(為tabbar切換畫面)
      view1輸入的帳號,view2&view 3可以收到,
      根據您提供的範例,可以成功利用appDelegate傳值,謝謝您,給予新手的我很大的幫助
      但同例想用segue傳值,都一直傳不成功,
      請問如果用segue要如何寫?是否有例子可參考,謝謝您

      刪除
    3. 您好:
      我建議妳可以使用tabbar樣板來測試,在tabbar樣板中的storyboard裡也同樣有「segue」,我自己是沒試過,不過應該也可透過增加 (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender 函式的方式來傳送參數,確認都ok沒問題,你可以在自行新增一個uiviewcontroller,把起始畫面指向他(移動storyboard中uiviewcontroller的箭頭即可),來實作帳號密碼輸入的介面。

      刪除
    4. 匿名1/07/2014

      謝謝,我會再試試看

      刪除
  9. 匿名9/02/2015

    請問 有沒有方法可以在prepareForSegue之前先動作
    比如我要加入loading畫面
    我是有試過利用按鈕touch up inside先觸發一個+入LOADING畫面的動作
    但是程式看起來當我按下的時候
    已經在prepareForSegue
    然後畫面就卡住在第二頁 VIEWDIDLOAD的部分了

    回覆刪除
    回覆
    1. 您好
      還是建議你將load的整個歷程做在第二頁,畢竟整個預載的資源都要在第二頁做計算
      如果硬要做在第一頁的話,你必須把切換頁面的程式碼動作寫在一個單獨的函式中
      當你按下換頁按鈕會觸發你預載畫面的等待函式,整個結束後,才去呼叫剛剛那個切頁的單獨函式

      刪除
    2. 匿名9/03/2015

      你好
      我的目的主要是要在切換的時候加入loading畫面
      因為我第二頁的有些資訊是從網路上抓的
      所以切換的時候都會等待一段時間
      我是想在他第2頁viewdidload之前的這段時間加入loading畫面
      這樣看起來比較不會以為是當機
      假如說在第2頁viewdidload才加入loading畫面
      當資源載入完成之後 好像也不必要loading畫面
      是否有辦法達成這個目的

      刪除
    3. 您好:

      我說的方式就是再換頁面前做讀取畫面,上面說的方式是寫在第一頁唷,不是第二頁。

      如果你要將讀取畫面實作再第二頁,你要把loading動畫放在viewdidload的地方,直到你網頁資訊都ready後,才去停止他。
      兩個方法都是一樣意思,你必須要能夠抓到你下載網路資源完成的那個瞬間!

      刪除
  10. 匿名2/11/2016

    你好!我想傳兩個欄位,設兩個(void)passValue,前面沒什問題,但是卡在最後這裡
    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    id page2 = segue.destinationViewController;
    [page2 setValue:nameTextField.text forKey:@"string"];
    id page2 = segue.destinationViewController;
    [page2 setValue:ageTextField.text forKey:@"string1"];
    [page2 setValue:self forKey:@"delegate"];
    } 我的問題是setValue這裡,好像只能傳一個值,小弟卡在這邊,牛奶大可否指點一下迷津,謝謝!

    回覆刪除
    回覆
    1. 您好:

      確認一下你第一個變數 string 有接收到正確的數值嗎?然後你另一個變數 string1 在 page2 也確定有這個變數!
      如果這樣還是不行的話,可以建議你傳「物件」過去,而不是單一字串。

      試試看把兩個字串變成一個物件一次傳過去!

      刪除
    2. 匿名2/12/2016

      謝謝大大!問題已解決。

      刪除
    3. 匿名2/13/2016

      大大,小弟再請教一個問題,當第二頁的值已經傳到第一頁了,並且顯示在第一頁,但是我想把傳過去的值,隱藏起來,TextField顯示空白。該怎麼做?我用self.ageTexField.hidden 會把文字包含整個欄位隱藏,改用.self.ageTexField.text hidden 沒辦法跑,哪裡需要改一下,謝謝您!

      刪除
    4. 你忘記還可以用 text color (文字顏色)了

      文字顏色=背景顏色 也可當做隱藏

      刪除
    5. 匿名2/14/2016

      阿哈!我倒是沒想到,謝謝牛奶大!。我還想釐清一個小問題。先把view1用segue連到view2。之後用Storyboard Segue將值由view1傳到view2,這部分行得通。但是要將view2的值回傳到view1,為何非得用到delegate?為何不用Storyboard Segue回傳?小弟這點不解,望牛奶大指點迷津。

      刪除
    6. 這個有點兒深奧,這牽扯到兩個VIEW的instance(實例)!

      Storyboard Segue 這是屬於View1的東西,View2並不會知道。
      在這個例子中,只有View1認識View2,並且在建立View2的同時設定其上的delegate指向自己,這樣可以上View2的函式轉移到View1上被實作,藉由函式的轉移,帶入的參數也可以帶至View1來執行了。

      當然想要在兩個CLASS之間傳遞參數,不只是使用delegate而已,你可以參考這邊的文章實作看看!!
      http://furnacedigital.blogspot.tw/2012/07/class.html

      刪除