Swiftアプリに複数のView Controllerがあり、それらの間でデータを受け渡しできるようにしたいとします。ビューコントローラースタックで数レベル下にいる場合、別のビューコントローラーにデータを渡すにはどうすればよいですか?またはタブバービューコントローラーのタブ間?
(注意:この質問は「リンガー」です。)非常に多くの質問が寄せられるので、このテーマに関するチュートリアルを書くことにしました。以下の私の答えを参照してください。
Swiftアプリに複数のView Controllerがあり、それらの間でデータを受け渡しできるようにしたいとします。ビューコントローラースタックで数レベル下にいる場合、別のビューコントローラーにデータを渡すにはどうすればよいですか?またはタブバービューコントローラーのタブ間?
(注意:この質問は「リンガー」です。)非常に多くの質問が寄せられるので、このテーマに関するチュートリアルを書くことにしました。以下の私の答えを参照してください。
回答:
あなたの質問は非常に広いです。すべてのシナリオに対して1つの単純なキャッチオールソリューションがあることを示唆するには、少し素朴です。それでは、これらのシナリオをいくつか見ていきましょう。
私の経験でスタックオーバーフローについて尋ねられる最も一般的なシナリオは、1つのビューコントローラーから次のビューコントローラーへの単純な情報の受け渡しです。
ストーリーボードを使用している場合、最初のビューコントローラーはをオーバーライドできますprepareForSegue
。UIStoryboardSegue
オブジェクトは、このメソッドが呼び出されたときに渡され、それが私たちの先のビューコントローラへの参照が含まれています。ここでは、渡す値を設定できます。
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)
}
どちらの場合も、myInformation
1つのビューコントローラーから次のビューコントローラーに渡す必要のあるデータを保持する各ビューコントローラーのプロパティです。各コントローラーで同じ名前にする必要はありません。
のタブ間で情報を共有することもできます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
}
他にもいくつかのシナリオがあります。この回答がそれらすべてをカバーすることは決してありません。
prepareForSegue
です。この非常に単純な観察が、ここにある他の回答や余談のなかで失われてしまうのは残念です。
prepareForSegue
か、他の直接のほぼすべてのシナリオでの情報の転送を、彼らはこのような状況が動作しないと私たちは、これらのよりグローバルなアプローチについてのそれらを教えるために持っているためのシナリオで現れたときに、単に初心者で大丈夫。
この質問はいつも出てきます。
1つの提案は、データコンテナーシングルトンを作成することです。アプリケーションの存続期間中に一度だけ作成され、アプリの存続期間中持続するオブジェクト。
このアプローチは、アプリのさまざまなクラスで利用可能/変更可能である必要があるグローバルアプリデータがある場合に適しています。
ビューコントローラ間で一方向または双方向のリンクを設定するなどの他のアプローチは、ビューコントローラ間で情報/メッセージを直接渡す場合に適しています。
(他の選択肢については、以下のnhgrifの回答を参照してください。)
データコンテナーシングルトンを使用して、シングルトンへの参照を格納するクラスにプロパティを追加し、アクセスが必要なときにいつでもそのプロパティを使用します。
シングルトンを設定してコンテンツをディスクに保存することで、アプリの状態が起動間で持続するようにすることができます。
これを行う方法を示すデモプロジェクトをGitHubで作成しました。ここにリンクがあります:
GitHubのSwiftDataContainerSingletonプロジェクト 以下は、そのプロジェクトのREADMEです。
データコンテナーシングルトンを使用してアプリケーションの状態を保存し、オブジェクト間で共有するデモ。
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()
}
オブザーバーコードでは、データコンテナーのプロパティをに保存しますNSUserDefaults
。NSCoding
、コアデータ、またはその他のさまざまな方法を使用して状態データを保存することもできます。
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があります。ViewController
1つ目は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でユーザーインターフェイスに値をロードする必要があります。
スウィフト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
}
}
ViewControllerA
に送信しましたViewControllerB
。最後の波括弧の直前に、コードスニペットViewControllerA.swift
(ViewControllerA.swift
実際には、ファイルの名前が実際に付けられている場所です)を貼り付けました。" prepare
"は、実際には特定のクラスに組み込まれた既存の特別な関数であり、[何もしない]ため、これを " override
"にする必要があります
もう1つの方法は、通知センター(NSNotificationCenter)を使用して通知を投稿することです。それは非常に疎結合です。通知の送信者は、誰が聞いているかを知る必要も、気にする必要もありません。通知を投稿してそれを忘れてしまうだけです。
通知は、1対多のメッセージパッシングに適しています。これは、特定のメッセージをリッスンするオブザーバが任意の数になる可能性があるためです。
データコントローラーシングルを作成する代わりに、データコントローラーインスタンスを作成して渡すことをお勧めします。依存性注入をサポートするには、まず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の答えを使用するのが最善です。
このアプローチにより、ビューをロジック部分から分離できます。
@nhgrifが彼の優れた答えで指摘したように、VC(ビューコントローラー)と他のオブジェクトが互いに通信する方法はたくさんあります。
最初の回答で概説したデータシングルトンは、直接通信することよりも、実際にグローバルな状態を共有および保存することについてです。
nhrifの回答では、送信元から宛先VCに直接情報を送信できます。返信で述べたように、宛先から送信元にメッセージを送り返すことも可能です。
実際、異なるView Controller間にアクティブな一方向または双方向のチャネルを設定できます。ビューコントローラーがストーリーボードセグエを介してリンクされている場合、リンクを設定する時間はprepareFor Segueメソッドにあります。
親ビューコントローラーを使用して2つの異なるテーブルビューを子としてホストするサンプルプロジェクトがGithubにあります。子ビューコントローラーは埋め込みセグエを使用してリンクされ、親ビューコントローラーはprepareForSegueメソッドの各ビューコントローラーと双方向リンクを配線します。
あなたはできますgithubの上でそのプロジェクトを見つける(リンク)。ただし、Objective-Cで作成しましたが、Swiftに変換していないため、Objective-Cに慣れていない場合、理解するのが少し難しいかもしれません。
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
}
}
データを取得するタイミングによって異なります。
いつでもデータを取得したい場合は、シングルトンパターンを使用できます。パターンクラスは、アプリの実行中にアクティブになります。シングルトンパターンの例を次に示します。
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")
}