你如何在视图控制器和Swift中的其他对象之间共享数据?
说我在我的Swift应用程序中有多个视图控制器,我想能够在它们之间传递数据。 如果我在视图控制器堆栈中的几个级别,我如何将数据传递给另一个视图控制器? 或在选项卡栏视图控制器中的选项卡之间?
(注意,这个问题是一个“铃声”。)被问到这么多,我决定写一个关于这个问题的教程。 看到我的答案在下面。
你的问题非常广泛。 build议有一个简单的全面解决scheme,对每个场景都是有点天真的。 那么,我们来看看其中的一些场景。
根据我的经验,关于堆栈溢出最常见的情况是从一个视图控制器到下一个视图控制器的简单传递信息。
如果我们使用故事板,我们的第一个视图控制器可以覆盖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
标签之间共享信息。
在这种情况下,它实际上可能更简单。
首先,我们来创build一个UITabBarController
的子类,并为其提供属性,以便在各个选项卡之间共享任何信息:
class MyCustomTabController: UITabBarController { var myInformation: [String: AnyObject]? }
现在,如果我们从故事板构build我们的应用程序,只需将标签栏控制器的类从默认的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 }
还有其他几种情况。 这个答案决不会涵盖所有这些。
这个问题一直出现。
一个build议是创build一个数据容器单例:一个在应用程序生命周期中只创build一次的对象,并持续应用程序的生命周期。
这种方法非常适合您需要在应用程序的不同类中提供/修改全局应用程序数据的情况。
其他方法,如在视图控制器之间设置单向或双向链接,更适合于在视图控制器之间直接传递信息/消息的情况。
(有关其他select,请参见下面的nhgrif答案。)
使用数据容器单例,您可以向类中添加一个属性,该属性存储对您的单例的引用,然后在需要访问的任何时候使用该属性。
您可以设置您的单身人士,以便将其内容保存到磁盘,以便您的应用程序状态在启动之间保持不变。
我在GitHub上创build了一个演示项目,演示如何做到这一点。 链接在这里:
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
,核心数据或其他各种方法来保存状态数据。
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中的键存储为string常量,这些常量是结构DefaultsKeys
一部分,定义如下:
struct DefaultsKeys { static let someString = "someString" static let someOtherString = "someOtherString" static let someInt = "someInt" }
你可以像这样引用其中的一个常量:
DefaultsKeys.someInt
使用数据容器单例:
这个示例应用程序使得数据容器单例成为可能。
有两个视图控制器。 第一个是UIViewController ViewController
的自定义子类,第二个是UIViewController SecondVC
的自定义子类。
两个视图控制器都有一个文本字段,它们都将数据容器singlelton的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。
另一种select是使用通知中心(NSNotificationCenter)并发布通知。 这是一个非常松散的耦合。 通知的发送者不需要知道或关心谁在监听。 它只是发布通知,忘记了。
通知对于一对多消息传递是有利的,因为可以有任意数量的观察者监听给定的消息。
SWIFT 3:
如果你有一个确定的赛段故事板使用:
func prepare(for segue: UIStoryboardSegue, sender: Any?)
虽然如果你做了一切以编程方式包括不同的UIViewControllers之间的导航,然后使用该方法:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)
注意:要使用第二种方法创build您的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 } }
正如@nhgrif在他的优秀答案中指出的那样,VC(视图控制器)和其他对象可以相互通信有很多不同的方式。
我在第一个答案中概述的数据单例实际上更多地是关于共享和保存全局状态,而不是直接通信。
nhrif的答案可以让你直接从源发送信息到目标VC。 正如我在回复中提到的那样,也可以将消息从目的地发送回源。
实际上,您可以在不同的视图控制器之间设置一个活动的单向或双向通道。 如果视图控制器通过故事板链接链接,则设置链接的时间在prepareFor Segue方法中。
我有一个Github上的示例项目,它使用父视图控制器来承载2个不同的表视图作为子项。 子视图控制器使用embedded段连接,并且父视图控制器在prepareForSegue方法中与每个视图控制器build立双向链接。
你可以在github上find这个项目 (链接)。 然而,我把它写在Objective-C中,并没有把它转换成Swift,所以如果你在Objective-C中不舒服,可能有点难以遵循
而不是创build一个数据控制器singelton我会build议创build一个数据控制器实例并传递它。 为了支持dependency injection,我首先创build一个DataController
协议:
protocol DataController { var someInt : Int {get set} var someString : String {get set} }
然后,我会创build一个SpecificDataController
(或任何目前适当的名称)类:
class SpecificDataController : DataController { var someInt : Int = 5 var someString : String = "Hello data" }
ViewController
类应该有一个字段来保存dataController
。 注意dataController
的types是协议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
传递dataController
:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { ... }
现在,当我们希望为不同的任务切换数据控制器时,我们可以在AppDelegate
执行此操作,而不必更改使用数据控制器的任何其他代码。
如果我们只想传递一个值,这当然是过度的。 在这种情况下,最好与nhgrif的答案一起去。
通过这种方法,我们可以将逻辑部分的视图分开。