如何在Swift中的视图控制器和其他对象之间共享数据?

时间:2015-04-19 19:43:27

标签: ios swift

假设我的Swift应用程序中有多个视图控制器,我希望能够在它们之间传递数据。如果我在视图控制器堆栈中关闭了几个级别,如何将数据传递给另一个视图控制器?或者在标签栏视图控制器中的标签之间?

(注意,这个问题是一个"铃声"。)它被问到这么多,我决定写一个关于这个主题的教程。请参阅下面的答案。

8 个答案:

答案 0 :(得分:87)

您的问题非常广泛。建议对每种情况都有一个简单的全能解决方案是有点幼稚。所以,让我们来看看其中一些场景。


根据我的经验,关于Stack Overflow的最常见场景是从一个视图控制器到下一个视图控制器的简单传递信息。

如果我们使用的是故事板,我们的第一个视图控制器可以覆盖prepareForSegue,这正是它的用途。调用此方法时会传入UIStoryboardSegue对象,并且它包含对目标视图控制器的引用。在这里,我们可以设置我们想要传递的值。

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

或者,如果我们不使用故事板,那么我们将从笔尖加载视图控制器。我们的代码稍微简单一些。

func showNextController() {
    let destination = SecondController(nibName: "SecondController", bundle: NSBundle.mainBundle())
    destination.myInformation = self.myInformation
    self.showViewController(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
}

还有其他几种情况。这个答案绝不会涵盖所有这些答案。

答案 1 :(得分:43)

这个问题一直存在。

一个建议是创建一个数据容器单例:一个在应用程序生命周期内只创建一次的对象,并在应用程序的生命周期内持续存在。

此方法非常适合您的应用中需要在不同类中提供/修改全局应用数据的情况。

在视图控制器之间设置单向或双向链接等其他方法更适合于在视图控制器之间直接传递信息/消息的情况。

(参见下面的nhgrif的答案,其他替代方案。)

使用数据容器单例,您可以向类中添加一个属性,该属性存储对您的单例的引用,然后在您需要访问时随时使用该属性。

您可以设置单例,以便将其内容保存到磁盘,以便在启动之间保持应用状态。

我在GitHub上创建了一个演示项目,演示了如何做到这一点。这是链接:

SwiftDataContainerSingleton project on GitHub 这是该项目的自述文件:

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,核心数据或其他各种方法来保存状态数据。

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

使用数据容器singleton:

此示例应用程序可以轻松使用数据容器单例。

有两个视图控制器。第一个是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都会更新。

答案 2 :(得分:8)

Swift 4

有很多方法可以快速传递数据。在这里,我将添加一些最好的方法。

1)使用StoryBoard Segue

Storyboard segues对于在源视图控制器和目标视图控制器之间传递数据非常有用,反之亦然。

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

    }

答案 3 :(得分:7)

另一种方法是使用通知中心(NSNotificationCenter)并发布通知。这是一个非常松散的耦合。通知的发送者不需要知道或关心谁在听。它只是发布通知并忘记它。

通知适用于一对多消息传递,因为可以有任意数量的观察者监听给定的消息。

答案 4 :(得分:2)

SWIFT 3:

如果你有一个带有识别segues的故事板,请使用:

Fold

虽然如果您以编程方式执行所有操作,包括在不同的UIViewControllers之间导航,那么请使用以下方法:

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

注意:要使用你需要制作UINavigationController的第二种方式,你要推动UIViewControllers,一个委托,它需要符合协议UINavigationControllerDelegate:

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

答案 5 :(得分:2)

我建议创建一个数据控制器实例并传递它,而不是创建一个数据控制器singelton。为了支持依赖注入,我首先要创建一个var timer = new Timer(o => Console.WriteLine("{0} Tick", DateTime.Now), null, 0, 6000); 协议:

DataController

然后我会创建一个protocol DataController { var someInt : Int {get set} var someString : String {get set} } (或当前适合的任何名称)class:

SpecificDataController

class SpecificDataController : DataController { var someInt : Int = 5 var someString : String = "Hello data" } 类应该有一个字段来保存ViewController。请注意,dataController的类型是协议dataController。通过这种方式,可以轻松切换数据控制器实现:

DataController

class ViewController : UIViewController { var dataController : DataController? ... } 中,我们可以设置viewController的AppDelegate

dataController

当我们移动到另一个viewController时,我们可以传递 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { if let viewController = self.window?.rootViewController as? ViewController { viewController.dataController = SpecificDataController() } return true } in:

dataController

现在,当我们希望为不同的任务切换数据控制器时,我们可以在override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { ... } 中执行此操作,而不必更改使用数据控制器的任何其他代码。

如果我们只想传递一个值,这当然是有点过分的。在这种情况下,最好采用nhgrif的答案。

通过这种方法,我们可以将视图形式与逻辑部分分开。

答案 6 :(得分:1)

正如@nhgrif在他的出色回答中指出的那样,VC(视图控制器)和其他对象可以通过很多不同的方式相互通信。

我在第一个答案中概述的数据单例实际上更多的是关于共享和保存全局状态而不是直接进行通信。

nhrif的回答让您可以直接从源发送信息到目标VC。正如我在回复中所提到的,它也可以将消息从目的地发送回源。

实际上,您可以在不同的视图控制器之间设置活动的单向或双向通道。如果视图控制器通过故事板segue链接,则设置链接的时间在prepareFor Segue方法中。

我在Github上有一个示例项目,它使用父视图控制器来托管2个不同的表视图作为子项。子视图控制器使用嵌入segue链接,父视图控制器连接prepareForSegue方法中每个视图控制器的双向链接。

你可以find that project on github(链接)。我在Objective-C中写了它,并且还没有把它转换成Swift,所以如果你对Objective-C感到不舒服,可能会有点难以理解

答案 7 :(得分: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")
}