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


88

假设我的Swift应用程序中有多个视图控制器,我希望能够在它们之间传递数据。如果我在视图控制器堆栈中处于多个级别,那么如何将数据传递到另一个视图控制器?还是在标签栏视图控制器中的标签之间?

(请注意,这个问题是个“响当当”。)它被问到太多了,所以我决定写一个关于这个主题的教程。请参阅下面的答案。


1
尝试谷歌搜索代表
milo526

4
我发布了这个信息,以便为每天在此处出现的10,000个问题提供解决方案。查看我的自我解答。:)
Duncan C

抱歉,我反应太快了:)好,无法链接到此:)
milo526

2
别担心。您以为我是#10,001,不是吗?<grin>
Duncan C

4
@DuncanC我不喜欢你的答案。:(可以,作为所有方案的答案都可以接受... insomuchas,它适用于所有方案,但对于几乎所有方案也不是正确的方法。尽管如此,我们现在已经把它付诸实践将该主题上的任何问题标记为与此主题重复是个好主意吗?请不要
nhgrif 2015年

Answers:


91

您的问题广泛。提出每种情况都有一个简单的万能解决方案有点天真。因此,让我们看一下其中的一些场景。


在我的经验中,有关堆栈溢出的最常见问题是将信息从一个视图控制器简单地传递到下一个。

如果我们使用情节提要,我们的第一个视图控制器可以覆盖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
}

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


1
我还要补充一点,有时您希望通道将信息从目标视图控制器发送回源视图控制器。处理这种情况的一种常见方法是将委托属性添加到目标,然后在源视图控制器的prepareForSegue中,将目标视图控制器的委托属性设置为self。(并定义一个协议,该协议定义目标VC用于将消息发送到源VC的消息)
Duncan C

1
恩格里夫,我同意。对于新开发人员的建议应该是,如果您需要在情节提要上的场景之间传递数据,请使用prepareForSegue。太糟糕的是,在其他答案和题外话中,这个非常简单的观察结果却丢失了。
罗布(Rob)2015年

2
@Rob是 单例和通知应该是最后的选择。我们应该在几乎每种情况下都喜欢prepareForSegue或以其他方式直接进行信息传递,然后在新手出现无法解决这些情况的情况时,与新手完全一样,然后我们必须教他们这些更全面的方法。
nhgrif 2015年

1
这取决于。但是我非常非常担心使用应用程序委托作为我们不知道要放在哪里的代码的转储场所。这就是疯狂之路。
nhgrif

2
@nhgrif。谢谢你的回答。但是,如果您希望在4个或5个视图控制器之间传递数据怎么办。如果香港专业教育学院说4-5视图控制器管理客户端登录名和密码等,并且我想在这些视图控制器之间传递用户的电子邮件,是否有比在每个视图控制器中声明var然后在prepareforsegue中传递var更为方便的方法。有没有一种方法可以声明一次,并且每个ViewController都可以访问它,但同时也是一种良好的编码习惯?
lozflan

45

这个问题一直出现。

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

当您拥有需要在应用程序中的不同类之间可用/可修改的全局应用程序数据时,这种方法非常适用。

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

(有关其他选择,请参见下面的nhgrif答案。)

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

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

我在GitHub上创建了一个演示项目,演示了如何执行此操作。链接在这里:

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

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都会更新。


8
我不想对此进行否决,因为我认为您花时间来创建问题和答案作为资源非常好。谢谢。尽管如此,我认为我们对新开发人员提倡模型对象的单例非常不利。我不在“单身人士是邪恶的”阵营中(尽管菜鸟应该用谷歌搜索这个短语以更好地理解这些问题),但是我确实认为模型数据是单身人士的可疑/可争议的用法。
罗布(Rob)2015年

希望看到您撰写的关于双向链接的
精彩文章

@Duncan C您好,Duncan我在每个模型中都制作了静态对象,因此我可以从任何正确的位置获取数据,或者我必须遵循您的路径,因为这似乎非常正确。
Virendra Singh Rathore

@VirendraSinghRathore,全局静态变量是跨应用程序共享数据的最糟糕的方法。它们将应用程序的各个部分紧密耦合在一起,并引入了严重的相互依赖性。这与“非常正确”完全相反。
Duncan C

@DuncanC-此模式适用于CurrentUser对象-基本上是登录到您的应用程序的单个用户吗?thx

9

斯威夫特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
        }

    }

对于那些完全不知道将StackOverflow答案的Swift代码段放在哪里的Google员工,似乎假定您应该始终知道他们推断代码的位置:我使用选项1)从发送ViewControllerAViewControllerB。我只是在最后一个花括号之前将代码段粘贴在我的底部ViewControllerA.swift(当然,ViewControllerA.swift实际上是文件的名称)。“ prepare”实际上是给定类中的一个特殊的内置预先存在的功能(不执行任何操作),这就是为什么您必须使用override
velkoon

8

另一种选择是使用通知中心(NSNotificationCenter)并发布通知。那是非常松散的耦合。通知的发送者不需要知道或关心谁在听。它只是发布通知,而忘记了它。

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


2
请注意,使用通知中心引入的耦合可能松散了。它会使跟踪程序流变得非常困难,因此应谨慎使用。
Duncan C

2

我建议不要创建一个数据控制器实例并传递它,而不是创建一个数据控制器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的答案。

通过这种方法,我们可以将视图分离为逻辑部分。


1
您好,这种方法是干净的,可测试的,在小型应用程序中,我大部分时间都使用这种方法,但在大型应用程序中,并不是每个VC(也许甚至不是根VC)都需要依赖项(例如,在这种情况下为DataController)对于每个VC来说,仅要求传递依赖项似乎是浪费的。另外,如果您使用不同类型的VC(例如常规UIVC与NavigationVC),则需要将这些不同类型的子类进行子类化,仅添加该依赖项变量即可。您如何处理?
RobertoCuba

1

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

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

nhrif的答案使您可以将信息直接从源发送到目标VC。正如我在回复中提到的,还可以将消息从目标发送回源。

实际上,您可以在不同的视图控制器之间设置活动的单向或两向通道。如果视图控制器是通过情节提要Segue链接的,则建立链接的时间在prepareFor Segue方法中。

我在Github上有一个示例项目,该项目使用父视图控制器将2个不同的表视图作为子项承载。子视图控制器使用嵌入的segue链接,而父视图控制器在prepareForSegue方法中与每个视图控制器进行2通链接。

您可以在github(链接)上找到该项目。但是,我是用Objective-C编写的,但尚未将其转换为Swift,因此,如果您不熟悉Objective-C,可能会有些困难


1

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

    }
}

永不做self.delegate = self
malhal

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")
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.