View ControllerとSwiftの他のオブジェクトの間でどのようにデータを共有しますか?


88

Swiftアプリに複数のView Controllerがあり、それらの間でデータを受け渡しできるようにしたいとします。ビューコントローラースタックで数レベル下にいる場合、別のビューコントローラーにデータを渡すにはどうすればよいですか?またはタブバービューコントローラーのタブ間?

(注意:この質問は「リンガー」です。)非常に多くの質問が寄せられるので、このテーマに関するチュートリアルを書くことにしました。以下の私の答えを参照してください。


1
代表者のグーグルを試す
milo526 '19

4
私はこれを投稿して、SOで毎日表示されるこの質問の10,000のインスタンスに対する解決策を提供できるようにしました。私の答えを見てください。:)
Duncan C

申し訳ありませんが、反応が速すぎました:)これにリンクできるようにするには:)
milo526 '19

2
心配ない。あなたは私が#10,001だと思ったでしょ?<grin>
Duncan C

4
@DuncanC私はあなたの答えが好きではありません。:(それは...キャッチオールあらゆるシナリオの答えとして大丈夫-ISNだinsomuchas、それはなります仕事のすべてのシナリオのために、それもありません右のほとんどのためのアプローチの任意のシナリオ。それにもかかわらず、私たちは私たちの頭の中にそれを今持っていますこのトピックの質問にこの質問の複製としてマークを付けるのは良い考えですか?しないでください
nhgrif

回答:


91

あなたの質問は非常に広いです。すべてのシナリオに対して1つの単純なキャッチオールソリューションがあることを示唆するには、少し素朴です。それでは、これらのシナリオをいくつか見ていきましょう。


私の経験でスタックオーバーフローについて尋ねられる最も一般的なシナリオは、1つのビューコントローラーから次のビューコントローラーへの単純な情報の受け渡しです。

ストーリーボードを使用している場合、最初のビューコントローラーはをオーバーライドできますprepareForSegueUIStoryboardSegueオブジェクトは、このメソッドが呼び出されたときに渡され、それが私たちの先のビューコントローラへの参照が含まれています。ここでは、渡す値を設定できます。

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "MySegueID" {
        if let destination = segue.destination as? SecondController {
            destination.myInformation = self.myInformation
        }
    }
}

または、ストーリーボードを使用していない場合は、nibからビューコントローラーを読み込みます。その場合、コードは少し単純になります。

func showNextController() {
    let destination = SecondController(nibName: "SecondController", bundle: nil)
    destination.myInformation = self.myInformation
    show(destination, sender: self)
}

どちらの場合も、myInformation1つのビューコントローラーから次のビューコントローラーに渡す必要のあるデータを保持する各ビューコントローラーのプロパティです。各コントローラーで同じ名前にする必要はありません。


のタブ間で情報を共有することもできますUITabBarController

この場合、実際にはさらに単純になる可能性があります。

まず、のサブクラスを作成してUITabBarController、さまざまなタブ間で共有したい情報のプロパティを与えます。

class MyCustomTabController: UITabBarController {
    var myInformation: [String: AnyObject]?
}

私たちはストーリーボードから我々のアプリを構築している場合今、私たちは、単にデフォルトから私たちのタブバーコントローラのクラスを変更UITabBarControllerしますMyCustomTabController。ストーリーボードを使用しない場合は、デフォルトのUITabBarControllerクラスではなく、このカスタムクラスのインスタンスをインスタンス化し、ビューコントローラーをこれに追加します。

これで、タブバーコントローラー内のすべてのビューコントローラーがこのプロパティにアクセスできるようになります。

if let tbc = self.tabBarController as? MyCustomTabController {
    // do something with tbc.myInformation
}

そしてUINavigationController、同じ方法でサブクラス化することにより、ナビゲーションスタック全体でデータを共有するために同じアプローチを取ることができます。

if let nc = self.navigationController as? MyCustomNavController {
    // do something with nc.myInformation
}

他にもいくつかのシナリオがあります。この回答がそれらすべてをカバーすることは決してありません。


1
また、チャネルが宛先のビューコントローラからソースのビューコントローラに情報を送り返すことが必要になる場合があることも付け加えておきます。このような状況を処理する一般的な方法は、宛先にデリゲートプロパティを追加し、次にソースビューコントローラーのprepareForSegueで宛先ビューコントローラーのデリゲートプロパティをselfに設定することです。(そして、宛先VCがソースVCにメッセージを送信するために使用するメッセージを定義するプロトコルを定義します)
Duncan C

1
nhgrif、同意する。新しい開発者への助言は、ストーリーボードのシーン間でデータを渡す必要がある場合は、を使用することprepareForSegueです。この非常に単純な観察が、ここにある他の回答や余談のなかで失われてしまうのは残念です。
Rob

2
@ロブうん。シングルトンと通知は最後の選択でなければなりません。我々は選ぶべきprepareForSegueか、他の直接のほぼすべてのシナリオでの情報の転送を、彼らはこのような状況が動作しないと私たちは、これらのよりグローバルなアプローチについてのそれらを教えるために持っているためのシナリオで現れたときに、単に初心者で大丈夫。
nhgrif 2015年

1
場合によります。しかし、アプリデリゲートを、他にどこに配置すればよいかわからないコードのダンプ場所として使用することについて非常に心配しています。ここに狂気への道があります。
nhgrif 2016年

2
@nhgrif。あなたの答えのためのTHX。しかし、たとえば4つまたは5つのビューコントローラー間でデータを渡したい場合はどうでしょうか。クライアントのログインやパスワードなどを管理する4-5個のビューコントローラーがあり、これらのビューコントローラー間でユーザーの電子メールを渡したい場合、各ビューコントローラーでvarを宣言し、それをprepareforsegue内で渡すよりも便利な方法があります。一度宣言して各ビューコントローラーがそれにアクセスできる方法はありますか?
lozflan 2016年

45

この質問はいつも出てきます。

1つの提案は、データコンテナーシングルトンを作成することです。アプリケーションの存続期間中に一度だけ作成され、アプリの存続期間中持続するオブジェクト。

このアプローチは、アプリのさまざまなクラスで利用可能/変更可能である必要があるグローバルアプリデータがある場合に適しています。

ビューコントローラ間で一方向または双方向のリンクを設定するなどの他のアプローチは、ビューコントローラ間で情報/メッセージを直接渡す場合に適しています。

(他の選択肢については、以下のnhgrifの回答を参照してください。)

データコンテナーシングルトンを使用して、シングルトンへの参照を格納するクラスにプロパティを追加し、アクセスが必要なときにいつでもそのプロパティを使用します。

シングルトンを設定してコンテンツをディスクに保存することで、アプリの状態が起動間で持続するようにすることができます。

これを行う方法を示すデモプロジェクトをGitHubで作成しました。ここにリンクがあります:

GitHubのSwiftDataContainerSingletonプロジェクト 以下は、そのプロジェクトのREADMEです。

SwiftDataContainerSingleton

データコンテナーシングルトンを使用してアプリケーションの状態を保存し、オブジェクト間で共有するデモ。

DataContainerSingletonクラスは、実際のシングルトンです。

静的定数sharedDataContainerを使用して、シングルトンへの参照を保存します。

シングルトンにアクセスするには、構文を使用します

DataContainerSingleton.sharedDataContainer

サンプルプロジェクトでは、データコンテナに3つのプロパティを定義しています。

  var someString: String?
  var someOtherString: String?
  var someInt: Int?

someIntデータコンテナーからプロパティを読み込むには、次のようなコードを使用します。

let theInt = DataContainerSingleton.sharedDataContainer.someInt

値をsomeIntに保存するには、次の構文を使用します。

DataContainerSingleton.sharedDataContainer.someInt = 3

DataContainerSingletonのinitメソッドは、のオブザーバーを追加しUIApplicationDidEnterBackgroundNotificationます。そのコードは次のようになります。

goToBackgroundObserver = NSNotificationCenter.defaultCenter().addObserverForName(
  UIApplicationDidEnterBackgroundNotification,
  object: nil,
  queue: nil)
  {
    (note: NSNotification!) -> Void in
    let defaults = NSUserDefaults.standardUserDefaults()
    //-----------------------------------------------------------------------------
    //This code saves the singleton's properties to NSUserDefaults.
    //edit this code to save your custom properties
    defaults.setObject( self.someString, forKey: DefaultsKeys.someString)
    defaults.setObject( self.someOtherString, forKey: DefaultsKeys.someOtherString)
    defaults.setObject( self.someInt, forKey: DefaultsKeys.someInt)
    //-----------------------------------------------------------------------------

    //Tell NSUserDefaults to save to disk now.
    defaults.synchronize()
}

オブザーバーコードでは、データコンテナーのプロパティをに保存しますNSUserDefaultsNSCoding、コアデータ、またはその他のさまざまな方法を使用して状態データを保存することもできます。

DataContainerSingletonのinitメソッドは、そのプロパティの保存された値もロードしようとします。

initメソッドのその部分は次のようになります。

let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code reads the singleton's properties from NSUserDefaults.
//edit this code to load your custom properties
someString = defaults.objectForKey(DefaultsKeys.someString) as! String?
someOtherString = defaults.objectForKey(DefaultsKeys.someOtherString) as! String?
someInt = defaults.objectForKey(DefaultsKeys.someInt) as! Int?
//-----------------------------------------------------------------------------

値をNSUserDefaultsにロードおよび保存するためのキーはDefaultsKeys、次のように定義されたstructの一部である文字列定数として保存されます。

struct DefaultsKeys
{
  static let someString  = "someString"
  static let someOtherString  = "someOtherString"
  static let someInt  = "someInt"
}

これらの定数の1つを次のように参照します。

DefaultsKeys.someInt

データコンテナーシングルトンの使用:

このサンプルアプリケーションでは、データコンテナーシングルトンを3部構成で使用しています。

2つのView Controllerがあります。ViewController1つ目はUIViewControllerのカスタムサブクラスで、2つ目はUIViewControllerのカスタムサブクラスですSecondVC

両方のビューコントローラーにはテキストフィールドがあり、どちらもデータコンテナーシングルトンのsomeIntプロパティからviewWillAppearメソッドのテキストフィールドに値を読み込み、テキストフィールドからデータコンテナーの「someInt」に現在の値を保存します。

値をテキストフィールドに読み込むコードは、viewWillAppear:メソッド内にあります。

override func viewWillAppear(animated: Bool)
{
  //Load the value "someInt" from our shared ata container singleton
  let value = DataContainerSingleton.sharedDataContainer.someInt ?? 0
  
  //Install the value into the text field.
  textField.text =  "\(value)"
}

ユーザーが編集した値をデータコンテナーに保存するコードは、ビューコントローラーのtextFieldShouldEndEditingメソッドにあります。

 func textFieldShouldEndEditing(textField: UITextField) -> Bool
 {
   //Save the changed value back to our data container singleton
   DataContainerSingleton.sharedDataContainer.someInt = textField.text!.toInt()
   return true
 }

ビューコントローラーが表示されるたびにUIが更新されるように、viewDidLoadではなく、viewWillAppearでユーザーインターフェイスに値をロードする必要があります。


8
リソースとして質疑応答を作成するために時間を費やしたのは素晴らしいことだと思うので、私はこれに反対票を投じたくありません。ありがとうございました。それにもかかわらず、モデルオブジェクトのシングルトンを提唱することは、新しい開発者に大きな害を及ぼします。私は「singletons is evil」キャンプには参加していません(ただし、問題をよりよく理解するには、このフレーズをググる必要があります)が、モデルデータはシングルトンの疑わしい/議論の余地のある使用だと思います。
Rob

双方向のリンクについてのすばらしい記事をぜひご覧ください
Cmag

@Duncan CこんにちはDuncan私は各モデルで静的オブジェクトを作成しているので、どこからでもデータを取得できます。
Virendra Singh Rathore 2017年

@VirendraSinghRathore、グローバル静的変数は、アプリ間でデータを共有するための最悪の方法です。アプリの各部分を密に結合し、深刻な相互依存関係を引き起こします。それは「まさに正しい」の正反対です。
ダンカンC

@DuncanC-このパターンはCurrentUserオブジェクトで機能しますか?基本的に、アプリにログインしている単一のユーザーですか?thx
timpone 2017年

9

スウィフト4

データを迅速に渡すためのアプローチは数多くあります。ここで私はそれの最良のアプローチのいくつかを追加しています。

1)StoryBoard Segueの使用

ストーリーボードセグエは、ソースビューコントローラーと宛先ビューコントローラー間でデータをやり取りする場合や、その逆の場合にも非常に役立ちます。

// If you want to pass data from ViewControllerB to ViewControllerA while user tap on back button of ViewControllerB.
        @IBAction func unWindSeague (_ sender : UIStoryboardSegue) {
            if sender.source is ViewControllerB  {
                if let _ = sender.source as? ViewControllerB {
                    self.textLabel.text = "Came from B = B->A , B exited"
                }
            }
        }

// If you want to send data from ViewControllerA to ViewControllerB
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if  segue.destination is ViewControllerB {
                if let vc = segue.destination as? ViewControllerB {
                    vc.dataStr = "Comming from A View Controller"
                }
            }
        }

2)デリゲートメソッドの使用

ViewControllerD

//Make the Delegate protocol in Child View Controller (Make the protocol in Class from You want to Send Data)
    protocol  SendDataFromDelegate {
        func sendData(data : String)
    }

    import UIKit

    class ViewControllerD: UIViewController {

        @IBOutlet weak var textLabelD: UILabel!

        var delegate : SendDataFromDelegate?  //Create Delegate Variable for Registering it to pass the data

        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            textLabelD.text = "Child View Controller"
        }

        @IBAction func btnDismissTapped (_ sender : UIButton) {
            textLabelD.text = "Data Sent Successfully to View Controller C using Delegate Approach"
            self.delegate?.sendData(data:textLabelD.text! )
            _ = self.dismiss(animated: true, completion:nil)
        }
    }

ViewControllerC

    import UIKit

    class ViewControllerC: UIViewController , SendDataFromDelegate {

        @IBOutlet weak var textLabelC: UILabel!

        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
        }

        @IBAction func btnPushToViewControllerDTapped( _ sender : UIButton) {
            if let vcD = self.storyboard?.instantiateViewController(withIdentifier: "ViewControllerD") as?  ViewControllerD  {
                vcD.delegate = self // Registring Delegate (When View Conteoller D gets Dismiss It can call sendData method
    //            vcD.textLabelD.text = "This is Data Passing by Referenceing View Controller D Text Label." //Data Passing Between View Controllers using Data Passing
                self.present(vcD, animated: true, completion: nil)
            }
        }

        //This Method will called when when viewcontrollerD will dismiss. (You can also say it is a implementation of Protocol Method)
        func sendData(data: String) {
            self.textLabelC.text = data
        }

    }

StackOverflowの回答のSwiftコードスニペットをどこに配置するかについて完全かつ完全に迷っているGoogle社員の場合、コードがどこに行くかを常に推測する必要があると思われるため、オプション1)を使用してからViewControllerAに送信しましたViewControllerB。最後の波括弧の直前に、コードスニペットViewControllerA.swiftViewControllerA.swift実際には、ファイルの名前が実際に付けられている場所です)を貼り付けました。" prepare"は、実際には特定のクラスに組み込まれた既存の特別な関数であり、[何もしない]ため、これを " override"にする必要があります
velkoon

8

もう1つの方法は、通知センター(NSNotificationCenter)を使用して通知を投稿することです。それは非常に疎結合です。通知の送信者は、誰が聞いているかを知る必要も、気にする必要もありません。通知を投稿してそれを忘れてしまうだけです。

通知は、1対多のメッセージパッシングに適しています。これは、特定のメッセージをリッスンするオブザーバが任意の数になる可能性があるためです。


2
通知センターを使用すると、結合が緩すぎる可能性があることに注意してください。プログラムのフローを追跡することが非常に困難になる可能性があるため、注意して使用する必要があります。
Duncan C

2

データコントローラーシングルを作成する代わりに、データコントローラーインスタンスを作成して渡すことをお勧めします。依存性注入をサポートするには、まずDataControllerプロトコルを作成します。

protocol DataController {
    var someInt : Int {get set} 
    var someString : String {get set}
}

次に、SpecificDataController(または現在適切な名前であれば)クラスを作成します。

class SpecificDataController : DataController {
   var someInt : Int = 5
   var someString : String = "Hello data" 
}

その場合、ViewControllerクラスにはを保持するフィールドが必要dataControllerです。のタイプがdataControllerプロトコルであることに注意してくださいDataController。このようにして、データコントローラーの実装を簡単に切り替えることができます。

class ViewController : UIViewController {
   var dataController : DataController?
   ...
}

ではAppDelegate、私たちのViewControllerのを設定することができますdataController

 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    if let viewController = self.window?.rootViewController as? ViewController {
        viewController.dataController =  SpecificDataController()
    }   
    return true
}

別のviewControllerに移動すると、次のものを渡すことができますdataController

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    ...   
}

これで、データコントローラーを別のタスクに切り替える場合、これをで行うことができAppDelegate、データコントローラーを使用する他のコードを変更する必要はありません。

もちろん、単一の値を渡すだけの場合は、やりすぎです。この場合、nhgrifの答えを使用するのが最善です。

このアプローチにより、ビューをロジック部分から分離できます。


1
こんにちは、このアプローチはクリーンでテスト可能で、小さなアプリケーションではほとんどの場合使用しますが、大きなアプリケーションでは、すべてのVC(おそらくルートVCでも)が依存関係を必要としない場合があります(この場合はDataControllerなど)。すべてのVCがそれを渡すためだけに依存関係を要求するのは無駄です。また、さまざまなタイプのVCを使用する場合(たとえば、通常のUIVCとNavigationVC)、依存関係変数を追加するためだけに、それらのさまざまなタイプをサブクラス化する必要があります。これにどのように取り組みますか?
RobertoCuba 2018

1

@nhgrifが彼の優れた答えで指摘したように、VC(ビューコントローラー)と他のオブジェクトが互いに通信する方法はたくさんあります。

最初の回答で概説したデータシングルトンは、直接通信することよりも、実際にグローバルな状態を共有および保存することについてです。

nhrifの回答では、送信元から宛先VCに直接情報を送信できます。返信で述べたように、宛先から送信元にメッセージを送り返すことも可能です。

実際、異なるView Controller間にアクティブな一方向または双方向のチャネルを設定できます。ビューコントローラーがストーリーボードセグエを介してリンクされている場合、リンクを設定する時間はprepareFor Segueメソッドにあります。

親ビューコントローラーを使用して2つの異なるテーブルビューを子としてホストするサンプルプロジェクトがGithubにあります。子ビューコントローラーは埋め込みセグエを使用してリンクされ、親ビューコントローラーはprepareForSegueメソッドの各ビューコントローラーと双方向リンクを配線します。

あなたはできますgithubの上でそのプロジェクトを見つける(リンク)。ただし、Objective-Cで作成しましたが、Swiftに変換していないため、Objective-Cに慣れていない場合、理解するのが少し難しいかもしれません。


1

SWIFT 3:

特定されたセグエを使用したスト​​ーリーボードがある場合は、以下を使用します。

func prepare(for segue: UIStoryboardSegue, sender: Any?)

ただし、異なるUIViewController間のナビゲーションを含め、プログラムですべてを行う場合は、次のメソッドを使用します。

func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)

注:UINavigationControllerを作成する必要がある2番目の方法を使用するには、UIViewControllersをデリゲートにプッシュし、プロトコルUINavigationControllerDelegateに準拠する必要があります。

   class MyNavigationController: UINavigationController, UINavigationControllerDelegate {

    override func viewDidLoad() {
        self.delegate = self
    }

    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {

     // do what ever you need before going to the next UIViewController or back
     //this method will be always called when you are pushing or popping the ViewController

    }
}

self.delegate = self
malhalを

1

データを取得するタイミングによって異なります。

いつでもデータを取得したい場合は、シングルトンパターンを使用できます。パターンクラスは、アプリの実行中にアクティブになります。シングルトンパターンの例を次に示します。

class AppSession: NSObject {

    static let shared = SessionManager()
    var username = "Duncan"
}

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        print(AppSession.shared.username)
    }
}

アクション後にデータを取得する場合は、NotificationCenterを使用できます。

extension Notification.Name {
    static let loggedOut = Notification.Name("loggedOut")
}

@IBAction func logoutAction(_ sender: Any) {
    NotificationCenter.default.post(name: .loggedOut, object: nil)
}

NotificationCenter.default.addObserver(forName: .loggedOut, object: nil, queue: OperationQueue.main) { (notify) in
    print("User logged out")
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.