假设我的Swift应用程序中有多个视图控制器,我希望能够在它们之间传递数据。如果我在视图控制器堆栈中处于多个级别,那么如何将数据传递到另一个视图控制器?还是在标签栏视图控制器中的标签之间?
(请注意,这个问题是个“响当当”。)它被问到太多了,所以我决定写一个关于这个主题的教程。请参阅下面的答案。
假设我的Swift应用程序中有多个视图控制器,我希望能够在它们之间传递数据。如果我在视图控制器堆栈中处于多个级别,那么如何将数据传递到另一个视图控制器?还是在标签栏视图控制器中的标签之间?
(请注意,这个问题是个“响当当”。)它被问到太多了,所以我决定写一个关于这个主题的教程。请参阅下面的答案。
Answers:
您的问题很广泛。提出每种情况都有一个简单的万能解决方案有点天真。因此,让我们看一下其中的一些场景。
在我的经验中,有关堆栈溢出的最常见问题是将信息从一个视图控制器简单地传递到下一个。
如果我们使用情节提要,我们的第一个视图控制器可以覆盖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
}
还有其他几种情况。此答案绝不能涵盖所有这些答案。
prepareForSegue
。太糟糕的是,在其他答案和题外话中,这个非常简单的观察结果却丢失了。
prepareForSegue
或以其他方式直接进行信息传递,然后在新手出现无法解决这些情况的情况时,与新手完全一样,然后我们必须教他们这些更全面的方法。
这个问题一直出现。
一个建议是创建一个数据容器单例:一个对象,在应用程序的生命周期内仅创建一次,并且在应用程序的生命周期内一直存在。
当您拥有需要在应用程序中的不同类之间可用/可修改的全局应用程序数据时,这种方法非常适用。
在视图控制器之间设置单向或两向链接等其他方法更适合于您在视图控制器之间直接传递信息/消息的情况。
(有关其他选择,请参见下面的nhgrif答案。)
使用数据容器单例,您可以在类中添加一个属性,以存储对单例的引用,然后在需要访问时随时使用该属性。
您可以设置单例,以便将其内容保存到磁盘,以便在两次启动之间应用状态保持不变。
我在GitHub上创建了一个演示项目,演示了如何执行此操作。链接在这里:
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中的键存储为字符串常量,它们是struct的一部分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都会更新。
斯威夫特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
它
我建议不要创建一个数据控制器实例并传递它,而不是创建一个数据控制器singelton。为了支持依赖注入,我首先要创建一个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
而不必更改使用该数据控制器的任何其他代码。
如果我们只想传递一个值,这当然是矫over过正。在这种情况下,最好选择nhgrif的答案。
通过这种方法,我们可以将视图分离为逻辑部分。
正如@nhgrif在其出色的回答中指出的那样,VC(视图控制器)和其他对象可以通过多种不同的方式相互通信。
我在第一个答案中概述的数据单例实际上更多地是关于共享和保存全局状态,而不是直接通信。
nhrif的答案使您可以将信息直接从源发送到目标VC。正如我在回复中提到的,还可以将消息从目标发送回源。
实际上,您可以在不同的视图控制器之间设置活动的单向或两向通道。如果视图控制器是通过情节提要Segue链接的,则建立链接的时间在prepareFor Segue方法中。
我在Github上有一个示例项目,该项目使用父视图控制器将2个不同的表视图作为子项承载。子视图控制器使用嵌入的segue链接,而父视图控制器在prepareForSegue方法中与每个视图控制器进行2通链接。
您可以在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,需要将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")
}