簡體   English   中英

你如何在 Swift 中的視圖控制器和其他對象之間共享數據?

[英]How do you share data between view controllers and other objects in Swift?

假設我的 Swift 應用程序中有多個視圖控制器,我希望能夠在它們之間傳遞數據。 如果我在視圖控制器堆棧中向下幾個級別,我如何將數據傳遞給另一個視圖控制器? 或者在選項卡欄視圖控制器中的選項卡之間?

(注意,這個問題是一個“鈴聲”。)問得太多了,我決定寫一篇關於這個主題的教程。 看我下面的回答。

你的問題廣泛。 建議對每種情況都有一個簡單的包羅萬象的解決方案有點幼稚。 那么,讓我們來看看其中的一些場景。


根據我的經驗,在 Stack Overflow 上被問到的最常見的場景是從一個視圖控制器到下一個視圖控制器的簡單傳遞信息。

如果我們使用故事板,我們的第一個視圖控制器可以覆蓋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
        }
    }
}

或者,如果我們不使用故事板,那么我們將從筆尖加載我們的視圖控制器。 那么我們的代碼稍微簡單一些。

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

在這兩種情況下, myInformation是每個視圖控制器上的一個屬性,保存需要從一個視圖控制器傳遞到下一個視圖控制器的任何數據。 它們顯然不必在每個控制器上都具有相同的名稱。


我們可能還想在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
}

還有其他幾種情況。 這個答案絕不涵蓋所有這些。

這個問題一直出現。

一個建議是創建一個數據容器單例:在應用程序的生命周期中創建一次且僅一次的對象,並在應用程序的生命周期中持續存在。

這種方法非常適用於需要在應用程序中的不同類之間可用/可修改的全局應用程序數據的情況。

其他方法,例如在視圖控制器之間設置單向或雙向鏈接,更適合直接在視圖控制器之間傳遞信息/消息的情況。

(有關其他替代方案,請參閱下面的 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()
}

在觀察者代碼中,它將數據容器的屬性保存到NSUserDefaults 您還可以使用NSCoding 、 Core Data 或各種其他方法來保存狀態數據。

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 DefaultsKeys
{
  static let someString  = "someString"
  static let someOtherString  = "someOtherString"
  static let someInt  = "someInt"
}

您可以像這樣引用這些常量之一:

DefaultsKeys.someInt

使用數據容器單例:

這個示例應用程序簡單地使用了數據容器單例。

有兩個視圖控制器。 第一個是 UIViewController ViewController的自定義子類,第二個是 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
 }

您應該在 viewWillAppear 而不是 viewDidLoad 中將值加載到您的用戶界面中,以便您的 UI 在每次顯示視圖控制器時更新。

斯威夫特 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) 使用委托方法

視圖控制器D

//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)
        }
    }

視圖控制器C

    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
        }

    }

另一種選擇是使用通知中心 (NSNotificationCenter) 並發布通知。 這是一個非常松散的耦合。 通知的發送者不需要知道或關心誰在聽。 它只是發布一個通知,然后忘記它。

通知適用於一對多的消息傳遞,因為可以有任意數量的觀察者監聽給定的消息。

我建議創建一個數據控制器實例並傳遞它,而不是創建一個數據控制器單例。 為了支持依賴注入,我首先要創建一個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。 正如我在回復中提到的,也可以將消息從目標發送回源。

事實上,您可以在不同的視圖控制器之間設置一個活動的單向或 2 向通道。 如果視圖控制器通過 storyboard segue 鏈接,則設置鏈接的時間在 prepareFor Segue 方法中。

我在 Github 上有一個示例項目,它使用父視圖控制器將 2 個不同的表視圖作為子視圖托管。 子視圖控制器使用嵌入轉場鏈接,父視圖控制器在 prepareForSegue 方法中與每個視圖控制器連接 2 路鏈接。

您可以在 github 上找到該項目(鏈接)。 然而,我是用 Objective-C 編寫的,並沒有將它轉換為 Swift,所以如果你不習慣使用 Objective-C,可能會有點難以理解

快速 3:

如果您有一個帶有已識別轉場的故事板,請使用:

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

盡管如果您以編程方式執行所有操作,包括在不同 UIViewController 之間進行導航,請使用以下方法:

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

注意:要使用第二種方式,您需要制作 UINavigationController,您將 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")
}

我這樣做的方式不是在視圖控制器之間傳遞數據,而是在全局聲明一個變量。 你甚至可以用一個函數來做到這一點!

例如:

var a = "a"
func abc() {
   print("abc")
}
class ViewController: UIViewController {

}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM