假设我的 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
}
}
}
或者,如果我们不使用故事板,那么我们将从 nib 加载我们的视图控制器。那么我们的代码稍微简单一些。
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 上创建了一个演示项目,演示如何做到这一点。链接在这里:
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
、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
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) 使用委托方法
视图控制器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
}
}
ViewControllerA
发送到 ViewControllerB
。我只是在最后一个花括号之前将代码片段粘贴在 ViewControllerA.swift
的底部(其中 ViewControllerA.swift
实际上是您的文件的名称,当然)。 “prepare
”实际上是给定类中的一个特殊的内置预先存在的函数[什么都不做],这就是为什么你必须“override
”它
另一种选择是使用通知中心(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。正如我在回复中提到的,也可以将消息从目的地发送回源。
实际上,您可以在不同的视图控制器之间设置一个活动的单向或两向通道。如果视图控制器通过故事板 segue 链接,则设置链接的时间在 prepareFor Segue 方法中。
我在 Github 上有一个示例项目,它使用父视图控制器来托管 2 个不同的表视图作为子视图。子视图控制器使用嵌入 segue 链接,父视图控制器在 prepareForSegue 方法中与每个视图控制器连接 2 路链接。
您可以find that project on github(链接)。然而,我是用 Objective-C 编写的,并没有将它转换为 Swift,所以如果你对 Objective-C 不满意,可能会有点难以理解
斯威夫特 3:
如果您有一个带有已识别 segues 的故事板,请使用:
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 {
}
prepareForSegue
。太糟糕了,这个非常简单的观察在这里的其他答案和题外话中丢失了。prepareForSegue
或其他直接信息传输,然后当新手出现这些情况不起作用的场景时,我们就可以接受教他们这些更全球化的方法。